# 2025 鹏程杯 逆向 wp
前两天复习课内考试一直没怎么打 ctf,赛中只解了两道,最近考完试以后又把剩下的题做完了(除了驱动),题目质量还是非常不错的,值得一做
# MoreMoreFlower
花指令 + vm,非常常规且老套的题
反编译看到 vm 不全,转汇编

经典 call + retn 花,写了一个简单 idc 脚本去除
1 |
|
然后下断点 trace 数据

最后得到的部分 trace 如下:
1 | 0 + 1 = 1 |
根据 trace 同构出加密脚本
1 |
|
然后写脚本解密
1 |
|
需要注意的就是密文为大端序
# babyconnect
这个题真有意思吧
client.exe
程序一共接受两次输入

第一次输出,发送到服务端,如果返回’c’就继续输入 flag,并将输入与 "check" 字符串拼接,继续向服务端发送
server.exe

如果发送来的数据开头是 "check",那么对数据通过 Address 函数进行处理后用 flag.dll 里的 check 函数进行校验
否则,对数据进行处理后再调用 inside.dll 里的 check 函数进行校验,通过后返回’c’,同时自解密 Address 函数

至此逻辑就清楚了,我们第一次输入的数据必须通过 inside.dll 中的 check,然后才能解密完 Address 函数分析第二个 check
inside.dll

当 dword_10014AC8 为 1 时,返回值为 2 通过校验,server.exe 中的逻辑暗示 inside_check 很可能是一个迷宫,但是发现这里只有一个函数表,这个地方其实和 sekaictf 的 miku 音乐机很像
观察整个函数表,有三中不同的函数
func1
1 | .text:10001060 way proc near ; DATA XREF: .data:10014004↓o |
func2
1 | .text:10001070 wall proc near ; DATA XREF: .data:10014008↓o |
func3
1 | .text:100013A0 sub_100013A0 proc near ; DATA XREF: .data:10014034↓o |
1 | BOOL __cdecl sub_10001000(void *a1) |
如果调试这个函数就会发现,执行 func1 是正常的,但执行 func2 就会异常,这是必然的, eax 为 0,取 [0] 的值就会触发内存访问异常,程序就会执行不下去。
所以其实 func1 就是迷宫里的路,是可执行的,func2 就是墙,不可执行的,那么 func3 呢,发现调用了 sub_10001000 函数,把一个墙函数地址的 + 5,+6 字节替换为 0x90,

也就是 nop 掉了会触发异常的那行代码,这样,原本不能走的墙,就变成了路。
根据上面的分析,我手动重命名了这些函数 (其实写脚本会更好),最后得到的地图为
1 | 0011111111 |
必须要经过大写字母才能解锁小写字母,手动或者 ai 跑一下能得到路径:
DSSSSDDWWWSSSSSAASSDAWWDDDSSDDWWWWAWWWDDDSSSSSSS
不过在进行迷宫运算之前还对数据进行了一个处理

异或运算,再异或一次就好了,上面的 SIMD 指令和下面这个其实是相同的,只不过编译器优化的比较难看
1 |
|
输入以后调试看关于 flag 的校验,先 ida 启动 server.exe 的调试,然后运行 client.exe
解密后的 Address 函数如下:

三次异或下一个数的操作
最后进行了调用了 flag_check 函数
flag.dll

sub_70E41020 是一个类似 rc4 的函数,里面只有这一处对 a2 进行异或,但是最终和密文比较的是 a1,就业时说这个地方其实没什么用,直接进行异或三次的解密即可

脚本如下
1 |
|
# emoji_encoder
给了一个加密的图片,根据字符串提示定位主要逻辑

这边有开始加密和结束加密的提示,在中间只找到 rc4 加密,因为 rc4 的对称性,把 enc_emoji.png 重命名为 my_emoji.png 再运行程序,得到一个模糊的图片

