*CTF 2021 Reverse*5
Contents
翻译
本文另有 英文版本。
乱七八糟的考试终于结束了,这次是时隔半年重新回来和 AAA 的队友们一起打比赛,又回归了熬夜的生活。这次比赛 6 个 Reverse 做出 5 个,剩下的一个 RL_Env 貌似是个机器学习题,真不愧是自由而无用的灵魂,我甚至都没有看懂题目在做什么。
stream
Rust 题目,还好有符号,拖到 IDA 里打开,std::rt::lang_start_internal 的第一个参数是 Rust 里的主函数。
| |
读入文件后主要加密过程在这个循环中完成。试着写了两行 Rust,发现 rand_chacha::guts::{init_chacha, refill_wide} 两个函数都不是 pub 的,说明并非直接调用这两个函数,而是编译时内联了进来。看 guts.rs 的源码可知 refill_wide 的第二个参数是 round,可以发现 10 轮的是同一个包里的 ChaCha20Rng。IDA 调试一下可知生成的随机字节填充在 v22 的 buffer 里,最后 *(_BYTE *)(v3 + v6) ^= LOBYTE(v22[0]); 表示只用了第一个字节异或到原先的明文上。上面的 init_chacha 是 ChaCha20Rng::from_seed 展开而成,key 是 v9 开始的 32 字节。*((_BYTE *)&v13 + (v4 & 0x1F)) = *(_BYTE *)(v3 + v6); 表示每次从明文取一个字节,循环放入 key 中,最终生成的伪随机字节重新和这个字节异或,那么每轮只有 256 种可能性,可以爆破。从明文取字节的顺序是 4,11,18……最开始的时候 key 全为 0,只放入一个明文字节爆破第一个字节后继续爆破第二个,依次类推,最后有多解情况,把 flag 限制在 ASCII 内减少尝试。
由于我不会 Rust,又没有找到合适的等价实现,就用 Rust 包装了一个随机数生成程序,主程序用 Python 实现。
| |
| |
flag: *ctf{EbXZCOD56vEHNSofFvRHG7XtgFJXcUXUGnaaaaaa}
wherekey
从网上找到一个 libc 2.31 的 signature 安上去,主函数花指令处 jnz 改为 jmp 可以反汇编。发现这个程序中间还有一个 listen 和 send,自己给自己发送数据来增加工作量。
| |
上面的或操作是宏 FD_SET(fd, set) 展开后的代码,下面的与操作是 FD_ISSET 宏,都是和 select 一起使用的。tty 收到数据后交给 sub_402072() 处理,看出发送的数据包应该是 5 个 5 字节的。看 sub_40223C() 处理 socket 收到的数据包,该数据包又交给 sub_4022DE(__int64 a1) 处理,每 5 个字节和 'flag{are_you_sure_friend}' 抠出来的字节相乘,实际上是一个线性方程组,常量字符串是系数矩阵,共有 5 组线性方程组。去掉花指令后找到比较对象是 unk_4C5150。翻出自己的陈年老代码用高斯消去法找出逆矩阵,再分别和 5 个向量相乘,即可得出明文。
| |
flag: *CTF{Ha23_f0n_9nd_G0od-1uck-OH}
ChineseGame
同样是静态链接,找到 libc 和 libstdc++ 的 signature 安上。有一个 10 元素的链表,链表节点存放了一个数,只需要关心这个数是否大于 100,若将大于 100 简记作 +,小于 100 为 -,初始状态是 +-++++++++,最后要求全为 -。输入内容是 01 字符串,分别用来选两种操作,冥冥之中感觉像是九连环游戏。每种操作都有一个位置参数,从 dword_5D5140 开始取。两种操作的内容是:如果该位置后面节点的状态是 +---…(或者根本没有后面的节点),则将该位置变为 +/-,否则什么都不干。经队友提示,发现 dword_5D5140 的位置参数恰好是正确解法需要操作的位置,所以只需找出进行此操作时是由 + 翻转到 - 还是 - 翻转到 + 即可。
| |
flag: *CTF{4ncient_G4me_Fr0m_4ncient_Ch1na!}
Favourite Architecure flag0
加入此题之前,队友已经逆向了一部分。flag 长度为 0x59,前 0x29 字节进行 Chacha20 加密(从函数的密钥扩展中的常量字符串 'expand 32-byte k' 看出。同样由 D. J. Bernstein 设计的 Salsa20、Ed25519、Ed448 都有特殊的字符串),提取固定的 key 和 ciphertext 解密即可。后 0x30 字节是 TEA 加密,但被修改成了 16 轮,维基百科上有如下示例代码:
| |
decrypt 中的 sum=0xC6EF3720 是加密时 sum 每轮加上 delta 得出的累积值,若轮数减少为 16 轮,则 sum=(0x9e3779b9*16)&0xffffffff=0xe3779b90,修改后解密即可。
flag: flag{have_you_tried_ghidra9.2_decompiler_if_you_have_hexriscv_plz_share_it_with_me_thx:P}
1rep
这是一个 perlcc 编译 Perl 产生的程序,自己搞了一个 Perl 脚本试着编译了一下,发现所有的这种程序的主函数都依次调用如下函数:
| |
从自己编译的源码中看出,v20 是一个 PerlInterpreter*,说明这种编译器实际上不是像 Cython 那样把脚本代码转为等价的 C 代码,而是预先设置好变量、字节码之后,整体交给 Perl 虚拟机运行,此处的 Perl 虚拟机实例就是这个 PerlInterpreter 对象。最初的解题思路是借助调试器的 appcall 机制,设法从这个虚拟机实例里提取字节码。发现 libperl 里有一个 perl_dump_all(),gdb 在 perl_run 设好断点后,将 rip 改为 perl_dump_all 的地址并继续,结果 dump 出一大堆 optree,依旧没法看。后来发现 Perl 的 B::Deparse 模块有反编译功能,参考 StackOverflow 上的提问,利用 Perl_eval_pv 向 PerlInterpreter 里注入代码运行。
| |
但是注入以上代码后,只反编译了开始的一小点检查。flag 去掉外壳后,里面 16 字节交给 kwvIJu 检查。再注入 B::Deparse 的其他功能反编译特定函数:
| |
不知道为什么 kwvIJu 没法反编译,但是试着反编译别的函数发现,所有函数都是检查当前 flag 的首字符(位于 0-f 范围),然后根据此字符 switch 到 16 个不同的分支,每个分支再调用不同的其他函数,调用的函数用来处理去掉首字符后剩下的 flag。观察 IDA 中存放字符串的地方,发现 .rodata:00000000032E6CAF 夹杂着一个 Correct,试着反汇编上方的 afRNDz,发现这个函数是打印 Correct,所以需要找出是哪个函数经 switch 语句调用了 afRNDz。试着再反汇编上方的 xmecUK,发现它调用了 afRNDz,再继续往上翻找,可以确定 flag 的最后 14 字节,但是后面再往上就找不到调用这一串函数的函数了,只好爆破最前面两字节,侥幸拿到 flag。
| |
flag: *ctf{7a4bb0982b39baa2}