DubheCTF 2024 Reverse 复现
Destination
挂上TitanHide
,注意到NtSetInformationThread, NtGetContextThread这两个反调试
x64dbg断下后在线程开始ret
即可。
NtSetInformationThread会使当前线程从调试器隐藏,调试器跑飞
NtGetContextThread可以获取到当前线程的寄存器状况,用来检测DR寄存器来反硬件断点
还有一个反调试是我进入后发现的,主线程卡住,但是线程窗口里面有一个线程一直在Sleep(1);
,于是干掉就行了。
于是我们就得到了第一个patch的附件,我们以这个附件为基础继续调。
注意到我们输入完flag之后给我们来了个int3
,导致进入SEH流程,如下图所示。
图中框起来的是SEH处理函数,我们进去后,显然是似了。
我们观察到,这些花指令由2种构成
1.call $+5; add [esp], 5; retn
2.jnz addr; jz addr
显然第一种可以化简为nop
,第二种我们可以化简为jmp addr
处理脚本如下
from pwn import u32, u8
binfile = open("Destination_patched_2.exe","rb").read()
founds = []
# find all junkcode
index = 0
while True:
index = binfile.find(b'\xE8\x00\x00\x00\x00\x83\x04\x24\x05\xC3',index+1) #! index+1 ! 不然死循环了...
if index == -1:
break
founds.append(index)
print(index)
binfile = bytearray(binfile)
for i in founds:
print(binfile[i:i+10])
for j in range(i,i+10):
binfile[j] = 0x90
print(binfile[i:i+10])
# find all junkcode
index = 0
while True:
i = binfile.find(b'\x0f\x84',index+1)
index = i
if i == -1:
break
if binfile[index+6:index+8] == b'\x0f\x85' and u32(binfile[index+2:index+6]) - u32(binfile[index+8:index+12]) == 6:
binfile[i] = 0x90
binfile[i+1] = 0xe9
binfile[i+7] = 0x66
binfile[i+8] = 0x0f
binfile[i+9] = 0x18
binfile[i+10] = 0x20
binfile[i+11] = 0x90
index = 0
while True:
i = binfile.find(b'\x74',index+1)
index = i
if i == -1:
break
if binfile[i+2] == 0x75 and binfile[i+1] - binfile[i+3] == 2:
binfile[i] = 0xeb
binfile[i+2] = 0x90
binfile[i+3] = 0x90
binfile[i+4] = 0x90
# replace all matches
print(open("Destination_p3.exe","wb").write(binfile))
处理完花指令后,我们进入Ghidra,直接当成Func,然后手动处理一点东西,就得到了:
void xxtea(void)
{
uint e;
uint sum;
uint z;
uint i;
int round;
uint y;
round = 50;
sum = 0;
z = src[0xb];
do {
sum += 0xa4b46062;
e = sum >> 2 & 3;
src[0xb] = z;
for (i = 0; i < 11; i += 1) {
y = src[i + 1];
z = src[i] + ((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4) ^ (sum ^ y) + (key[i & 3 ^ e] ^ z));
src[i] = z;
}
z = src[0xb] +
((z >> 5 ^ src[0] << 2) + (src[0] >> 3 ^ z << 4) ^ (sum ^ src[0]) + (key[i & 3 ^ e] ^ z));
src[0xb] = z;
round += -1;
} while (round != 0);
return;
}
显然的xxtea。
分析完这个后,我们下硬件断点,发现没断下来而且值还被改了,我们回到主函数,找到这个地址
有一个jmp far
ida的反汇编这里有点问题,我们x32dbg看一看,发现是jmp far 0x33:xxxxx
这是windows下的天堂之门,0x23就是转32位,0x33就是转64位运行,这个是CS寄存器的值。
我们把jmp far的目标地址函数dump下来,放到一个二进制文件,拖入ida,选中64 bit mode,就可以看到真正的源码了。
可以注意到,这是一个已经玩烂的不安全CRC,它曾出现在:北邮去年招新赛的五子棋程序逆向、D^3CTF,moectf2024中。
提供两种解法,第一种就是z3-Solver
from z3 import *
from copy import deepcopy
s = Solver()
x = [BitVec(f"x{i}", 32) for i in range(12)]
y = deepcopy(x)
ans = [0xA790FAD6, 0xE8C8A277, 0xCF0384FA, 0x2E6C7FD7, 0x6D33968B, 0x5B57C227, 0x653CA65E, 0x85C6F1FC, 0xE1F32577, 0xD4D7AE76, 0x3FAF6DC4, 0x0D599D8C]
print(len(ans))
for i in range(12):
for j in range(32):
x[i] = If(LShR(x[i], 31) == 1, 2*x[i] ^ 0x84A6972F, 2*x[i])
s.add(x[i] == ans[i])
print(s.check())
print(s.model())
'''
12
sat
[x1 = 1585230664,
x10 = 950802794,
x0 = 2656343363,
x3 = 2246810078,
x7 = 688747158,
x11 = 422273590,
x9 = 2522199337,
x4 = 3061995652,
x5 = 3304541857,
x6 = 582058634,
x8 = 428794995,
x2 = 3651684911]
'''
也是最方便的,下面的解法是按照原理来的:
由于由于左移会变偶数,而xor后又会变成奇数,所以有唯一解。
我们先获取最低位是否为1,如果是1,说明是奇数,如果为奇数,说明它刚刚走的是最高位是1的那条分支,也说明它异或了立即数,我们直接给它异或回来,然后右移1,由于我们已经确定最高位是1,我们给它补上1即可。
如果最低位是0,我们就直接右移1,然后确定最高位是0即可。
解密脚本
#include <iostream>
#include <Windows.h>
// n = 12
typedef DWORD uint;
void xxtea(uint32_t* src, int n, uint32_t const key[4])
{
if (n > 0) /* Coding Part */
{
uint e;
uint sum;
uint z;
uint i;
int round;
uint y;
round = 50;
sum = 0;
z = src[0xb];
do {
sum += 0xa4b46062;
e = sum >> 2 & 3;
src[n - 1] = z;
for (i = 0; i < n - 1; i += 1) {
y = src[i + 1];
z = src[i] + ((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4) ^ (sum ^ y) + (key[i & 3 ^ e] ^ z));
src[i] = z;
}
z = src[n - 1] +
((z >> 5 ^ src[0] << 2) + (src[0] >> 3 ^ z << 4) ^ (sum ^ src[0]) + (key[i & 3 ^ e] ^ z));
src[n - 1] = z;
round += -1;
} while (round != 0);
}
else if (n <= 0)
{
uint32_t y, z, sum;
unsigned p, rounds, e;
n = -n;
rounds = 50;
sum = rounds * 0xa4b46062;
y = src[0];
do
{
e = (sum >> 2) & 3;
for (p = n - 1; p > 0; p--)
{
z = src[p - 1];
y = src[p] -= ((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4) ^ (sum ^ y) + (key[p & 3 ^ e] ^ z));
}
z = src[n - 1];
y = src[0] -= ((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4) ^ (sum ^ y) + (key[p & 3 ^ e] ^ z));
sum -= 0xa4b46062;
} while (--rounds);
}
}
unsigned char a[] = {0xd6, 0xfa, 0x90, 0xa7, 0x77, 0xa2, 0xc8, 0xe8, 0xfa, 0x84, 0x03, 0xcf, 0xd7, 0x7f, 0x6c, 0x2e, 0x8b, 0x96, 0x33, 0x6d, 0x27, 0xc2, 0x57, 0x5b, 0x5e, 0xa6, 0x3c, 0x65, 0xfc, 0xf1, 0xc6, 0x85, 0x77, 0x25, 0xf3, 0xe1, 0x76, 0xae, 0xd7, 0xd4, 0xc4, 0x6d, 0xaf, 0x3f, 0x8c, 0x9d, 0x59, 0x0d};
//unsigned char a[] = "flag{111111111111111111111111111111111111111}";
unsigned char k[] = {0x6b, 0x7a, 0x0e, 0x6b, 0xee, 0x11, 0x30, 0xd1, 0x6d, 0x2c, 0xe1, 0xa7, 0xa6, 0xac, 0x99, 0xc1 };
int main()
{
for (int i = 0; i < 12; i++)
{
uint32_t n = *(i + (uint32_t*)a);
for (int j = 0; j < 32; j++)
{
if (n & 1)
{
n ^= 0x84A6972F;
n >>= 1;
n |= 0x80000000;
}
else
{
n >>= 1;
n &= 0x7fffffff;
}
}
*(i + (uint32_t*)a) = n;
}
xxtea((uint32_t*)a, -12, (uint32_t*)k);
xxtea((uint32_t*)a, -12, (uint32_t*)k);
std::cout << a; //DubheCTF{82e1e3f8-85fe469f-8499dd48-466a9d60}
return 0;
}
regadgets 版本的xxtea exp
from regadgets import *
k = [0x6b, 0x7a, 0x0e, 0x6b, 0xee, 0x11, 0x30, 0xd1, 0x6d, 0x2c, 0xe1, 0xa7, 0xa6, 0xac, 0x99, 0xc1]
k4 = byte2dword(k)
enc = byte2dword(bytes.fromhex("""
43 95 54 9e 48 b3 7c 5e 2f 4a a8 d9 de 99 eb 85
84 58 82 b6 a1 4e f7 c4 8a 82 b1 22 96 72 0d 29
73 e4 8e 19 29 b5 55 96 6a 19 ac 38 36 62 2b 19
"""))
dec = xxtea_decrypt(enc, k4, delta=0xa4b46062, additional_rounds=40)
dec = xxtea_decrypt(dec, k4, delta=0xa4b46062, additional_rounds=40)
print(dword2bytes(dec))