NPC²CTF 2025 Reverse 方向复现
Week1
esrever
主逻辑
ida 中点击发现在 main 和 start 之间无限跳转,搜索字符串再 x 交叉引用到关键汇编,发现这部分汇编全部飘红,反编译失败
1 | |
但是观察后不难发现这里的汇编顺序是反的,往后翻的话还能找到几处类似的地方
上面的那部分汇编的逻辑大致是:
读取用户输入到 byte_4080,
有一个字节交换
调用加密函数
字符串比较判断
密文
密文在 unk_4040 处
提取出来为 131C175213525245143E3E114550053E16511A0F0006070D

flag 格式对比
第二处飘红的地方,比较输入的字符是否符合 flag{} 的格式
1 | |
主要加密
这部分可以看到有很多异或操作,并且是多次异或同一个值,一个 0x76 ,一个 0x72
1 | |
解密
用密文异或 0x76 和 0x72 之后没得到可见明文字符,爆破出来第三个异或值是 0x65(后来在汇编中异或 0x76 和 异或 0x72 之间的部分看到有一个异或 0x65)

得到 r}v3r33$u__p$1d_w0{nagfl,可以看出这个是从后往前两个字符一组交换顺序,整理出来 flag 是flag{nw0d_$1_pu_3$r3v3r}
原理
做题的时候就很好奇,为什么汇编是反着的,但是程序却能正常运行,解惑参考这篇文章
cccc
处理混淆
C# 逆向,动调之后基本能确定主要逻辑在 dll 中,被confuser 混淆了,先用 de4dot 去一下混淆
1 | |
然后用 dnSpy 打开,发现关键类

可以看到它读取了输入之后进行了一系列操作,后面的数据应该是密文了
动调找程序逻辑,我最开始直接调试会报错,后面设置成这样就能调了(选中使用宿主可执行文件)

动调梳理
逐语句(F11) 一直调
到达用户输入的地方

这里在读取输入

找到一串字符 doyouknowcsharp,和输入的内容传入同一个函数,推测这个字符串应该是密钥

找到加密逻辑,魔改 RC4,array2[t3] = (byte)(A_1[t3] ^ array[(array[t] + array[t2]) % 256] ^ 100); 这部分多了一个异或 100

再往后就是比较完输出 Wrong
解密
1 | |
flag 为 flag{y0u_r34lly_kn0w_m@ny_pr0gr@mm1ng_l@ngu@g3$}
ugly_world
处理混淆
main 函数初看很简单
1 | |
读取了用户输入,sub_55555555E656 对 input 和地址 555555561020 处的数据(0x5566778811223344,0xCCDDEEFF9900AABB)进行处理,sub_55555555E656 函数中是一个循环,其中的 sub_555555555401 是一个很大的经过混淆了的函数,byte_555555561040 的位置是密文 0x0BD3BE58, 0xBE73BBFB, 0xC8C8AF4E, 0x0B3C7D86, 0xC0257C09, 0x1D8FE0B0, 0x8837180C, 0xF5CF9D23, 0xB7A8B599,0xAE630F3D,sub_55555555E705 输出 your are right!,else 输出 you are wrong!
1 | |
对 sub_555555555401 中的函数逐个分析
sub_189 -> a1 + a2 -> ADD
1 | |
sub_1BB -> a1 - a2 -> SUB
1 | |
sub_2BF -> a1 >> a2 -> SHR
1 | |
sub_336 -> 把内存里连续的 4 个字节,拼成一个 小端序的 32 位整数 -> byte2dword
1 | |
sub_31A -> a1 << a2 -> SHL
1 | |
sub_1F2 -> (a1 ^ a2) & 1 -> XOR1
1 | |
sub_21D -> a1 ^ a2 -> XOR
1 | |
sub_28B -> (a1 ^ a2 ^ a3) -> XOR3
1 | |
修改之后反编译界面如下
1 | |
加密同构
梳理第一个循环结构一下可以得到
1 | |
可以看出这个这是一个魔改的 TEA,一共有 128 轮,每轮的 delta 都不一样,并且每轮左移右移的数也不一样
提取出每一轮的 delta 和 移位值
1 | |
核心加密处理
1 | |
解密
1 | |
flag 是flag{UG1y_T3A_m@k35_[m3]_fe31_NaU5Eou5!}
babyre
之前写过了,移步[NPC²CTF 2025]babyVM WP
week2
randomXor
分析
ida 反编译的 main 函数
1 | |
其中 srand 和 rand 组成了一个类似 MT19937 的伪随机数生成器
1 | |
1 | |
& cipher 处提取出密文(128 字节)
1 | |
解密
伪随机算法已经反编译得很清楚了,稍作修改就可以了,要注意加密 input[i] ^= rand()是对单字节操作,所以获得 rand() 值时要取低八位
1 | |
flag 为 flag{R4ndom_rand0m_happy_Funnny_R4ndom_1s_r3ally_funNy!!_hhh233HHH_this_1s_just_a_pi3ce_of_sh1t_fffkkkkkk_f4ck_jjjjKKKKKjjjjasd}
simple
java 层
主要逻辑在 com.example.simpleandroid.CheckActivity
1 | |
声明了一个 native 方法 CheckData,在本地库 libcheck.so 里
读取字符串输入之后,先调用 isValidInput(input) 做格式检查,再调用 native 方法 CheckData(input) 做进一步校验
native 层
先找到 CheckData
1 | |
可以看出 ee 数组中的每一个元素都不等于 0x7F,说明 ee[i] != 0x7F 的结果恒等于 1,所以只有当传入的字符串前 32 个字符是 32 个连续的 \x01 时,这个函数才会返回 1,否则返回 0
在 JNI_Onload 里面看到函数 sub_1C430
1 | |
提取出密文: 0xCD8413F20B6FCE4C, 0x5C9B1B43933043CF, 0x1C6EB6E1A546128E, 0x77C6E3D7AE009A3E
sub_1C010
1 | |
sub_11073
1 | |
sub_11073 主要实现了 RC4 里面的 KSA,这里找到的 key 是 this_is_a_key
等效 python :
1 | |
这里面还有个 srand(0x217) 先放一边
sub_21345
1 | |
对 v5 数组中的前 13 个数据进行异或 0x45 得到 v6 数组 ,然后 hook 了函数 sub_11073,把 v6 作为参数传入,srand(0x159357)设置了一个随机数种子,把之前的 0x217 给改了
计算出 v6
1 | |
所以 RC4 的密钥即为 Give_it_a_try
加密

先是一个RC4(密钥被修改过的),再根据随机数进行了移位异或
base
和题目描述的一样,坑点确实多
修花
main 函数有花指令,三种结构
一种是 jz jnz 的

一种 xor test + jz jnz 的

一种是 call retn 的

修好之后的 main 函数

其中 sub_41132F 里面是反调 IsDebuggerPresent(), sub_4110D2 里是 RC4 的 KSA

sub_41111D 里面是 PRGA ,但是在最后又多了一步异或 0x47

除此之外,还发现了三个 PRGA
sub_4111FE 是一个查表替换

反调试
程序有反调试,一开始调试就退出
打断点测试发现程序在 _initterm(First, Last);初始化时会触发异常退出,在此处打断点进行单步调试,发现了函数 sub_412AA0

通过动态解析的方式调用 api,动调解密发现是 Ntdll 和 ZwSetInformationThread,反调试


1 | |
在调用 call [ebp+var_2C] 之前,代码使用 mov esi, esp 保存了当前的栈指针 esp,调用之后,有 cmp esi, esp 和 call j___RTC_CheckEsp 来检查栈指针是否一致,如果只 NOP 掉 call 指令,栈上的参数不会被清理,导致 esp 比 esi 小 16 字节(因为参数仍在栈上),从而触发栈检查失败,程序会异常终止
函数调用前有四个参数被压栈
1 | |
每个push操作压入 4 字节,所以总共压入了 16 字节,在正常的函数调用中,这些参数会被被调函数清理(如果使用stdcall调用约定),或者由调用者清理(如果使用cdecl调用约定)。但在这里,由于我们跳过了调用,就需要自己清理这些参数,以保持栈平衡,所以要把这些 push 和 call 一并 nop 掉
或者换种方式,把 00412B96 地址处原本的 call [ebp+var_2C](调用反调试函数) 替换成 add esp, 10h,即把原始的机器码 FF 55 D4 改成 83 C4 10;add esp, 10h 指令将栈指针 esp 增加 16 字节,清理了栈上的 4 个参数;替换后,栈指针 esp 恢复到参数压栈前的状态,与 esi 保存的值一致,因此后续的 cmp esi, esp 会通过,栈检查不会触发异常,同时也绕过了反调试
进入 main 函数逻辑,nop 掉上文提到的 IsDebuggerPresent 绕过反调试,只 nop 调试时还是会报错,将 004139C2 的 call sub_41132F 也 nop 掉,然后把 004139C7 的 test eax, eax 替换为 xor eax, eax(33 C0),这样 eax 总为 0 ,jz 跳转一定会执行
除此之外,main 里面还有 try except 结构手动触发除零异常反调试

有一行 idiv [ebp+var_80],其中 [ebp+var_80] 被设置为 0,这会导致除以零异常,从而触发 __except 块
修改代码,不执行 __try 块,直接跳转到 __except 块(地址 00413AD0);开始地址是 00413A96,jmp 指令长度:2 字节(EB + 偏移量),下一条指令地址:00413A98,目标地址:00413AD0,偏移量 = 00413AD0 - 00413A98 = 38,因此用 EB 38 覆盖 00413A96 处的指令,并用 90 填充剩余字节
加解密
改完之后 main 函数的逻辑多了一块,现在加密逻辑完整了

用 key1 经过 KSA 之后生成了一个 S_box1 -> 经过 PRGA0 + 异或 0x47 处理得到 byte_41C1B8 -> 输入 input -> 用 key2 由经过了一次 KSA 生成 S_box2 -> sub_4111FE 函数使用 byte_41C1B8 对 input 进行查表替换 -> 根据模 3 的结果用不同的 PRGA 进行处理(魔改的最后一步异或值在动调的时候取) -> 对 input 进行循环异或
exp :
1 | |
flag 为 flag{2af4101a-201a-4ab9-a664-903577cb9ff0}
简单的逆向
虚假的逻辑
dnSpy 打开先粗略翻了一下,看到了 RC4 相关加密,没找到密文,动态调试,断在入口点

直接跳转到 Main 的位置

有个很明显的假 flag
猜测这个 array2 里就存的密文
cipher = [
128, 120, 214, 255, 142, 235, 58, 98, 154, 204,
10, 107, 117, 17, 192, 198, 61, 162, 218, 95,
57, 177, 175, 62, 111, 6, 40, 178, 149, 231,
163, 19, 36, 9, 249, 192, 94, 3, 236, 201,
123, 203, 150, 98, 59, 141, 234, 117, 229, 108,
135, 40, 105, 48, 89, 128, 11, 189, 35, 174,
167, 13, 55, 220
]
动调的时候看到逻辑就是一个魔改的 RC4(PRGA 最后一步多了一个异或 0X100),密钥是 17, 69, 20,但是发现解不出来 flag
在 where 这里发现除了 rc4 之外还有两个函数

点进去查看分别是 AES(where.QZoPKLYRExWdGJMn) 和 DES(where.TYuNpWqDZfXoRJvB)




假 flag 在运行时交会打印 Right!,而在运行程序时提交却显示 Wrong!
在 Cor20 头里面看到 ManagedNativeHeader 地址 0968

在该地址处发现 RTR(ReadyToRun)

使用工具 R2RDump 分析附件里的 where.dllR2RDump.exe --in "D:\aaa\NPCCTF\简单的逆向\where.dll" --disasm --naked --ho --out dump.txt
真实的逻辑
找到 main 函数的全部逻辑
1 | |
所以真正的加密过程是 DES -> 两次异或 -> AES
解密
exp
1 | |
所以 flag 是 flag{1_@m_th3_$3cr3t_h1dd3n_1n_th3_d3p7h}
week3
b&w
分析
附件给了两个 exe,一个 txt,txt 里的应该就是密文
分别单独打开两个 exe 的话会显示等待另外一个的连接,用 ida 看了一下,题目是两个进程之间的命名管道(FIFO)通信,先开 black.exe,打开读端,再开 white.exe 打开写端,white 读取输入之后通过 pipe__white_to_black 发给 black 处理,black 处理完再通过 pipe__black_to_white 发回 white ,white 接收后打印输出
动态调试 black.exe, 搜索字符串找主要逻辑,逐步调试找到主要加密,是个 tea



测试输入发现如果输入的数据不是 8 的倍数后面会补 0,如果是 8 的倍数的话会在最后再加上 8 个 \x00 字节;tea 的轮数也不是标准的 32 轮而是 64 轮;同时发现在进 tea 加密之前数据已经改变,观察发现是一个自反的映射,{}不做处理,字母和数字做映射处理;而且调试过程中 delta 的值也是随机生成的
输入 flag{1111111111}

输入 hbmg{3333333333}

1 | |
解密
测试发现 delta 的值应该在 0x4831FFFF 以内,根据 hbmg 和密文的前 8 个字节 0x5f6f8bf4, 0x3b31cec1 爆破出 delta = 0x48315716
exp
1 | |
flag 为 flag{Ru57_and_g0_1s_fun_T0_r3v3rs3_XD}