很遗憾,看不清 flag,一定是还有像素没有被解密,所以继续找程序中还有那些加密,翻和 main 函数相近的函数找到 tea 加密 (因为都是出题人自己写的,编译的时候会编译到一起)
交叉引用发现这个函数里有花指令

把 push - pop 的一串 nop 掉就好了
通过分析发现,只有一个常量数组进行了加密,图片本身是没有经过 tea 加密的,只是加上了这个数组的值

那么反过来,只要改为减去数组的值,再结合 rc4 的对称性,就能够 patch 程序实现自解密

我们看原程序是在调用完 rc4_init 函数后,跳到 loc_4021C7 这个地址进入 rc4 循环,最后调用 tea 加密,想要解密,必须先调用 tea,再调用 rc4,那么,就可以让程序先跳转到 tea 函数调用出,再跳转到 rc4,最后跳转回 tea 的下一行指令继续执行。
为了增加跳转指令,必须 nop 掉一些指令,

发现这部分汇编只是进行一个输出作用,对整体的执行流没有什么影响,所以 nop 掉这边就可以
patch 完以后如图:


最后再运行一遍程序即可得到原图

# chal
打开看到 jz 花,写脚本去除, 发现还是被混淆过的
以第一个函数 sub_5b12 为例,说明应该怎么去除这种混淆

通过观察可以发现有两种混淆方法

一是不透明谓词,二是一些直接返回值的函数,函数的返回值是存放在 rax 这个寄存器里的,那么 call 一个直接返回值的函数也就等于给 rax 赋值

且字节码刚好都是 5 个,不会改变下面的逻辑。
不透明谓词也同理

直接换成 mov 对应寄存器数即可,因为字节码比原始的少,所以同样不会有影响
完整去混淆脚本如下:
1 |
|
效果如下:

还是比较干净的,那么同时也就能看出这些函数没什么用,一定是返回 0 的,所以还是同理在 main 函数里把 call 换成对 eax 的赋值
同时像这样的代码也不必理会
1 | v42 = 0; |
调用的函数以及参数都可以调试获得。
最终手动化简后得到的代码如下:
1 | __int64 __fastcall main(int a1, char **a2, char **a3) |
可以看出这其实是一个壳,动态加载了一个 elf 文件并执行,可以断在写入函数, rsi 寄存器里就是新的 elf 文件,dump 出来进行分析即可
新的 elf 依旧是被混淆过的,可以沿用之前的脚本去混淆
手动去混淆后代码如下:
1 | __int64 __fastcall main(__int64 a1, char **a2, char **a3) |
现在的逻辑很清晰了,让 ai 写了一个 z3 脚本得到 flag: flag{B62o3$cC..*cE70O7?}
# medddgo
题目名称就可以看出来是 go 语言,根据字符串定位到关键函数

通过调试发现 sub_14007DF20 函数对输入进行读取,sub_1400B3740 是主要 check 函数
进入 check,里面代码不太好看

可以通过’\n’和’\r’的检查判断出是在计算输入的长度,下面的的 check_format 函数非常混乱,调试看逻辑

从内存中取值放入 rsi 和 rdi,再相减,返回右移后的值,其中 rsi 是用户输入,rdi 是传进来的字符串 flag{ ,这其实是在检查输入格式,同理下边的另一次调用也是在检查是否以 } 结尾

下面进行加密以及判断密文是否与 off_7FF684D80310 相同,通过 s 盒识别为 sm4 算法

转为汇编看 sm4 参数,key 是这个 xmm 值

检查 key 的初始化,交叉引用发现有一个函数一直在不断修改 key 的值

这边是一个反调试,只要检测到调试,这边的 while 就会一直循环改变 key 的值,直到用户输入完毕跳转到加密那里,所以可以在函数开头断下,获取到原始未修改的 key 值
直接解密未能解出,通过与标准 sm4 对比,发现密匙生成的地方多异或了一个数组

修改 fk 的值即可解出
1 |
|