0x00 前言

很猛的异常处理和反调试

0x01 解

无壳,64位IDA

上来就是创建快照,指定是反调试

查询发现检测的内容是xmmword_14003A578存的东西,解出来是vmtools

看来是检测虚拟机的,不过直接过掉就行

patch之后继续看,发现还有反调试

继续走,动调发现直接退出,看来还有反调试

走一下,发现在输出提示信息的后面有个函数sub_140001540,里面有个NtQueryInformationProcess

这边使用的NtQueryInformationProcess可以用于反调试,但官方提供的文档有所缺失,实际上反调试不仅可以传ProcessInformationClass = 0x7,还可以传其他的

patch掉

接受输入后面的那个函数15E0也有反调试

这边用了0x30,一样过掉,不演示了

之后继续运行,又退出

发现是这

直接patch掉

继续走,又退出,可能又有反调试,但是也可能已经进行了判断,因为走到这会发现下面就是输出判断结果,而这个判断之后就退出了

进入这个函数,发现输入(pbData)和长度(dwDataLen)被丢进了另一个函数里面

进去发现是个hash函数,首先使用了CryptCreateHash,其中ALG_ID的值是0x8004(SHA1),然后调用了CryptHashData,再然后后调用了CryptHashData,最后调用了CryptGetHashParam(这里使用的0x2的含义参考这个

所以这个地方就是获取了输入的Hash值,那这个值有什么用呢,继续往下看

发现下面有个反调试

针对这个函数,在main函数开头有个校验,所以不能将这个函数直接进行patch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
v5 = (char *)sub_14002C000;
v6 = (char *)sub_14002C000 + (unsigned int)dword_140047000;
v7 = -1;
if ( (char *)sub_14002C000 != v6 )
{
do
{
v8 = (unsigned __int8)*v5 ^ v7;
v9 = (v8 >> 1) ^ 0xEDB88320;
if ( (v8 & 1) == 0 )
v9 = v8 >> 1;
v10 = (v9 >> 1) ^ 0xEDB88320;
if ( (v9 & 1) == 0 )
v10 = v9 >> 1;
v11 = (v10 >> 1) ^ 0xEDB88320;
if ( (v10 & 1) == 0 )
v11 = v10 >> 1;
v12 = (v11 >> 1) ^ 0xEDB88320;
if ( (v11 & 1) == 0 )
v12 = v11 >> 1;
v13 = (v12 >> 1) ^ 0xEDB88320;
if ( (v12 & 1) == 0 )
v13 = v12 >> 1;
v14 = (v13 >> 1) ^ 0xEDB88320;
if ( (v13 & 1) == 0 )
v14 = v13 >> 1;
v15 = (v14 >> 1) ^ 0xEDB88320;
if ( (v14 & 1) == 0 )
v15 = v14 >> 1;
v7 = (v15 >> 1) ^ 0xEDB88320;
if ( (v15 & 1) == 0 )
v7 = v15 >> 1;
++v5;
}
while ( v5 != v6 );
}
v16 = dword_140042BF0;
if ( ~v7 != dword_140047004 )
v16 = 1;
dword_140042BF0 = v16;

所以这个地方走到时候改改ZF寄存器就好了

而下面将v5v6进行对比,v5就是上面计算出来的输入的Hash值,所以这个v6的内容就是对比的内容了

1
2e95a5c5f9643df668d3965a6cdf19541bc0496b

之后看到有个CxxThrowException(pExceptionObject, (_ThrowInfo *)&_TI1_AVexception_std__);,这是C++抛出异常的,有异常就会有处理,通过看汇编可以在main函数调用sub_14002C00前发现一个try

说明这地方确实是有个自定义的异常处理的

由于Hash值对比正确的话会抛出异常,原来的判断就没用了,所以自定义的异常处理里面一定有其他逻辑校验flag

需要注意的是C++里面的try...catch...SEH__try...__except...是不一样的,要进行区分

这里引申出来一个问题:在程序抛出异常后,IDA会首先捕获,而传递给应用程序之后就会直接执行异常处理(catch部分代码),由于没有中断的存在(没有断点),所以会直接执行至退出,因此为了能正常查找逻辑,要找到catch部分的代码然后下断点才行,那要怎么找到catch部分的代码呢?

参考:X64逆向异常分析

首先交叉索引main函数,到pdata段

以此题为例,可以到这一段

1
RUNTIME_FUNCTION <rva main, rva algn_7FF679F54CB4, rva stru_7FF679F8CCEC>

到最后一个地址stru_7FF679F8CCECUNWIND_INFO)里面

