L3HCTF2025
TemporalParadox
拿到这道题目,我运行程序后发现报错
检查了一下我没缺少动态库就没管了,直接静态分析
主函数里面有两个分支,当程序在特定的时间点v58 > 1751990400 && v58 <= 1752052051运行时会输出query:这类东西,由t,r,cipher组成
另一个分支时,当不在时间段内就会要求用户输入然后进行md5加密和一个md5值进行比较(这里我对函数进行了重命名)你可以点进MD5那个函数看到
这是md5的特征
我们主要分析sub_140001963函数
这里有个if条件,当条件成立时输出的东西就是我们需要的明文
也就是&t=t&r=r&cipher=cipher的值
具体分析可以结合我写的注释进行代码理解
这里的salt值应该是可以直接调试出来的,但是我无法调试,就直接进行静态分析
这是salt生成的主要逻辑,以下是我编写的获取salt的脚本
1 |
|
这里的结果是tlkyeueq7fej8vtzitt26yl24kswrgm5,正好32字节
R和t以及cipher我是想着直接爆破得到
R和t这里的话没啥加密,主要是cipher生成这里有个ciphersub_14000184D函数,为了实现这一部分函数我花费了些时间,以下是函数逻辑
涉及到了两个换表
下面是我写的c语言代码整体实现sub_140001963这个关键函数
1 |
|
这边是可以正常输出在时间段内全部的数据,剩下就是进行一个比较了,这边我是因为不会写c语言的md5加密代码,我一直在找md5的头文件,一开始查到用的是openssl里面的头文件,然后我还专门下了一个openssl,但是下的最新版的运用后说vs2022已经不用了(安全问题),然后强制过掉警告又有新的报错,实在搞不下去了,我用AI直接帮我转化为python代码后进行一个md5值的比较
这边调教AI也花费了很多时间
1 | import math |
L3HCTF{5cbbe37231ca99bd009f7eb67f49a98caae2bb0f}
终焉之门
分析主函数代码
1 | __int64 sub_7FF6A04C1CF0() |
可以根据进行的函数重命名和注释来理解,这里的大概意思就是将用户输入的32字节字符串加密拼接成16进制数据存入co_consts前16元素
我们看到
这里是著加密代码点进co_consts数组可以看到前16个元素都是留空的,给了后16个元素,这里我们依旧没有找到check函数,因为这道题目是把验证逻辑放进了OpenGL Compute Shader里,也就是aVersion430Core里面,点进去可以看到
很明显,我们需要找的check函数就是void main()
1 | .data:00007FF6A04C3C2B db 'void main()',0Ah |
这是一个小型VM的实现,操作码在前面的主函数中给了就是opcode数组,我们直接编写代码将其模拟运行,进行输出就可以得到加密逻辑
以下是我写的VM运行脚本:
1 |
|
得到运行逻辑:
1 | [IP=0] stack_data[0] = co_consts[0] = 0xb0 |
这个VM只进行了异或和加减法
将用户输入加密后的前16个元素和固定的后16个元素进行混合加密后和密文cipher进行比较。现在我们有了cipher和co_consts的后十六字节,我们可以进行解密
解密分析
这里我们选择从 IP = 6进行分析,因为从IP = 0进行分析会很懵 别问我咋知道的
IP = 0这里进行的加密和IP = 6的加密一样,都是case 8
1 | [IP=6] stack_data[2] = co_consts[2] = 0xfa |
最后的结果就是cipher[2]
[IP=10] stack_data[2] = a - b = stack_data[3] - stack_data[2] = 0xffffffce = cipher[2]
这里的[IP=7] stack_data[3] = co_consts[1]就是相当于
[IP=7] stack_data[3] = cipher[1]
也就是说这里先将用户输入的第二位与密文cipher[1]进行异或后与co_consts的固定值co_consts[17]向减
好那么上面的[IP=2] stack_data[2] = co_consts[0]就相当于[IP=2] stack_data[2] = co_consts[0] = cipher[0]
后面的加法也是一样的分析,也就是未对co_consts[0]进行操作
解密代码
1 |
|
L3HCTF{df9d4ba41258574ccb7155b9d01f5c58}
easyvm(复现)
找到主函数
如果反编译是一大串函数的话就 ida 打开先设置Options->Compiler… [Compiler: Visual C++]
这边我进行一个重命名
cmp函数存放着32位的数组密文
很清晰了,我们进入VM实现函数发现有很多switch-case函数
这就是VM的handle步骤
我们找到主要操作码分类
算术运算
- 加法 (0x10)
- 减法 (0x11)
- 乘法 (0x12)
- 除法 (0x13)
- 取模 (0x14)
- 位移 (0x16左移, 0x17右移)
- 异或 (0x18)
这边的话我们可以通过条件断点的方式进行查看该vm的具体加密流程,我们可以在add,sub,xor,shl,shr进行条件断点(乘除和取余的话一般是无法逆向恢复的)
给出idapython代码(参考SU的WP)
1 | import idc, idaapi |
得到加密逻辑,取一段进行分析
1 | shl 0x31313131, 0x3 = 0x89898988 |
这边就可以通过代码一行一行进行复原,能知道是一个魔改的xtea加密,有个点要注意一下
1 | add 0x11223344, 0x376a9dbc = 0x488cd100 // 上一组sum值 |
下一组加密8字节用的sum值是上一组结束后的sum值
给出解密脚本
1 |
|
L3HCTF{9c50d10ba864bedfb37d7efa4e110bf2}
obfuscate(复现)
打开主函数进行反编译时发现大量指令混淆,能力不够去不掉,但是可以看别的函数进行分析
发现反调试,还不只一个,我们可以一个一个nop掉或者直接通过修改掉_exit(1)这个退出函数,因为这个函数被多处调用
检查一下发现都是反调调用的,直接将
将jmp改为ret后patch
之后就可以进行调试了
这里先分析其他函数,发现sub_1250和sub_1E80有加密逻辑,但是混淆的有点严重,我们可以通过D810或者IDA自带的goomba插件解除部分混淆
(右键-De-obfuscate)即可,但是得到的函数依然存在很多逻辑混淆,大部分是永真永假和一些指令替换
1 | if ( unk_B1C8 < 10 && unk_B1C8 >= 10 ) // 恒假 |
1 | *v37 - 1067854539 + 1067854538 // 等价于*v37 - 1 |
我们可以将永真永假去除后得到较为干净的代码
1 | __int64 __fastcall sub_1E80(_DWORD *a1, __int64 *a2, _DWORD *a3) |
1 | _BOOL8 __fastcall sub_1250(__int64 a1, __int64 a2) |
将这些代码丢给gpt让其去除掉剩余的指令替换得到更加直白的代码
1 | _BOOL8 sub_1250(__int64 key_ptr, __int64 input_ptr) |
1 | __int64 sub_1E80(uint32_t* output, uint32_t* S, uint32_t* input) |
RC5加密算法,魔改点就是主加密哪里多了个异或和密钥扩展S[i] = S[i - 1] - 0x61C88647这部分将加改为了减
Q的数据也改了一下,密钥可以动调出来(rc5的密钥扩展哪里),动调的话随便输入一个32位长度的数据就行(一开始是进行输入数据长度判断)
密文在sub_6180函数
可以动调也可以手动异或出来
解密代码
1 |
|
L3HCTF{5fd277be39046905ef6348ba89131922}
snake(复现)
这一题主要是找到加分函数在哪
sub_17BAC0里面有加分的逻辑,上面的那个if的判断是:这里会判断是否和食物坐标一样,一样的话就是吃到了豆豆,也就会加一分,那么我们可以将其改为
当坐标不一样时,视为吃,然后就可以不吃就加分,活一会就有了
patch后直接打开会随机触发不稳定的反调试,这是因为在游戏Call开头会发现一个时间检测,检测时间差是否大于80ms
我们可以在cmd中调用patch后的文件,这样的话不会崩
L3HCTF{ad4d5916-9697-4219-af06-014959c2f4c9}
ez_android(复现)
用jadx反编译找到MainActivity类发现没东西
运行APK发现输入数据错误时提示Wrong answer
解压APk分析so文件根据Wrong answer提示,找到加密函数
1 | __int64 __usercall ez_android_lib::greet@<X0>(void *a1@<X0>, size_t n27@<X1>, __int64 *a3@<X8>) |
很明显的加密,直接进行爆破可以得到flag
1 |
|
不能直接复制使用ida反编译的加密代码,因为char 类型在 C 语言中默认是有符号的,当值大于 127 时会变为负数,导致移位操作不正确,导致部分爆破失败flag缺失
L3HCTF{ez_rust_reverse_lol}