2022-2023做过的一些有意思的逆向题
前言
这些题目本来是放在XDSEC内部论坛的,供学弟学妹练习的帖子,年末正好搬过来,作为这一年的记录。
第一周
0×00 春秋杯 冬季赛 godeep
用好idapython可以让你做得更优雅
WriteUp
题目逻辑比较清晰了:输入字符串 -> 转成二进制01串 -> 根据每一位的0、1选择不同的分支 -> 直到二叉树的终点。
难点在于节点很多,难以手动走遍整棵二叉树找出正确路径,此时就需要使用idapython做一些自动化的工作。
上面浮小云的做法是从终点往回倒推到起点,这里给出一个从前往后dfs拿到正确路径的做法:
给节点改个名先
|
|
总共5672个节点,获取所有节点和边
|
|
建立二叉树+dfs求路径
|
|
0×01 RCTF2022 huowang
这题最后有2个解,不用纠结
writeUp
放这道题主要想法是想让大家对unicron有一个初步的了解,放两篇感觉不错的文章(大家有其他好文章也可以贴一下捏):
整道题有两个check的逻辑,一个很明显的在0×40203D(基址0×400000),一眼地图,但是有很多条路。另一个就是unicron执行shellcode的逻辑。
unicron主要代码逻辑在这块:
|
|
流程大概是:
- 向unicorn内存空间写入此程序loc_145E010这个地方的shellcode
- 把input作为某一步的一个参数,覆盖了一个无用的函数loc_407B34
- 修改ID为30的寄存器(ESP)的值为0xFFFFF(开个栈)
- 添加了一个hook函数,check最后第一个寄存器(应该是RAX)值为1
- 调用uc_emu_start从0×145E0A9模拟执行到0×145E0FA
- 接着模拟执行从0×145E0FA到0×145E067
这里需要注意:unicorn自己的虚拟内存空间跟程序空间是无关的,且默认基址为0×400080。可以使用如dd等命令把它保存为一个新的二进制文件更好分析:dd if=./HuoWang of=output skip=17162256 bs=1
分析一下可以发现:
shellcode在不断循环自修改,每次检查输入的一位,实现了地图的第二个check。整个文件很大,就是因为后面附带了很长的shellcode执行所需的数据。
至于解法当时比赛时dx是手动走的,比较费时。一个更简单的做法是根据第一个check枚举出所有可能解,然后输入程序进行爆破,最后只剩下两个解,flag是合理的最短路径。这是这道题出得不好的一点,不用在意。
第二周
0×00 N1CTF Junior checkin-rs
你还害怕rust吗?
附件:checkin-rs
WriteUp
程序运行流程
怎么找到加密逻辑应该是这道题最难的部分,这个问题应该已经在这个帖子里说得很清楚了,这就不多说了。
main函数在re_checkin_rs::main
|
|
前面对输入的处理就不多说了,下面主要写写开启新线程进行加密的过程。
首先使用std::thread::spawn
创建了新线程,这里v9就是新线程中执行的闭包,主要是线程内部执行的函数,当然具体是啥在这段伪代码里看不出来。闭包里面也会捕获一些主进程中的变量,包括前面可以看到input相关的赋值操作。
然后使用std:thread::JoinHandle::join
阻塞当前线程直到新线程执行完毕,使用std::sync::mpsc::Receiver::recv
接收新线程传递来的消息(即v11),v11是1则将输出字符串赋值为congratulations
,是0则将输出字符串赋值为wrong
。这块对应了加密函数中最后的check部分:
|
|
新线程使用std::sync::mpsc::Sender::send
向主线程传递check成功与否的消息。
想要了解rust多线程消息传递实现细节的可以看 线程同步:消息传递 - Rust语言圣经(Rust Course)
加密算法识别
这道题第二个问题在于静态分析难度很大,看不懂在干嘛,但是这题是可以调试的。
调起来可以发现,这个函数的参数a1
就存储了输入。在函数的开始就会做一些输入的赋值操作:
|
|
_mm_loadu_si128
类似这种东西是Intel SSE指令集的一些操作,不清楚的直接百度就有。
最后的check是在这:
|
|
结合调试可以看出:v21是被加密的输入,n是比对的密文
输入总共32位,对输入的加密分为了两块
对后16位的加密在这:
|
|
对前16位的加密在这:
|
|
不要被这些东西吓住,动态看还是能看的,你会发现其实它就是实现了一个简单的input[i] ^= i
的加密
0×01 鹏城杯 2022 gocode
没错还是vm
附件:chall.exe
WriteUp
这段时间大家应该做了不少vm了,之所以再放这道,是因为这道并没有实现什么加密算法,也不是什么有规律的加密,必须手动写一个类似反编译器的东西,然后扔到z3里去解方程
善用python可以在进行vm逻辑的同时直接构造表达式丢给z3,省去了来回复制的麻烦事,比如这样:
|
|
第三周
0×00 CISCN 2023 moveAside
你能用不止一种方法做出来吗?
附件:moveAside.zip
WriteUp
demov
拖进ida发现全是mov,首先应该意识到这是mov混淆,然后尝试demov。一个工具:GitHub - leetonidas/demovfuscator: A work-in-progress deobfuscator for movfuscated binaries
编译有点麻烦,可能会有奇怪的报错,但装上很好用。
这道题使用这个工具不能得到完全正常的程序,但会好看很多,至少可以让你在调试到发疯之前看懂逻辑。
关于demovfuscator工具的安装
首先这三个依赖库一定得装好,这直接按照相应仓库readme make
、make install
应该问题不大
- libcapstone as the core disassembler
- libz3 to reason about the semantics of the mov code
- libkeystone for re-substitution
下面针对我遇到的报错给出一些解决办法,不一定通用,可以作参考。
-
装好依赖库后直接执行
make
命令进行编译,出现报错:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
clang++ -Wall -Wextra -pedantic -std=c++11 -O3 -c demov.cpp clang++ -Wall -Wextra -pedantic -std=c++11 -O3 -c ctlhlp.cpp In file included from ctlhlp.cpp:1: In file included from ./ctlhlp.hpp:8: ./ctlelem.hpp:11:2: error: unknown type name 'uint32_t' uint32_t name; ^ ./ctlelem.hpp:17:37: error: unknown type name 'uint32_t' ctlelem(ctlflow tp = CTL_INVALID, uint32_t o = 0, uint32_t d = 0); ^ ./ctlelem.hpp:17:53: error: unknown type name 'uint32_t' ctlelem(ctlflow tp = CTL_INVALID, uint32_t o = 0, uint32_t d = 0); ^ ./ctlelem.hpp:18:11: error: unknown type name 'uint32_t' ctlelem(uint32_t pos, uint32_t name, bool function = false); ^ ./ctlelem.hpp:18:25: error: unknown type name 'uint32_t' ctlelem(uint32_t pos, uint32_t name, bool function = false); ^ ./ctlelem.hpp:21:3: error: unknown type name 'uint32_t' uint32_t pos; ^ ./ctlelem.hpp:23:4: error: unknown type name 'uint32_t' uint32_t dst; ^ In file included from ctlhlp.cpp:1: In file included from ./ctlhlp.hpp:9: ./node.hpp:19:26: error: unknown type name 'uint32_t' node(std::string name, uint32_t pos, uint32_t label = 0); ^ ./node.hpp:19:40: error: unknown type name 'uint32_t' node(std::string name, uint32_t pos, uint32_t label = 0); ^ ./node.hpp:25:16: error: unknown type name 'uint32_t' void set_end(uint32_t end); ^ ./node.hpp:26:3: error: unknown type name 'uint32_t' uint32_t get_pos(); ^ ./node.hpp:27:3: error: unknown type name 'uint32_t' uint32_t get_end(); ^ ./node.hpp:30:18: error: unknown type name 'uint32_t' void set_label(uint32_t l); ^ ./node.hpp:34:3: error: unknown type name 'uint32_t' uint32_t pos; ^ ./node.hpp:35:3: error: unknown type name 'uint32_t' uint32_t label; ^ ./node.hpp:36:3: error: unknown type name 'uint32_t' uint32_t end; ^ ./node.hpp:44:34: error: unknown type name 'uint32_t' node* get_node(std::string name, uint32_t pos); ^ In file included from ctlhlp.cpp:1: ./ctlhlp.hpp:16:25: error: use of undeclared identifier 'uint32_t' std::vector<std::pair<uint32_t, uint32_t>> get_blocks(); ^ ./ctlhlp.hpp:18:12: error: use of undeclared identifier 'uint32_t' std::map<uint32_t, ctlelem> elems; ^ fatal error: too many errors emitted, stopping now [-ferror-limit=] 20 errors generated. make: *** [Makefile:35:ctlhlp.o] 错误 1
提示
uint32_t
无法识别,手动编辑./ctlhlp.hpp
添加头文件#include <cstdint>
-
再次编译,继续报错:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
clang++ -Wall -Wextra -pedantic -std=c++11 -O3 -c demov.cpp clang++ -Wall -Wextra -pedantic -std=c++11 -O3 -c ctlhlp.cpp clang++ -Wall -Wextra -pedantic -std=c++11 -O3 -c ctlelem.cpp In file included from ctlelem.cpp:1: ./ctlelem.hpp:11:2: error: unknown type name 'uint32_t' uint32_t name; ^ ./ctlelem.hpp:17:37: error: unknown type name 'uint32_t' ctlelem(ctlflow tp = CTL_INVALID, uint32_t o = 0, uint32_t d = 0); ^ ./ctlelem.hpp:17:53: error: unknown type name 'uint32_t' ctlelem(ctlflow tp = CTL_INVALID, uint32_t o = 0, uint32_t d = 0); ^ ./ctlelem.hpp:18:11: error: unknown type name 'uint32_t' ctlelem(uint32_t pos, uint32_t name, bool function = false); ^ ./ctlelem.hpp:18:25: error: unknown type name 'uint32_t' ctlelem(uint32_t pos, uint32_t name, bool function = false); ^ ./ctlelem.hpp:21:3: error: unknown type name 'uint32_t' uint32_t pos; ^ ./ctlelem.hpp:23:4: error: unknown type name 'uint32_t' uint32_t dst; ^ ctlelem.cpp:5:30: error: unknown type name 'uint32_t' ctlelem::ctlelem(ctlflow tp, uint32_t o, uint32_t d) { ^ ctlelem.cpp:5:42: error: unknown type name 'uint32_t' ctlelem::ctlelem(ctlflow tp, uint32_t o, uint32_t d) { ^ ctlelem.cpp:9:3: error: use of undeclared identifier 'lab'; did you mean 'labs'? lab.name = o; ^~~ labs /usr/include/stdlib.h:862:17: note: 'labs' declared here extern long int labs (long int __x) __THROW __attribute__ ((__const__)) __wur; ^ ctlelem.cpp:9:6: error: member reference base type 'long (long) noexcept(true)' is not a structure or union lab.name = o; ~~~^~~~~ ctlelem.cpp:10:3: error: use of undeclared identifier 'lab'; did you mean 'labs'? lab.function = d ? true : false; ^~~ labs /usr/include/stdlib.h:862:17: note: 'labs' declared here extern long int labs (long int __x) __THROW __attribute__ ((__const__)) __wur; ^ ctlelem.cpp:10:6: error: member reference base type 'long (long) noexcept(true)' is not a structure or union lab.function = d ? true : false; ~~~^~~~~~~~~ ctlelem.cpp:12:3: error: use of undeclared identifier 'dst' dst = d; ^ ctlelem.cpp:16:18: error: unknown type name 'uint32_t' ctlelem::ctlelem(uint32_t pos, uint32_t name, bool function) { ^ ctlelem.cpp:16:32: error: unknown type name 'uint32_t' ctlelem::ctlelem(uint32_t pos, uint32_t name, bool function) { ^ ctlelem.cpp:19:2: error: use of undeclared identifier 'lab'; did you mean 'labs'? lab.name = name; ^~~ labs /usr/include/stdlib.h:862:17: note: 'labs' declared here extern long int labs (long int __x) __THROW __attribute__ ((__const__)) __wur; ^ ctlelem.cpp:19:5: error: member reference base type 'long (long) noexcept(true)' is not a structure or union lab.name = name; ~~~^~~~~ ctlelem.cpp:20:2: error: use of undeclared identifier 'lab'; did you mean 'labs'? lab.function = function; ^~~ labs /usr/include/stdlib.h:862:17: note: 'labs' declared here extern long int labs (long int __x) __THROW __attribute__ ((__const__)) __wur; ^ fatal error: too many errors emitted, stopping now [-ferror-limit=] 20 errors generated. make: *** [Makefile:32:ctlelem.o] 错误 1
手动编辑./ctlelem.hpp
添加头文件#include <cstdint>
同样的还有./node.hpp
。
-
全部添加完成后,执行
make
即可正常编译出二进制文件(只有一个warning)1 2 3 4 5 6 7 8 9
❯ make clang++ -Wall -Wextra -pedantic -std=c++11 -O3 -c ctlhlp.cpp clang++ -Wall -Wextra -pedantic -std=c++11 -O3 -c node.cpp In file included from node.cpp:1: ./node.hpp:38:8: warning: private field 'del' is not used [-Wunused-private-field] bool del; ^ 1 warning generated. clang++ -Wall -Wextra -pedantic -std=c++11 -lcapstone -lkeystone -lz3 -lssl -lcrypto elfhlp.o demov.o test.o memhlp.o dishlp.o utils.o ctlhlp.o ctlelem.o node.o asmhlp.o -o demov
-
但是运行程序提示找不到一个库
1 2
❯ ./demov ./demov: error while loading shared libraries: libkeystone.so.0: cannot open shared object file: No such file or directory
搜索发现有这个库文件(在/usr/local/lib/libkeystone.so.0),直接尝试patchelf
1
patchelf --replace-needed libkeystone.so.0 /usr/local/lib/libkeystone.so.0 ./demov
即可成功运行程序
1 2 3 4 5 6 7 8 9 10 11 12
❯ ./demov -h The demovfuscator supports the following parameters: ./demov [-h] [-i symbols.idc] [-o patched_bin] [-g cfg.dot] obfuscated_input -h Use for a description of the options -i Derive symbols from the input bin and store them into symbols.idc -o Generate a UNIX dot compatible file containing the control flow graph (might be easier to read than IDA's graph view) Convert the .dot file to something usable by cat cfg.dot | dot -Tpng > cfg.png
下面说三种解法。
0-神仙解法
[CISCN 2023 初赛]moveAside_FeiJiNcFan的博客-CSDN博客
同样的mov混淆,点名强网拟态2022决赛题目movemove。当时是这么做的:
1-调试看逻辑
建议先用上面的工具demov再调,会好看一些,且把原程序通过异常实现的跳转优化成了直接的jmp,舒服很多。
定位输入存储地址
我们调试的目的是观察输入的变化,搞明白程序是如何check的,所以第一步就要锁定输入存储的地址。
这里可以在函数列表找到__isoc99_scanf
这个函数,所以直接在这个函数里下断点。
调起来在断点成功断下,由于32位程序使用栈传参,故直接看右下角就能看到scanf的参数,即输入存储地址unk_8600198
在输入存储地址下读写断点
与平常在代码段下断点不同,这次我们直接在数据段下断点,按下F2弹出对话框,勾选Read和Write,点击OK即可。每当有代码访问这个地址的数据时,就会触发断点。
F9执行直到关键代码
下好断点直接F9,每次断下记得先看右下角堆栈窗口,前两次会断在scanf、puts这些函数里面,直接继续F9即可,比如这个:
直到跳回text段执行一些神必代码:
这里就是关键加密位置了。
多调两遍识别加密逻辑
到这里如果你看的是demov之后的程序,直接F5是可以看到一个完整的函数的,虽然还有很多很乱的取址操作,但可以跟汇编佐证着看。
|
|
调着看汇编的话会发现还有很多重复的部分,重点在于每一步抓住输入量参与了什么运算,整个代码已经比较清晰了。
整个逻辑主要有三步:
- 通过地址相加实现加法(lea edx, [eax+ecx])
- xor进行异或
- strcmp进行比较
这里看右下角栈窗口也能清楚地看到比较的参数。
整个的逻辑就是每一位check(input[i] + 0x18) ^ 0x19 == enc[i]
,逆过来解密即可。
脚本
|
|
2-hook strcmp + 爆破
LD_PRELOAD hook
LD_PRELOAD
是linux系统下的一个环境变量,对于动态链接程序,LD_PRELOAD指定的动态链接库中的符号会先于标准库或其他库被使用。
简单来说就是可以自定义函数替代程序中使用的外部库中的函数,达到hook的效果。
这题由于是逐字节加密逐字节比较,故可以hook掉strcmp,加上一些输出,再通过输出判断输入正确的字符数,爆破即可。
脚本
hook.c定义strcmp函数:
|
|
编译成so文件:
|
|
爆破脚本:
|
|
0×01 陕西省省赛 2023 WebAssembly
考验你的耐心
附件:wasm.zip
WriteUp
关于wasm的逆向,一般来说有三条路:
- 使用wabt工具包中的
wasm2c
将wasm转化成C,再拿gcc编译成二进制,拖进ida分析 - jeb等直接支持wasm反编译的软件
- 浏览器调试
也可以结合使用,前两个用来静态分析,看懂大体逻辑。第三个用来动态分析,确定一些特殊变量的值/不清楚的逻辑。
第一个的效果很多时候不如第二个,调试一般比较麻烦。
这道题这里主要使用jeb进行静态分析。拖进jeb,main函数还很清晰。
|
|
主要加密在__f8
函数中:
|
|
有点丑,但是能看。
flag共32位,key114!514!
,enc91fba5ccfef6e0905eeeb47940d25543c286b10de778fbb268ab7580414c0758
整个加密逻辑如下:
- 每次取8位,第i位异或上
key[i%8]
|
|
- 取出的8位进行114轮
__f7
系列加密
|
|
- 将结果转换为16进制字符串
|
|
最难看的在__f7
函数:
|
|
最好是把这个函数复制出来,在编辑器比如vscode中做一些变量/字符串替换,会比在jeb里硬盯着看舒服一点。
另外一个特征应该注意到整个函数是非常规整的,基本可以确定是具有对称性的算法,于是在不确定的点上就可以大胆猜测。最终长这样:
|
|
最终脚本:
|
|
第四周
0×00 强网拟态2023决赛 movemove
上周的方法你学会了吗?
进阶:程序是如何实现cmp,对比加密明文和密文的?
附件:https://pan.baidu.com/s/1b2U_hawqIoBv8RSCBiHybA?pwd=y9d6
WriteUp
放这题主要是想让大家加深一下对mov混淆的记忆,以及进阶
来考验一下大伙的调试能力和耐心。
但由于上周写wp的时候提到了这题,大家都先入为主地认为一个异或有什么好调的,于是都没有细看。
上周那题最后加密输入和密文的比较直接调用了strcmp
函数,但这道题实际是进行了巧妙的计算来判断两个值是否相等。
调试前最好还是先demov一下,我们先从后往前理清最终需要什么样的条件才能check通过。
最后一段判断的代码:
|
|
可以锁定eax,至少需要满足eax最低一个比特位为1(其实就是等于1,后面可以确定),才会不往下赋值error,eax的值来自于dword_81F4B50。
追查dword_81F4B50,它是在这块被赋值的:
|
|
dword_81F4B50的值来自于al,al来自于alu_false数组(第一项是1,后面全0),想要dword_81F4B50为1,al就要为1,运行到080497E9时eax就要是0,继续向上推,可推出alu_s必须为0
alu_s的赋值来自于这块:
|
|
这其实就是输入与密文计算得到的结果,具体如何计算,我们从头开始,把整个调试过程中的对数据的操作梳理一遍:
- 输入异或0×37
|
|
- 密文取反
|
|
- 前两步结果相加再加1存入alu_s
|
|
故对于每一位输入,check的完整表达式是:
|
|
由于正数的补码(符号位不变,其余位取反,再加1)为其本身,此处相当于符号位也取反,则结果为原数的相反数,即相加得0。故上式可化为:
|
|
于是就只剩下一个异或了。
0×01 CatCTF 2022 CatFly
猫猫会告诉你答案!
hint:程序没有输入,跑起来经过足够长时间猫猫上面的乱码就会变成flag。
hint2:抄算法跑一分钟内出flag,flag格式:CatCTF{}
附件: https://pan.baidu.com/s/1WC13f_cETvr141NFF6JMaA?pwd=822n
WriteUp
这种题也算是挺新颖的,不过刚看见可能有点懵。
程序跑起来猫猫上面会有一串乱码,时间足够长就会变成flag。做题思路就是找到打印上面这串乱码的逻辑,抄下来自己跑,直到开头符合flag的格式即可。
注意中间不要有输出,要不然会慢很多。
放一个之前写的wp,粘一个最终脚本:
|
|
第五周
还热乎的巅峰极客2023
0×00 g0Re
神必的OKXX到底是什么?
附件: https://pan.baidu.com/s/1nXY9lKvoc3pVtHwZXCbe1g?pwd=q5b6
WriteUp
这道题主要点在upx魔改,但是很简单,只要将OKXX
改成UPX!
即可正常upx -d
剩下的逻辑打开main函数就很纯粹了,正常逆了就行,贴个脚本:
|
|
0×01 m1_read
dx完全找不到加密在哪
附件: https://pan.baidu.com/s/12YGN84CaTo9QduaYc_eO1Q?pwd=3ji5
WriteUp
白盒AES识别
这是这道题首要的问题,如果你逆过相关算法肯定能很快地反应过来,如果没见过那就只能根据零星的特征进行猜测了。
定位加密逻辑位置
面对一个无从下手的二进制文件,这都是我们通常的第一步。一般都是通过关键字符串/函数进行交叉引用,或者对于很多库函数的程序,需要能辨别出来哪些是出题人写的哪些是库函数,这就需要经验的积累了。
这道题根据题目得知是写卡程序,直接可以定位SCardTransmit
这个函数,再交叉引用结合整个函数进行分析,可以大致猜测加密就在sub_140004BF0
函数:
findcrypt查常量
这道题使用findcrypt插件可以直接查出来一堆AES的S盒。AES的S盒是精心设计的,一般来说,不会被魔改,能直接找到S盒,多半也可以确定是和AES有关。(这里要注意有些程序直接把一整个第三方加密库静态编译到程序中,实际并没有使用到,注意甄别)
分析加密函数内容
|
|
整个算法复杂度很高,有反复多样的查表替换操作,以及后半部分的异或0×66.
如果这样一个算法让人去逆,那么估计全场零解不在话下。我们第一想法是这玩意应该是某个著名的算法,然后就可以开猜了。
当时直接上传了binary ai,然后它告诉我有百分之七十多的可能是blake哈希,然后我信以为真,以为这是什么数据包的校验哈希之类的,然后就寄了。所以结论就是,别用binary ai,真不行。
赛后测试了一下,你把这块伪代码直接发给gpt3.5,然后告诉他这可能是一种著名的加密算法,它也会猜AES,不得不说,gpt在很多时候还是很有用的。
越是复杂的算法,魔改的可能性就越小,尤其是AES这种,整个算法是精心设计的,稍有更改都可能会导致整个算法解密的不准确性,这道题也不例外。
所以我们的任务就是拿到AES的密钥,即可进行解密。
DFA攻击白盒AES
详细攻击原理有兴趣可以去找点资料看看,这里不涉及密码学原理,只写一下具体流程。
我们要做的无非三步:
-
对于给定的明文(控制函数输入参数),在倒数第二轮列混合之前更改密文,构造缺陷数据,重复多次拿到多组错误数据。对于给定的明文,不进行更改拿到正确密文。
-
根据正确密文和缺陷密文获取第十轮密钥。
-
根据第十轮密钥恢复出初始密钥。
对于第一步,由于这道题无法正常调试,所以需要一些特殊手段执行程序,加密过程中动态修改密文,并获取最终密文。这里有多种方式可以选择,我这里使用qiling、angr、frida均尝试成功,具体脚本可以看这:常用模拟执行、符号执行、插桩hook框架概览与比较
对于第二步,将正确密文和缺陷密文写入tracefile
直接使用phoenixAES库即可:
|
|
输出:
|
|
对于第三步,还是使用现成工具即可。
最终拿到第一轮密钥,进行解密即可。
第六周
0×00 *CTF 2023 ezcode
PWSH是什么?
附件:https://pan.baidu.com/s/1JvwyLrWtPW3WUCLrEni9FA?pwd=brx6
WriteUp
一眼Powershell,但是加了混淆。其实笔者对这玩意并不了解,在这放一个仅仅能做的方法来抛砖引玉。欢迎了解更多的师傅在下面补充!
思路非常简单,vscode打开直接开调,但是只有一行,不好下断点,故可以在下面加一行echo done
或者其他什么东西都行,然后断在这一行,看左边local变量$@*
里面一堆[CHar]其实就是ascii码,转换一下就能发现是个python脚本。剩下看着源码逆个算法就不是啥问题了。
0×01 corCTF 2023 crabheartpenguin
KO!!
hint1:一些基础知识推荐阅读 a3的文章
hint2:此题原本有远程环境,现在做这题尝试与内核模块交互拿到test flag即可(搜字符串直接拿flag不算)
附件:https://pan.baidu.com/s/1WF9xuUY6mdJr2ZB9b4BNbg?pwd=4i78
WriteUp
做题之前
内核(通常是内核模块)逆向是有一定门槛的。需要你首先对内核编译、文件系统构建(通常使用busybox)、qemu基本指令有基本的认识,这其实不难,自己多动手就行了。逆向题一般不会涉及多少的内核机制原理,所以难度更大的题还是在二进制本身做文章,比如这道题使用rust编写,本身就已经很难看了,再整点混淆啥的,估计很可能就0解了。
内核题出题也是有门槛的,所以在国内比赛很少见到。随便写个异或移位拿个不知道哪来的编译器一编译丢给选手嗯看不是更爽(x)。但是在国际赛事中还是能经常看到的,最近的例子比如D3CTF 2023, corctf 2023,以及上周末刚打完的project sekai ctf都有出现。
说到这里,其实我觉得你应该更认真地思考一下要不要继续尝试这类题。首先,能接触到这类题说明你的水平已经不低了,你应该发现,逆向这个分类底下还可以分很多个方向,每个方向都足够你研究大学四年。你应该有大致的认知你对哪方面更感兴趣、更想研究下去,想要做内核题你首先就要在环境配置上花费很多时间,有没有必要花这个时间你应该有所权衡。希望你继续尝试下去的理由不仅仅是ctf里会有这种题
接下来开始正题,一些最基本的东西你都可以在a3的blog中学到,遇到问题多问搜索引擎/AI就好,这里不再赘述。
静态分析
第一次看这种题,最简单的办法就是顺着函数列表挨个点一遍,相比于常规逆向题,内核逆向中的函数数量一般少得多。
注意两个名字最正常的两个函数init_module
和cleanup_module
,这是内核模块的初始化函数和清理函数,所以init_module
可以看作是main函数,是整个程序的入口点。
|
|
可以看到,首先调用了crabDev_kernelmoduleinit这个函数初始化了crab模块,下面就是一些错误处理和rust的drop_in_place,忽略即可。
|
|
这个函数里要关注RNvMNtCs8YfjL7j61RY_6kernel6chrdevNtB2_4Cdev3add
创建了一个字符设备
。这一点在成功跑起来qemu虚拟机之后就可以看到/dev/crab
。
另外RNvMNtCs8YfjL7j61RY_6kernel6chrdevNtB2_4Cdev5alloc
这里就是对这个字符型设备注册了一系列回调函数,具体点进去qword_2260
就可以看到,包括4个设备读写的回调。当我们与这个字符型设备/dev/crab
交互时,就会触发这一系列回调函数。
其中read_iter_callback
和read_callback
同时存在,是为了兼容不同内核,二者同时存在时,内核会选择一个执行,忽略另一个。write_callback同理,同时我们也可以看出带iter和不带iter的函数功能是完全一致的。
可以发现,在read_callback
中,会判断*(_QWORD *)(*a1 + 192LL) + 84
这个标志位,为1则将flag写入文件(设备),在write_callback
中,会进行一些check的逻辑。
到这里我们可以先尝试与/dev/crab进行交互看看效果(就像我们普通题运行看看一样),用简单的open、read、write就行,由于qemu环境中没有库支持,所以我们这选择写静态编译的C程序,然后重新打包进文件系统,就能在qemu中运行了。
我们会发现它生成了许多emoji表情,准确来说是6种,这就是make_prompt
函数干的事了,make_prompt
函数调用一次,会生成10个随机的emoji,对应的字符串在0×24B0这个位置。
再看伪代码,我们可以发现最终check输入的逻辑在这块:
|
|
这就很明显了,要保证字符串长度和内容都相同,而off_24B0中都是那些emoji对应的单词,可以猜到我们需要识别程序随机生成的emoji并转化成字符串发回来。知道了大致的逻辑,剩下就是一些细节问题了。
往上看,上面两个switch-case:
|
|
这些case的值其实就是那6个emoji的unicode值,用python直接print(chr(128049))
就能看到具体是啥。
上面还有一个重要的点在这:
|
|
byte_2490是-
,实际上,在我们的输入中要用-
分割每一个单词。
最后一个也是最重要的点在bcmp前一行:
|
|
其中*v24
是代表轮数的循环变量,如果连续11轮正确的话就会成功,这段逻辑在这:
|
|
这里的v4同样是*(_QWORD *)(*a1 + 192LL)
,和上面read_callback中检查的值相同,故只要这里设为1,就能get flag
v26就是emoji的序号,后面这一串暂且把*v24 + v26
看作一个整体,类型转换可有可无,这里省略了,即:
|
|
其实就是num % 6
的逻辑,注意这里&0xfffe
的作用。前面再乘2只是寻址需要,与逻辑无关。
总结一下:程序会随机生成11组、每组10个emoji,我们需要读取这些emoji,计算(轮数 + emoji下标)% 6
对应的字符串,返回给程序,11轮验证通过程序就能get flag
动态调试
如果你能静态分析得到上面的结论,那么你已经可以动手写exp了,但这实际是很困难的,调试能帮我们简化或者验证猜想的逻辑。但是想要调试内核模块,首先得折腾一番环境。一般会选择qemu+gdb+pwndbg/gef
这样的组合,搭建过程推荐阅读这篇文章Kernel pwn CTF 入门 - 1-安全客 - 安全资讯平台),跟着来搭建好环境应该问题不大,遇到特殊问题再去解决。下面主要结合这道题举例说明一下,这篇文章涉及到的内容不会再赘述,请仔细阅读并动手尝试。
特别的,这里推荐一个插件decomp2dbg,可以将ida反编译出的伪代码实时显示在gdb中,同时可以使用gdb命令查看ida中定义的临时/全局变量值,使用ida中自定义的变量/函数符号,十分方便。下面有图示。
启动脚本/init脚本修改
这里的启动脚本指run.sh
,init脚本指文件系统根目录下的init脚本。
-
为了方便调试,我们最好关闭
kaslr
(内核地址随机化),即-append "nokaslr"
。 -
为了方便调试,可以选择将init脚本前两行
insmod crab; rm crab.ko
删去,等qemu启动完成后手动加载内核模块。 -
将最后一行gid由1000改为0,使用root登录,防止出现权限问题。即
setsid /bin/cttyhack setuidgid 0 /bin/sh
与模块交互时触发断点
这种情况发生在insmod crab
之后,加载模块后,在gdb中下断点即可,手动/exp读写crab设备时就会触发断点,在gdb中就可以进行调试了。
比如我们可以断在bcmp处查看比较的值:
下面这可以看到同步过来ida的伪代码(decomp2dbg插件)、栈数据、调用栈信息
上面有汇编代码和各种寄存器的值,这里由于vmlinux的符号没恢复,所以只能看到call一个地址,但这不影响分析
bcmp的三个参数即在rdi、rsi、rdx中,rdi是我们的输入,rsi是比较的内容,rdx为6是长度,可以说非常清晰了。在其他地方下断点还能拿到更多信息,帮助分析程序逻辑。
在模块初始化函数触发断点
由于在模块加载之前,模块地址还不存在,而一旦模块加载完成,又无法下断点重新断回去。所以这种情况我们只能断在模块被加载入内存之后,模块初始化函数开始执行之前。
a3告诉我的方法是拦截sys_init_module
,这个syscall的时机恰好就是我们想要的,但是我尝试发现会报错warning: Error inserting catchpoint 1: Your system does not support this type
,不知道是我的操作有问题还是这题的内核有问题。
后来又发现,模块加载之前会调用一个do_init_module
函数,所以我们可以先断在这个函数。如果vmlinux成功恢复符号的话直接break do_init_module
就行,但是这道题的内核我使用vmlinux-to-elf
解压报错退出了,目前原因不明,故需要手动查一下这个函数的地址,具体命令如下:
|
|
此时在gdb中break 0xffffffff810c0530
,然后手动insmod即可断下来。此时模块代码已被加载入内存中,且我们可以使用add-symbol-file直接在gdb中导入符号信息,所以直接break init_module
,再continue即可断在init_module函数中:
exp
只要能理解题目逻辑,写exp应该不是啥问题,注意静态编译就行。贴一个在这:
|
|
运行效果:
第七周
0×00 WMCTF 2023 ezAndroid
AES? AES! AES?!?!
附件: https://pan.baidu.com/s/1uPmYm38lZMroKjqRetXWPQ?pwd=cc8a
WriteUp
java层很简单,输入用户名、密码,分别进行check,两个check函数都是native的,直接看lib即可。
lib加了OLLVM,很难看,但是能看,有x86的,可以直接模拟器开调
先看JNI_OnLoad
:
|
|
这块调试起来看得更清楚,Checkusername
和Check2
正是java层调用的函数,对应的内容就是check_username和check_pass
先看check_username
首先会有一个长度的检测,需要username长度等于10:
|
|
这里会生成字符串12345678
,重复32次作为后面RC4的key:
|
|
这里实际是一个魔改RC4的逻辑,魔改的点在于将生成的第i个流密钥异或上i:
|
|
|
|
最后和固定字符串比较:
|
|
由此可以先给username逆出来了:
|
|
得到username是Re_1s_eaSy
再看check_pass,加了ollvm就是一坨,但是能通过一些特征去猜,findcrypt能查到AES的s盒,基本就已经可以确定是aes了
首先会检查password的长度为16,和上面如出一辙:
|
|
主要逻辑在这部分:
|
|
进到aes_encrypt函数中,能看到更多aes的细节,比如:
sub_7FF9545D0080
这个函数中,有明显的44轮循环
|
|
比较容易确定是AES的密钥扩展
一个大循环中连续调用多个函数,基本能猜到是AES各个步骤:
|
|
调试断在aes_encrypt这个函数里就可以清晰的看到参数:
|
|
密文动调或是直接看_init_array中的赋值函数都可以很容易地拿到,但是直接尝试解密会得到乱码
大部分题目中的AES是不会被魔改的,因为一不小心容易玩坏了没法解密,但这道题是个例外,它在_init_array里修改了s盒(打乱了顺序):
|
|
我们需要根据S盒算出逆S盒,才能进行解密:
|
|
最后随便找个AES的实现,改掉S盒和逆S盒,跑一遍即可,下面贴一个个人看着最舒服的版本:
|
|
0×01 WMCTF 2023 RightBack
不会有人和我一样当阅读题做吧(
附件:https://pan.baidu.com/s/1LID0fxXp5N72Snuc60D9SQ?pwd=5aee
WriteUp
pyc、python3.9、花指令,buf叠满了。
去花 + 反编译 + 逆源码
这是PZ师傅的预期解,具体可以直接看PZ师傅的解析,同时可以参考源码仓库进行学习
我这里就不再赘述了。
阅读题 + pyc调试
必须得说,这道题在这场比赛中,绝大部分师傅都当阅读题做了,包括我。赛后发现应该只有一位师傅成功去花反编译了,但还是因为这道题里的花比较简单且只有一种,直接全nop掉了。在大多数场景中,写脚本去花风险很高,万一没搞出来就白白浪费了时间,但硬看总是能搞出来的。
但是硬看效率低且易出错,这时就可以尝试调试,去拿到一些关键数据/验证猜测的逻辑。比如这道题最后是个RC5,但是好久未能成功解密,最后调试发现roundKey和用密钥生成的不同,直接取出roundKey就可以秒了。
关于如何调试,推荐一个trepanxpy,能够提供一个类似gdb的调试环境,能够直接对行设置断点,方便查看变量,具体效果如下:
运行trepan-xpy
时可能会遇到这样的报错:
|
|
这是xdis这个包的问题,因为python版本在不断更新,而作者并未及时更新新版本的magics,需要手动修改下magics.py添加你的python版本即可,具体参见这个pr