-
-
[推荐]看雪·安恒 2020 KCTF 春季赛 | 第三题设计思路及解析
-
2020-4-20 17:20 6645
-
第三题《寻踪觅源》历时2天,已于19号中午12点关闭攻击通道。此题共有3188人围观,最终共有12支战队攻破。
接下来让我们一起来看一下这道题的设计思路和详细解析吧。
出题团队简介
本题出题战队 HU1 :
lelfei,业余的crack爱好者。学生时代对电脑产生了浓厚的兴趣,经历了很长时间的游戏沉迷后,开始慢慢转向学习技术,工作后自学了ASM,VB,VC,HTML,ASP,Python等语言的入门。工作原因上网较少,对单机的逆向分析、算法比较感兴趣,但是由于缺少系统的学习,水平处于“入行较早层次较低知识较杂”的阶段。
设计思路
预设用户名:KCTF
预设SN:40017535dad01714402635730122
设计说明:发现一个Fabrice Bellard大牛写的开源js引擎quickjs,想学习一下源码,自己动手编译了一个WIN版本,发现可以直接把js脚本编译成EXE,并且生成的文件大小不到1M,用js写crackme是一个多么有意思的事!于是就有了这个CM,也帮大牛推广一下quickjs。膜拜作者Fabrice Bellard,知名开源项目FFMPEG和QEMU都出自他手,项目地址:https://bellard.org/quickjs/ 。
CM设计思路很简单,详见src.js。编译过程比较麻烦,需要先修改Makefile启用win编译环境支持,用MSYS编译出编译引擎,再修改qjsc.c源码启用win下把js编译成exe的功能,用qjsc.exe从js脚本生成C文件,最后用gcc编译出crackme.exe。修改过的Makefile和qjsc.c也提供了,可以对比原始文件看看改动的位置。
编译qjsc.exe命令:运行msys中的mingw32.exe并切换到工作目录,运行mingw32-make
使用qjsc.exe编译js文件:在msys环境中运行:./qjsc -fno-date -fno-eval -fno-string-normalize -fno-regexp -fno-json -fno-proxy -fno-map -fno-typedarray -fno-promise -o crackme src.js
修改C源码文件后重新编译命令:gcc -O2 -flto -D _GNU_SOURCE -I . -o crackme out9492.c ./libquickjs.lto.a -lm -static-libgcc -static-libstdc++ -Wl,-Bstatic -lstdc++ -lpthread -Wl,-Bdynamic
破解建议:想要分析出quickjs引擎的运行原理难度太大,应该比一般的VM引擎还要复杂,不过CM中并没有抹除quickjs的相关信息,敏感的人应该很快发现关键字,然后上网搜索到quickjs源码,从源码中分析出字节码的定义,反编译出js脚本,然后就简单了。目前这个项目还比较小众,估计没有现成的反编译工具,不过有源码参考,自己动手吧。
文件说明:KCTF2020Q1_by_lelfei.rar 参赛文件
源码文件:quickjs-2020-01-19.tar.xz 官方源码Makefile 修改后的Makefile文件,可以在win下用gcc编译qjsc.c 修改后的qjsc.c,可以在win下把js编译成exesrc.js js脚本源码out9492.c 包含js字节码的参赛文件源码kctf2020q1.py js脚本的python实现以及keygenCM设计说明.txt 本说明文件(附件下载链接:https://bbs.pediy.com/thread-258890.htm)
解析过程
本题解题思路由金箍棒配色的泰迪熊战队Riatre提供:
说明:本文分析的是修正多解 bug 前的 binary,但相关方法应当是通用的。
观察
题目给出了一个 Win32 控制台应用程序 lelfei.exe,简单看下会发现它是由 GCC: (i686-posix-dwarf-rev2, Built by MinGW-W64 project) 6.3.0 构建的,且带有调试符号,IDA Pro 可正常识别。 这些像是 JS_NewObjectFromShape 的符号非常扎眼,搜索一下会发现它是 QuickJS 中的。下载 QuickJS,构建出来(直接打 make 就可以)之后试一试,可以发现它有一个 qjsc,可以将一段 JavaScript 脚本编译为字节码后,和一个生成出的 stub 及 QuickJS 本身一起链接成一个可执行程序。使用 qjsc -e 可以让它输出嵌入字节码后,编译链接前的 C 代码。随便写一个 Hello World,这么做一下,观察得到的程序结构:
/* File generated automatically by the QuickJS compiler. */ #include "quickjs-libc.h" const uint32_t qjsc_hello_size = 78; const uint8_t qjsc_hello[78] = { 0x02, 0x04, 0x0e, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x06, 0x6c, 0x6f, 0x67, 0x16, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x10, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x6a, 0x73, 0x0e, 0x00, 0x06, 0x00, 0x9e, 0x01, 0x00, 0x01, 0x00, 0x03, 0x00, 0x00, 0x14, 0x01, 0xa0, 0x01, 0x00, 0x00, 0x00, 0x39, 0xdf, 0x00, 0x00, 0x00, 0x43, 0xe0, 0x00, 0x00, 0x00, 0x04, 0xe1, 0x00, 0x00, 0x00, 0x24, 0x01, 0x00, 0xcf, 0x28, 0xc4, 0x03, 0x01, 0x00, }; int main(int argc, char **argv) { JSRuntime *rt; JSContext *ctx; rt = JS_NewRuntime(); ctx = JS_NewContextRaw(rt); JS_SetModuleLoaderFunc(rt, NULL, js_module_loader, NULL); JS_AddIntrinsicBaseObjects(ctx); JS_AddIntrinsicDate(ctx); JS_AddIntrinsicEval(ctx); JS_AddIntrinsicStringNormalize(ctx); JS_AddIntrinsicRegExp(ctx); JS_AddIntrinsicJSON(ctx); JS_AddIntrinsicProxy(ctx); JS_AddIntrinsicMapSet(ctx); JS_AddIntrinsicTypedArrays(ctx); JS_AddIntrinsicPromise(ctx); JS_AddIntrinsicBigInt(ctx); js_std_add_helpers(ctx, argc, argv); js_std_eval_binary(ctx, qjsc_hello, qjsc_hello_size, 0); js_std_loop(ctx); JS_FreeContext(ctx); JS_FreeRuntime(rt); return 0; }
跟 lelfei.exe 基本是一样的。lelfei.exe 的 main 函数显著大,是因为构建时开了 LTO,自动 inline 了很多函数进来。
分析
简单阅读一下可以发现,lelfei.exe 在 qjsc 本身生成的逻辑上做了一点改动,加入了读入用户名、序列号的部分,读入的用户名和序列号会直接 patch 进程序内嵌的字节码。
简单看下代码可以发现程序中嵌入的字节码在 0x458040 处,长度为 946 字节。
我们将它提取出来,照猫画虎换进一个像是上面那样的 .c 中,然后编译并和 QuickJS 链接起来,看看能不能执行:
$ gcc -ggdb hello.c libquickjs.a -lm -ldl $ ./a.out [1] 2164345 segmentation fault ./a.out
呃……好像不可以。挂上调试器调一下(因为是源码
建的,可以源码调试 QuickJS),会发现看起来读入的字节码从中间某块往后就不太正确了。考虑到并不是全都不对,先不考虑是作者将它打乱了,而是考虑是否是版本不匹配。下载 QuickJS 的历史版本逐一尝试,尝试到 2020-01-19 版的时候:
$ gcc -ggdb hello.c libquickjs.a -lm -ldl $ ./a.out Error...
执行成功了。得到了在 Windows 下直接执行这个程序并输入错误序列号一样的反应。
我们手动将作者给出的用户名和序列号 patch 进字节码里重新编译,可以发现确实能输出 Success!。至此,原始的 lelfei.exe 已经没有了任何作用,可以抛在一边了。 事情进展到这种地步之后,基本的思路自然是在 QuickJS 中加入代码,将读取回来的字节码输出出来,然后逆向字节码。
但稍微读一下 QuickJS 的代码就会发现,它本身就带有这个功能,修改相关宏定义启用即可;另外,当函数对象是读入而非编译出来的时,并没有打印字节码,修改 JS_ReadObjectRec 函数在 BC_TAG_FUNCTION_BYTECODE 这个 case 最后加一行输出即可。
总之,对 quickjs.c 文件应用下面这个 patch:
85c85 < //#define DUMP_BYTECODE (1) --- > #define DUMP_BYTECODE (1) 99c99 < //#define DUMP_READ_OBJECT --- > #define DUMP_READ_OBJECT 33899a33900,33902 > #if DUMP_BYTECODE > js_dump_function_bytecode(ctx, b); > #endif
再照上文编译并运行程序,即可得到一段比较漂亮的输出(编辑掉了无关的部分,完整文件见附件):
0000: 02 0e 14 atom indexes { 0002: 04 75 6e string: 1"un" 0005: 04 73 6e string: 1"sn" 0008: 02 73 string: 1"s" 000a: 02 69 string: 1"i" 000c: 02 6a string: 1"j" 000e: 02 6b string: 1"k" 0010: 02 6c string: 1"l" 0012: 02 6d string: 1"m" 0014: 02 6e string: 1"n" 0016: 20 4b 43 54 46 32 30 32 30 51 31 6c 65 6c 66 65 69 string: 1"KCTF2020Q1lelfei" 0027: 40 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a string: 1"********************************" 0048: 14 63 68 61 72 43 6f 64 65 41 74 string: 1"charCodeAt" 0053: 18 66 72 6f 6d 43 68 61 72 43 6f 64 65 string: 1"fromCharCode" 0060: 0a 70 72 69 6e 74 string: 1"print" } 0066: 0e function { 0067: 00 06 00 9e 01 00 01 00 06 00 0a d6 05 01 name: "<eval>" args=0 vars=1 defargs=0 closures=0 cpool=10 stack=6 bclen=726 locals=1 vars { 0075: a0 01 00 00 00 name: "<ret>" } bytecode { 007a: 40 df 00 00 00 00 40 e0 <... 略 ...> 00 00 00 f1 cf 28 at 1, fixup atom: un <... 略 ...> at 719, fixup atom: s } debug { <... 略 ...> } cpool { <... 略 ...> } } s:1: function: <eval> locals: 0: var <ret> stack_size: 6 opcodes: check_define_var un,0 check_define_var sn,0 check_define_var s,0 check_define_var i,0 check_define_var j,0 check_define_var k,0 check_define_var l,0 check_define_var m,0 check_define_var n,0 check_define_var i,0 define_var un,0 define_var sn,0 define_var s,0 define_var i,0 define_var j,0 define_var k,0 define_var l,0 define_var m,0 define_var n,0 define_var i,0 push_atom_value KCTF2020Q1lelfei dup put_var un put_loc0 0: "<ret>" push_atom_value "********************************" dup put_var sn put_loc0 0: "<ret>" push_const8 0: 0n dup put_var m put_loc0 0: "<ret>" undefined put_loc0 0: "<ret>" push_0 0 dup put_var i drop 163: get_var i get_var un get_length lt if_false8 243 get_var m push_const8 1: 43n mul dup put_var m put_loc0 0: "<ret>" get_var m get_var BigInt get_var un get_field2 charCodeAt get_var i call_method 1 call1 1 add dup put_var m put_loc0 0: "<ret>" get_var i post_inc put_var i drop goto8 163 243: get_var Number get_var m push_const8 2: 127n mod call1 1 dup put_var l put_loc0 0: "<ret>" push_const8 3: 0n dup put_var n put_loc0 0: "<ret>" push_0 0 dup put_var s put_loc0 0: "<ret>" push_0 0 dup put_var k put_loc0 0: "<ret>" undefined put_loc0 0: "<ret>" push_0 0 dup put_var i drop 299: get_var i get_var sn get_length lt if_false 601 get_var sn get_field2 charCodeAt get_var i call_method 1 dup put_var j put_loc0 0: "<ret>" undefined put_loc0 0: "<ret>" get_var j push_i8 48 gte dup if_false8 363 drop get_var j push_i8 57 lte 363: dup if_true8 388 drop get_var j push_i8 97 gte if_false 601 get_var j push_i8 102 lte 388: if_false 601 get_var k post_inc put_var k put_loc0 0: "<ret>" get_var j push_i8 48 sub dup put_var j put_loc0 0: "<ret>" undefined put_loc0 0: "<ret>" get_var j push_i8 9 gt if_false8 447 get_var j push_i8 39 sub dup put_var j put_loc0 0: "<ret>" 447: get_var s push_i8 16 mul dup put_var s put_loc0 0: "<ret>" get_var s get_var j add dup put_var s put_loc0 0: "<ret>" undefined put_loc0 0: "<ret>" get_var k push_2 2 mod push_0 0 eq if_false8 586 get_var s get_var l xor dup put_var s put_loc0 0: "<ret>" get_var s push_4 4 sar push_i8 10 mul get_var s push_i8 16 mod add dup put_var s put_loc0 0: "<ret>" get_var n push_const8 4: 100n mul dup put_var n put_loc0 0: "<ret>" get_var n get_var BigInt get_var s call1 1 add dup put_var n put_loc0 0: "<ret>" push_0 0 dup put_var s put_loc0 0: "<ret>" goto8 586 586: get_var i post_inc put_var i drop goto16 299 601: undefined put_loc0 0: "<ret>" get_var m get_var n eq if_false8 627 push_const8 5: 18071254662143010n dup put_var n put_loc0 0: "<ret>" goto8 636 627: push_const8 6: 24706849372394394n dup put_var n put_loc0 0: "<ret>" 636: push_empty_string dup put_var s put_loc0 0: "<ret>" undefined put_loc0 0: "<ret>" 646: get_var n push_const8 7: 0n gt if_false8 713 get_var s get_var String get_field2 fromCharCode get_var Number get_var n push_const8 8: 127n mod call1 1 call_method 1 add dup put_var s put_loc0 0: "<ret>" get_var n push_const8 9: 127n div dup put_var n put_loc0 0: "<ret>" goto8 646 713: get_var print get_var s call1 1 set_loc0 0: "<ret>" return Error...
可以看到并不长,也很容易理解,对照着将验证逻辑还原回 JavaScript:
let un, sn, s, i, j, k, l, m, n; un = "KCTF2020Q1lelfei" sn = "********************************" m = 0n for (i = 0; i < un.length; i++) { m = m * 43n + BigInt(un.charCodeAt(i)) } l = Number(m % 127n) n = 0n s = 0 k = 0 for (i = 0; i < 256; i++) { z = i; z = (z >> 4) * 10 + (z % 16); } for (i = 0; i < sn.length; i++) { j = sn.charCodeAt(i) if ((j >= 48 && j <= 57) || (j >= 97 && j <= 102)) { k++ j -= 48 if (j > 9) { j -= 39 } } else break; s = s * 16 + j if (k % 2 == 0) { s = s ^ l; s = (s >> 4) * 10 + (s % 16); n = n * 100n n = n + BigInt(s) s = 0 } } if (m == n) { n = 18071254662143010n // Success } else { n = 24706849372394394n // Error.. } // 略去输出提示信息的部分
求解
验证逻辑比较简单,解起来应该也相对容易,但比较无语的是,下面这一行做的变换显然不是双射(怀疑作者是想打 0x10,手滑打错了):
s = (s >> 4) * 10 + (s % 16)
因此存在多解。 用上面逆向得出的 js 代码算出 KCTFKCTFKCTFKCTF 对应的 m 和 l:
let un, sn, s, i, j, k, l, m, n; un = "KCTFKCTFKCTFKCTF" m = 0n for (i = 0; i < un.length; i++) { m = m * 43n + BigInt(un.charCodeAt(i)) } l = Number(m % 127n) console.log(m, l)
得到 243377798925556026477314360n 66。 再算出序列号:
f = lambda x: (x >> 4) * 10 + (x % 16) m = 243377798925556026477314360 x = m p = [] while x: p.append(x%100) x //= 100 p = p[::-1] ans = [] for w in p: ans.append([x for x in range(256) if f(x) == w]) for z in itertools.product(*ans): print(xor(z, 66).hex())
接下来去提交多解即可。
主办方
看雪CTF(简称KCTF)是圈内知名度最高的技术竞技之一,从原CrackMe攻防大赛中发展而来,采取线上PK的方式,规则设置严格周全,题目涵盖Windows、Android、iOS、Pwn、智能设备、Web等众多领域。
看雪CTF比赛分为两个阶段:第一阶段是防守篇,防守方根据比赛要求制作题目,根据题目被破解的时间排名,被破解时间长者胜出。第二阶段为攻击篇,攻击第一阶段的题目,根据攻击成功的时间与题目排名,破解时间短且破解题目数多者胜。既给了防守方足够的施展空间,也避免过度浪费攻击方的时间。
从攻防两个角度看,都是个难得的竞技和学习机会。
合作方
杭州安恒信息技术股份有限公司(简称:安恒信息)成立于2007年,科创板股票代码:688023,一直专注于网络信息安全领域,公司主营业务为网络信息安全产品的研发、生产及销售,并为客户提供专业的网络信息安全服务。公司的产品及服务涉及应用安全、大数据安全、云安全、物联网安全、工业控制安全及工业互联网安全等领域。
公司秉承“助力安全中国、助推数字经济”的企业使命,以“诚信正直、成就客户,责任至上,开放创新,以人为本,共同成长”作为企业的价值观,不断提高核心技术创新能力,致力于成为一家具有优秀企业文化和社会责任感的新时代网络信息安全产品和服务提供商。
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。