RCTF 2024 wp [复现]
RCTF 2024 wp [复现]
2048
2048游戏,分数达到1000000即可获得flag。
每轮将获得分数为输入的sorce,上限为当前得分,初次上限为1w,那么每次成功分数翻倍的情况下用不了几次即可到达100w分:
RCTF{you_are_2048_master}
bloker_vm
字符串追踪到sub_411A10
密文给出了,sub_41100F是个RC4加密,那我们其实可以猜出来这个流程了
RC4->移位->异或
RC4->异或->移位….组合测一下其实也可以出
先看看能不能直接调试出吧
1 | rc4_key = "thisisyoursecretke" |
值得注意的是key长度是18而不是19
1 | RCTF{a_baby_debug_bloker} |
Dr.Akira
这个工具脱壳,定位到函数
这段 CTF 逆向题中的反汇编代码是在 访问 Windows 注册表的某个键值并尝试获取一个叫“MyKeys”的值。
尝试访问注册表路径:
HKEY_CURRENT_USER\Software\DrAkira
,并读取其中名为"MyKeys"
的键值内容。如果该注册表键不存在,则报错"Registry key not found."
;如果键存在但没有该键值,则报错"MyKeys value not found."
;否则,将该键值存入传入的参数a2
中。
添加后即可
交叉引用一下这里的函数sub_1001A2BB0
sub_1001A2F20
1 | char __fastcall sub_1001A2F20(__int64 a1) |
sub_10000D7C0可能是构造结构数组 / 字符串数组
sub_1001A27B0加密 / 映射 / 字符转换
将输入字符串(注册表中的MyKeys)进行逐字节处理、截取,转化为另一个字符
1 | __int64 __fastcall sub_1001A27B0(__int64 rcx0, __int64 a2, __int64 a3) |
应该是两两处理,输入可能是16进制的字符串
sub_1001A2CB0可能是加密函数
其中发现了check函数:
1 | char __fastcall sub_1001A5A10(__int64 n30_2) |
需要先逆向check函数
中间通过LLM了解到他这里开头几个函数涉及虚表,可能需要动调,但是动调有很多反调试,Sharod可过,如下拉满即可(注意的是勾选完需要重启调试器)
涉及的细节可以参考SharpOD 反反调试插件 v0.6e (增加功能和修复BUG) - 吾爱破解 - 52pojie.cn
经过漫长的动调寻找,终于找到了隐藏的check逻辑
从下面找过去的,00000001001A628F
动调发现的,会跳转到以上地址
从byte数组发现这个顺序:1, 4, 5, 4, 3, 2, 5, 2, 3, 1, 4, 2, 1
在x64dbg中已经可以看出来了,其中几个是没用的,有用的就5个,按顺序其实已经可以知道逻辑了
注意要逆向,我们从右边可以发现是0x1-0x5
也可以通过参数推导其行为(下图是check5)
参考RCTF2024赛后复现(上) - 0xD009(可以看看学习下具体的反调试技术,不依赖工具)
和2024 XCTF 联赛 RCTF 部分题解 - S1uM4i
因此得到以下题解,加密部分还包括维纳攻击
1 | from Crypto.Util.number import * |
最后解得,改下后缀即可,改成jpg
PPTT
根据约束写出了这样的代码
1 | from z3 import * |
问题是这个是个多解问题,需要排除掉一些,这意味着我们需要根据一些调试,但是调试会出现异常,应该是有反调
找一下
1 | .text:00DEC086 xor eax, eax ; 将eax寄存器清零 |
在开头发现了
这个函数
if ( *a1 == -1073741819 ) // 0xC0000005 访问违规异常(Access Violation)
1
2
3
4
5 VirtualProtect(*(LPVOID *)(a3 + 184), 2u, 0x40u, flOldProtect);
*(_BYTE *)(*(_DWORD *)(a3 + 184) + 1) = -112; // -112 = 0x90, 是 NOP
VirtualProtect(*(LPVOID *)(a3 + 184), 2u, flOldProtect[0], flOldProtect);
++*(_DWORD *)(a3 + 184);
*(_DWORD *)(a3 + 192) |= 0x100u;这几行的目的是:
- 获取并修改指令内存权限为可写执行
- 把某个地址处的第二个字节修改为
0x90
(NOP),通常是用于覆盖断点或异常指令- 恢复原来的内存权限
- 移动该地址指针,并设置一个状态标志位
if (*a1 == 0x80000004) // EXCEPTION_SINGLE_STEP
这是 调试器使用的单步异常(Single Step),通常由
Trap Flag
引发这里记录了
dword_42944C
的值,并增加计数dword_429450++
这也是一种检测调试器是否设置了Trap Flag进行调试
交叉引用一下dword_42944C
而
sub_418200
中出现的dword_42944C
很有可能就是和这个dword_429448
相关的全局变量——比如记录触发异常修复的地址。
- 检测是否有调试器(利用异常 + 单步)
- 如果异常发生了并是预期的访问违规,说明不是调试器触发的,就修复某些代码(比如还原跳转逻辑、去掉非法指令)
- 如果异常是调试器造成的(比如触发 Trap Flag),记录下来
- 如果调试器没有正确模拟这些行为,程序会因异常而崩溃
我们发现0X41C0D2就是第二个红框的位置,其实就是在跳过第一次swap
这个函数其实是RCTF2024赛后复现(上) - 0xD009
调试:
输入 : abcdefghijklmnopqrstuvwx
先序遍历后: abdhpqirsejtukvwcflxmgno
中序遍历后: phqdrisbtjuevkwaxlfmcngo
swap后 : qopmukwlfcrveisxabtdjgnh
Str1的要求: tyeuaTsm}qjrp
tyeuaTsm}qjrp
根据这个来缩小范围
1 | pre = '?' * 11 + 'tyeuaTsm}qjrp' |
?p?qyeamsT?u???}??t??jr?
或者
1 | pre = '?' * 11 + 'tyeuaTsm}qjrp' |
其实上面还是错的,因为题目里藏了一个hook strcmp
1 | int sub_A78370() |
这个藏在 initterm
的全局初始化函数中。
会对第13个执行一个++的操作
因此 “tyeuaTsm}qjrp”正确的结果应该是 “tyeuaTsm}qjsp”
exp
1 | from z3 import * |
最后出来:
找了下原因是这样:
16,17,9,19,12,8,21,23,13,20,5,7,3,22,1,2,0,10,14,18,4,11,6,15
15,17,9,19,12,8,21,23,13,20,5,7,3,22,1,2,0,10,14,18,4,11,6,16,
突然想起来了是之前我们把那个xor给patch了,所以第一次交换就运行了,但是其实是不做交换的那么码表就是
dpsqyeamsTau{nqR}CtFwjsk
RCTF{sjknwemqspsdaqtyua}
1 | from z3 import * |
0