它们指向的是UNWIND_INFO结构体:(UNWIND是异常展开,异常展开的作用就是能知道如何去释放资源,析构的时候,不过我们一般不关心析构,我们需要知道的是如何去处理抛出的异常)

具体每个字段代表什么就不写了,看上面参考

来到这后可以看到下面有个FuncInfo4

继续下滑可以找到一个注释为catch code的rva地址(对应HandlerInfo4中的dispOfHandler

这个地址就是catch的地址了

在开头下个断点就能愉快调试了

不过这个地方IDA没法反编译,但是将他的机器码复制出来另外创建一个十六进制文件就能反编译了(但是会丢失所有地址)

也可以将main函数暂时Undefined掉,然后在这个函数的位置创建函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
__int64 __fastcall sub_7FF679F7AF09(__int64 a1, __int64 a2)
{
__int64 v3; // rcx
void **v4; // rdx
const void *v5; // rcx
size_t v6; // r8
__int64 v7; // rax
__int64 v8; // rax
__int64 *v9; // rcx
__int64 v10; // rax

Address(); // function address: 0x140009DF0(未调试前)
sub_7FF679F59F50();
(*(void (__fastcall **)(_QWORD, __int64))(**(_QWORD **)(a2 + 32) + 24i64))(*(_QWORD *)(a2 + 32), a2 + 168);
// 上面那条实际上是一个call,到的地址是0x140004100(未调试前)
v3 = a2 + 136;
if ( *(_QWORD *)(a2 + 160) >= 0x10ui64 )
v3 = *(_QWORD *)(a2 + 136);
sub_7FF679F54620(v3, *(unsigned int *)(a2 + 152));
v4 = &xmmword_7FF679F92C20;
if ( *((_QWORD *)&xmmword_7FF679F92C30 + 1) >= 0x10ui64 )
v4 = (void **)xmmword_7FF679F92C20;
v5 = (const void *)(a2 + 168);
if ( *(_QWORD *)(a2 + 192) >= 0x10ui64 )
v5 = *(const void **)(a2 + 168);
v6 = *(_QWORD *)(a2 + 184);
if ( v6 == (_QWORD)xmmword_7FF679F92C30 && !memcmp(v5, v4, v6) )
{
v7 = sub_7FF679F558E0(a2 + 208);
v8 = sub_7FF679F54F70(v7);
v9 = &qword_7FF679F91330;
}
else
{
*(_QWORD *)(a2 + 48) = 0i64;
*(_WORD *)(a2 + 56) = 0;
*(_BYTE *)(a2 + 58) = 0;
*(__m128i *)(a2 + 60) = _mm_load_si128((const __m128i *)&xmmword_7FF679F8A850);
*(__m128i *)(a2 + 76) = _mm_load_si128((const __m128i *)&xmmword_7FF679F8A810);
*(_DWORD *)(a2 + 92) = 10;
*(_DWORD *)(a2 + 96) = 52;
v8 = sub_7FF679F54CC0(a2 + 48);
v9 = &qword_7FF679F91600;
}
v10 = sub_7FF679F58C30(v9, v8);
sub_7FF679F58EF0(v10);
sub_7FF679F51D40(a2 + 168);
return 0i64;
}

函数Address()会将0x140009F50处的代码为retn(直接跳出函数),所以接着的那个函数就是一进去就跳出来(什么逆天操作)

之后一长串实际上是一个函数,看汇编会比较好看懂,函数的地址是0x140004100

之后的sub_7FF679F54620是校验用的函数,用了三种校验,第一个是恒真式sub_7FF74A2A19E0(没搞懂为什么要这么一个东西,不过这个函数在main函数后面也被用到了,不过这个判断之后是”nonono”,猜测是给强制不跳过if的人准备的),第二个sub_7FF74A2CC150是跟上文一样的取输入的hash然后去对比,第三个dword_7FF74A2E2BF0是上面提到的main开头的校验的结果,也就是说上面对函数的校验在这个地方被用到了(注意不能在被校验的函数中提前下断点再调试,因为这样相当于提前写了int 3断点进了这个函数,造成了函数内容的改变,所以校验是不会通过的,如果一定要下断点的话可以在这个校验函数里面下断点手动过一下,或者全杀了

最后是一些赋值操作,然后在memcmp(v5, v4, v6)处进行对比

在这里可以获取到密文

1
N17EHf1DWHD40DWH/f79E05EfIH1E179E1

所以关键函数就是那一长串东西了(位于0x140004100的函数)

在调试过程中会进到这个函数

a1是一个表,应该有用

1
ghijklpqrstuvwxyzABCabcDEFdef0123GHI4567JKL+/MNOmnoPQRSXYZ8TUVW9

总有种换表base64的感觉,但实际上不是()

这个函数的返回值是输入在表中的位置(相当于取了个索引)

后面也没用上,感觉只是个初始化的

之后是这一段

这段的作用是取两个字节Src[0]+v18对应的是第一个字节,v49+17对应的是输入的第二个字节,如果两个字节相等则中间插入一个X(有点playfair密码的味道)

之后继续走,发现进行一些内容转移之后从一个变量里面到了下面这个函数

看两眼发现这个函数就是个大号的Playfair

首先取两个字节(经过了上面的加X变换的结果),然后分别取它们在上面那个表中的位置,注意到这个表是一个8x8的表

后面不放了,取值和Playfair的方法一样(同行右移1,同列下移1,不同行不同列取对角)

之后就走出sub_0x140004100,校验之后进行对比了

写脚本吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def playfair_dec(data):
table = 'ghijklpqrstuvwxyzABCabcDEFdef0123GHI4567JKL+/MNOmnoPQRSXYZ8TUVW9'
dec = ''
for i in range(0, len(data)//2):
ch_0 = data[2*i]
pos = table.index(ch_0)
r_0 = pos // 8
c_0 = pos % 8

ch_1 = data[2*i + 1]
pos = table.index(ch_1)
r_1 = pos // 8
c_1 = pos % 8
if r_0 == r_1:
# 同行的情况
dec += table[8*r_0 + (c_0-1)%8]
dec += table[8*r_1 + (c_1-1)%8]
else:
if c_0 != c_1:
# 不同行不同列
dec += table[8*r_0 + c_1]
dec += table[8*r_1 + c_0]
else:
# 同列
dec += table[8*((r_0-1)%8) + c_0]
dec += table[8*((r_1-1)%8) + c_1]
print(f'flag{{{(dec.replace('X', ''))}}}')

enc = 'N17EHf1DWHD40DWH/f79E05EfIH1E179E1'
playfair_dec(enc)
# flag{6c324d2c86a72b864a22f30e46d20220}

有一说一我想骂人,这题所有的hash值校验都是flag的hash值,直接找个md5网站都能弄出来,压根不用找异常、过校验去解题,还有那个函数校验,如果是快解的话压根不用考虑,写出来玩的,而且后面多次调用这个hash值校验,生怕别人不知道这是flag,挺弱智的,要不是为了学习反调试和try...catch...语句,这种题直接秒了算了(눈‸눈)