寒假参加了 HWS 冬令营选拔赛,最近整理文件的时候想着记录一下。
整体比赛难度不大,水了个第十名,主要大佬们都去参加 Real World CTF 了,只有我还在这里摸爬滚打 /(ㄒoㄒ)/~~
MISC
sound from somewhere
听了下音频一眼 SSTV,直接就出了。
can not speak
比赛的时候都根据教程绕过反调试了,但是还是做不下去。
结果比赛比完和我说有非预期,过反调试就能直接在内存里搜索到 flag?而且比赛出题人复现不出正解,真是服了。
首先 DIE 查一下发现是 VMP 壳,这玩意网上都没啥解决方案,只能尝试过反调试。
以下内容均只为步骤搬运,大部分不了解原理,就看着图一乐吧。
- 把 PEB 调试位清空:
把 gs:[60]
和 gs:[60]+bc
置零
- 下软件条件断点:
NtQueryInformationProcess
-> rdx == 0x07 || rdx == 0x1E
NtSetInformationThread
-> rdx == 0x11
NtQueryInformationProcess
断点命中前一个条件就把 r8 <- 0
运行直至 rdx == 0x1E
, 将 r8 <- 0
,然后运行到返回,rax <- C0000353
NtSetInformationThread
命中后把 rdx <- 3
- 禁用软件断点,启用硬件条件断点:
NtClose
-> rcx == 0xDEADC0DE
NtQuerySystemInformation
-> rcx == 0x23
选择无视异常运行
NtQuerySystemInformation
命中后执行到返回,将 rax <- 0xC0000001
NtClose
命中后将 rcx <- 0
- 禁用全部断点,运行一次就到用户代码了
或者你直接用 x96dbg 的插件 ScyllaHide,选择 VMP 的 Profile 就行,它原理主要是把上面的一些检测给 nop 了,但是这个代码会检测自身的完整性,在某个函数里会弹窗说文件被篡改,但在这道题里不影响最终效果(
过了反调试运行几次,然后右键 -> 搜索 -> 字符串就能直接看到内存里的 flag 了。
REVERSE
babyre
jadx 打开看了眼,先是有个明显的加密代码,但是感觉没那么简单直接跳过了。
然后发现有个动态加载 dex 文件的代码,又发现有个解密 assets 文件夹下 enc 的代码段。
直接把 enc 解密成 dex,进去发现是一个 AES + BASE64,CyberChef 一下就出了。
P.S. 我其实一开始并不知道这个动态加载 dex 文件是哪里被调用的,后来听讲解才知道是在 AndroidManifest.xml
中定义了 App 启动前的行为。
easyre
IDA 大致看了眼,程序结构比较常规,大概是输入个字符串加密然后和密文比较,然后加密是 TEA 系列的加密,比较用的 memcmp
。
直接调试发现中途会直接异常退出,想到刚开始新建了线程,猜测是检测了主进程是否长时间挂起,直接退出。然后 hook 了 NtClose
就过了,就没管具体实现了。
动调的时候在加密函数下断拿到了 key,在 memcmp 下断,得到了密文长度和密文。IDA 里看加密函数的时候第一眼以为改了不少,后来发现类似以下代码
1 | v8 = v4 + dword_140005728[v6 & 3]; |
其中的 v8 都是没用的,中间那一坨都可以改写成一致的形式,就是个改了 delta 方向和数值的 XTEA。
解密拿到原文包上 flag 就出了。
1 |
|
P.S. 看冬令营的讲解发现在主函数之前有一些操作,寒假里有个 KM 推荐学弟做的 bbctf 也有类似的操作,看到时候能不能开个新坑。
repy
这题比赛的时候没做出来,反而最后一部分的脑洞和出题人对上了,有点难绷。
这题一共分为 3 部分,前 2 个部分都用了控制流平坦化,一点一点来吧。
第一部分其实很简单,当时被吓退了,代码其实并不复杂,甚至可以直接手搓。
大致逻辑如下:
1 | bool check(char s[]) { |
虽然这个手推是个小学奥数题,但是最近刚学了 z3,正好写个脚本一把梭了:
1 | from z3 import * |
得到第一问的输入 6210001000
。
紧接着程序根据第一问的答案对内存中的数组进行换表,全是控制流平坦化,用了冬令营的时候推荐的插件 D810 进行求解。
反混淆以后发现程序将第二问输入的字符串和处理后的表一一对应了起来,存到一个数组里,中间有一片代码都没对这个数组进行处理(也可能是 IDA 反编译出错了)。
紧接着直接传进 sub_401600
又通过表还原回输入的字符串,然后对其进行 MD5 并与一段已知值比较,破解可得 yOUar3g0oD@tc4nd
。
然后就一头雾水了,比赛后根据讲解,左边还有 sub_402930
等函数不在流程中,其中 sub_402930
调用了文件输入输出,内部还有加密,极有可能和剩下两个文件有关。
中间有很多没啥用的垃圾赋值语句,去掉以后连逆带猜得到这个逻辑:
1 | int __fastcall sub_402930(char *iv, char *key) { |
用脚本将 secret.bin
解密:
1 | from Crypto.Cipher import AES |
然后是脑洞部分,根据题目名以及题中字符串 But someone told me that he caught 38 giant snakes yesterday.....
(Anaconda 有蟒蛇的意思,还告诉了版本是 3.8),或者直接拿 DIE 检测都能知道它是 Python 3.8 的文件。
用 3.8 版本的 python 反汇编字节码发现会报错,检查报错信息发现它通过 JUMP_FORWARD 36
来跳过非法的字节码,同时影响反汇编。
通过 dis.opmap
我们得知 JUMP_FORWARD 36
的字节码为 6E 24
,NOP
的字节码为 09
。
1 | # 注意要 3.8 版本的 python |
于是我们可以 patch 字节码:
1 | with open('decrypted.pyc', 'rb') as f: |
然而还是有部分代码无法复原,而且部分 while
有关控制流反编译是错误的,只能去自己阅读字节码。
主函数和之前程序一样只会连逆带猜,总感觉哪里少了点啥,能猜出有个 try
语句块异常处理来让前半段异或上后半段,但是整不出完整的逻辑,因为字节码顺序变得很奇怪,就和前面的 sub_402930
是怎么被调用的一样莫名其妙。
然后再说会能被直接反编译的三个类:
II00O0III0o0o0o0oo
类是一个换表 BASE64IIoo00IIIo0o0oo0oo
类是一个 TEA 系列的加密chall
类先调用了II00O0III0o0o0o0oo
类,然后调用IIoo00IIIo0o0oo0oo
类
也就是说加密逻辑是先换表 BASE64 然后 TEA 系列加密,这样我们就可以开始写脚本了:
1 | from string import ascii_uppercase, ascii_lowercase, digits |
解出最后一部分是 PytH0n_KZBxDwfkIzbEgUOY
,把三部分按题目要求合起来就是 flag。
CRYPTO
Numbers Game
发现有随机过程生成了两个长度为 53 的序列,算了下 (128 + 256) / 32 * 52 = 624,正好能满足解出随机数盒的条件。getrandbits
內部实现是反复生成 32 bits 的数,从 LSB 填充到 MSB,多余的 bits 就 drop。正好 128 和 256 都是 32 的倍数,可以完全复原生成的 32 bits 的数。 最后就只要倒推之前的生成的两个数即可。
逆推 state 盒的参考资料:https://badmonkey.site/archives/mt19937.html#2020-vn-招新赛-backtrace
1 | import hashlib |
math
看 chal.py
,发现主要由 3 部分构成:
读取满足 的 和
同余
打乱 bits
是经典的 PELL 方程,可以用连分数得到最小解,然后递推生成通解。
同余过程太经典了,可以直接通过 在 下的逆元和 求出 。
还有个打乱,一开始以为和上一道题一样要去看 getPrime
的源码,想想不太实际。
注意到 肯定是小于 的(不然不用解了), 只有 512 bits,85 bits 一个组,最多也就 6 组(事实上题目已经给了 )。
直接全排列枚举复杂度才 6! = 720 完全可以接受,枚举 , 通解,枚举全排列,求解 看是否为 b'flag'
开头。
1 | import itertools |