XYCTF2025
XYCTF2025 逆向WP
虽然这次失去Web手,但是配合新来的师傅,小队的队员们也一起努力打了不错的成绩。
墨水师傅的MDriver题也是拼尽全力无法战胜(,总体逆向题的质量挺不错的,没什么烂活,值得一试。
WARMUP
网上抄的VBS解密代码
1 | Function Defuscator(vbs) |
output.txt:
1 | MsgBox "Dear CTFER. Have fun in XYCTF 2025!" |
RC4解密,密钥为rc4key
flag{We1c0me_t0_XYCTF_2025_reverse_ch@lleng3_by_th3_w@y_p3cd0wn‘s_chall_is_r3@lly_gr3@t_&_fuN!}
ezVM
通过字符串界面里的unicorn和加密函数的一些特征发现是使用了unicorn框架调用了一串代码。
找一个使用unicorn框架的程序进行bindiff恢复一些unicorn函数的符号。
发现是调用了一串ARM64的代码字节进行模拟执行,将输入字符串传入加密返回,并附上了一些data和栈空间初始化。
将以上调用write写入的数据提取,随便找一个ARM64框架的.so复制到对应地址,以便反编译看代码。
最后得到一个函数,很清晰的看出里面是一个VM虚拟机执行的流程。
使用c++编写代码调用unicorn库进行模拟。
使用Hook,在关键计算地址处进行Hook,输出各个计算流程以及数据。
1 |
|
最后运行输出得到一个vm加密流程
以下是部分输出内容。
output:
1 | W3 = W1 << W0 --- 4 << 0 = 4 |
通过观察可以发现是一个魔改的XTea加密。
通过对比标准XTea加密流程,可以得到里面参与计算的4个key值 {0x776f6853,0x656b616d,0x616d5f72,0x74696564} 以及delta值 0x5f5fe6e7
写出对应加密的c++代码:
1 | void encipher(uint32_t v[2], const uint32_t key[4]) |
解密代码:
1 | void decipher(uint32_t v[2], const uint32_t key[4]) |
提取chal程序中的密文,进行解密即可。
完整解密代码:
1 |
|
XYCTF{fun_un1c0rn_with_4rm64_VM}
Moon
跟到moon.xor_crypt实际加密处。
发现是进行了单次xor,并加入到一个list中,前后过程不清楚。
断在xor这个命令,运行附加调试,随便输入一串1
发现是输入的’1’和一个0x24进行xor,多运行几次发现就是将输入的字符串都异或上一些值。
直接断在return处,v20是最后将list转成Bytes的结果。
发现是28长度的一串字节,从0x15开始的,就是我们输入字符串长度以及异或完的结果。
继续运行会返回到check_flag代码处,底下有一个RichCompare比较两个数据。
v45可以看到就是将刚刚v20的bytes直接unhex转成了一串字符串。
那么v9就应该是flag的密文,可以数出一共是要35字节。
重新调试运行输入35个1,在check_flag开头断点,把输入的字符串全都patch成0。
最后在RichCompare处就可以得到xor密文的列表。
将v9的密文与这个数据进行xor即可得到flag。
flag{but_y0u_l00k3d_up_@t_th3_mOOn}
Dragon(5m10v3师傅解题)
.bc 后缀
反编译为LLVM IR
1 | llvm-dis-17 Dragon.bc -o Dragon.ll |
分析得知为crc64,以两个为一组进行校验,直接爆破就行
1 |
|
Summer(5m10v3师傅解题)
haskell程序
函数式编程语言,这意味着一切都是惰性计算,什么是惰性计算? 简单来说就是在调用之前不会对该值进行计算
浏览 main 函数,可以看到 hs_main 将 ZCMain_main_closure 作为它的参数,它指向 haskell 程序的真正入口点
ZCMain_main_closure里面我们发现它调用了stg_ap_p_fast,这个是底层函数,主要调用Main_main_closure这个函数
GHCziInternalziBase也是底层函数,主要关注两个参数
第一个参数的地址处的函数为打印字符串
GHCziInternalziList_length 为处理我们的传入的字符串的长度,直接调用的是zdwlenAcc**,**zdwlenAcc 将通过检查下一个是否是列表的末尾来计算 “flagTable” 的长度(这里的”flagTable” 是我自己命名,其实就是存储惰性列表,我们可以根据惰性列表的指针数判断字符串的长度)
我们这里就用flag进行测试,一方面是为了查看他的返回值
此时他是将rbx此处(即为惰性列表的末尾),可以人工数(即为50)
另外一种为看返回值,第一次断下是返回我们输入字符串的长度,第二次断下是返回密钥的长度,第三次断下是返回密文的长度
另外一处为GHCziInternalziNum_zdfNumIntzuzdczp,这个也是在网上一篇文章看到的,在add rbx, [rax] 在经过几次迭代后,我可以看到一些字符开始出现,此时我们可以得到密钥为Klingsor’s_Last_Summer
我们在.data段得到了密钥,因此我们可以猜测下面可能为密文,并且下面都是指针+元素的存储形式
然后通过CE调试得到明文和密文,然后得出为rc4+xor
flag{Us3_H@sk3ll_t0_f1nd_th3_truth_1n_th1s_Summ3R}
Lake
单步跟到主函数。
这边输入字符串后先赋值到了另一个数组,然后进行了一次简易VM计算进行了第一次加密,
然后接着第二次加密,最后循环比较。
发现VM只用到了加减和XOR计算,在这三个地方的关键点打断点,输出寄存器和计算流程,这边为了方便直接复制到代码里面解密,将加减断点里面的输出运算符反过来,输出出来的代码直接复制到代码就是进行解密的流程。
调试输出:
1 | Input[2] += 12; |
这也就是第一层加密的解密代码。
第二层加密直接对着写即可,我写的有点问题(懒得改),其中几个字节解密不对,不过根据解密出的flag也能猜出是啥,替换完那几个字节就得到完整的flag。
完整解密代码:
1 |
|
flag{L3@rn1ng_1n_0ld_sch00l_@nd_g3t_j0y}
EzObf
main_0函数跟入发现有混淆,红框处为原真实汇编指令,其他都是混淆指令。
混淆流程:
- 执行真实指令
- call $+5执行pop rax,rax就是call时push到栈的返回地址,也就是pop rax指令的地址。
- 给ebx赋值,进行rol计算,最后用rax加上或减去(共两种)rbx,得到跳转地址,进行jmp rax。
之后每jmp过去一次,那边就都是一样的结构,popfq和pushfq之间就是真实汇编。
deobf的思路即为nop那一堆pop和push,保留真实汇编指令,然后计算跳转地址,手动计算相对地址写jmp,保持代码执行流程。
deobf idc脚本:
1 | static NopCode(Addr, Length) |
执行完,把main_0剩余代码都手动nop即可。
然后Apply patches to input file,应用一下patch,重新打开ida载入程序分析。
从main_0的jmp进入两层到这边,然后用IDA Delete Function删除sub_1401F7B77函数,然后对jmp那边按E即可重新重构完main函数(如图2),F5即可分析。
Main函数原代码:
1 | int __fastcall main_0(int argc, const char **argv, const char **envp) |
很清晰看出来是XXTEA加密,密钥是固定种子随机数随机得到的,Delta被魔改,然后密文也能看到。
注:写WP时用的是旧版附件分析,缺失了后面16字节密文
完整密文数据:
1 | 0xa9934e2f, 0x30b90fa, 0xdcbf1d3, 0x328b5bde, |
解密代码:
1 |
|
flag{th15_15_51mpLe_obf_R19Ht?}
CrackMe
有反调试,在WinMain开头断点,使用ScyllaHide一把梭去除((。
从WinMain可以跟踪到窗口消息函数,图四就是验证函数按钮消息。
从TLS那边可以看到启动了一个线程,线程函数如下
这边死循环判断了一个值,然后调用CallBack,随便输入flag,点击验证,发现会先调用CallBack中的mark2函数进行第一次验证。
将这边v4都异或上0xBB会得到”flag{“五个字符,就明白这边是检测输入flag开头是否为”flag{“,进行了第一次验证,然后继续下一次验证进入了case 5的mark3函数。
mark3这边是用固定值生成了一个v5数值列表,a1是输入的字符串,但是可以看到+5跳过了前面的五个字符,然后对括号内的前7个字符做一些加密计算然后和v5列表前7个数值进行检验。
这边就可以直接提取v5生成的数值列表,然后利用爆破得到括号内的前七个字符。
爆破代码:
1 | unsigned int box1[] = { |
得到前七个字符为:moshui_
第三次Check是在case 0处,程序起始的时候启了一个线程,死循环然后这边判断前两次Check是否成功,然后进入最后一次Check代码。
开始的时候利用前五个字节以及括号内前七个字节生成了两个四字节密钥,然后又赋值了另外两个固定的密钥值。
由于前五字节和括号内前七个字节是已知固定的,所以生成的密钥也是固定,可以直接提取计算完的密钥。
密钥:0x42B2986C, 0x12345678, 0x0D6D6A3E, 0x89ABCDEF
然后下面赋值了密文到v7,判断输入的字符串第29个字符是否为’}’,这边可知flag长度为29,然后利用密钥和输入字符串,进行加密,最后和v7判断。
加密是8字节8字节加密,观察sub_7FF7ADAB1640可知是IDEA加密算法,循环加密0x10000次没什么用,因为Input和Output在两个不同数组,所以和加密一次是一样结果。
利用IDEA解密算法配合密钥解密v7的值即可得到后16字节,最后拼接得到完整flag。
解密代码:
1 |
|
flag{moshui_build_this_block}