不知道你有没有发现,在我们电脑的C盘里的windows文件夹下有一个SysWoW64,看起来像是一个开发者的感叹,他的功能是保存在64位电脑上仍需要运行的32位程序或者依赖。
这就引出了一个在动态调试层面恐怕是最有难度的逆向反调试--天堂之门(Heaven's Gate)
这一篇文章已经写了如何在C语言中使用天堂之门技术
https://bbs.kanxue.com/thread-270153.htm
所以在此我不会过多赘述实现机制,而是在反汇编层面进行解析
一、实现原理
天堂之门的实现主要依靠操作系统提供的在不同位数CPU进行跨架构的指令调用,这使得32位和64位的指令环境可以放在同一个程序中,但目前的调试器几乎没有能够跨架构的,所以程序能够标准的在操作系统中进行操作,但是在调试器中,当走到跨架构的指令时,就会因为指令无法识别而跳飞。
而在代码层面实现之后,在反汇编层面会有一些比较明显的指令,其中分为从64位跳转到32位,该种指令如下:jmp far 33:地址
或者push 0x33call $+5add dword [esp], 5 retf
我们稍微了解一下这些指令
首先jmp far和jmp的区别是jmp far会比jmp多执行一个指令,即在修改ip的值之外还会修改CS的值,修改的值就是far后面跟的数字,在硬编码层面表现如下:
EA 77 3F 41 00 33 00
可以看见,这个指令就是在jmp一个地址的硬编码后面加上四字节的数字,这个数字就是CS修改为多少,32位寄存器CS值是0x22,64位CS寄存器值是0x33。
在ida中,修改的值不会直接显示,我们需要设置option->general->Number of opcode bytes,将后面的框中的数字改为9或者其他值(这个数字的意思是每一行能够同时指令的几个硬编码)
下图是实例

这样就能在ida里面看见每条指令对应的硬编码了。
一旦理解了jmp far指令,push 0x33``call $+5 和add dword [esp], 5 这三条指令理解起来就比较方便了
push 0x33 在栈中压入0x33,作为CS寄存器的新值。
call $+5 下一条指令的地址入栈,并继续执行下一条指令
add dword [esp], 5 栈顶的返回地址加5,指向retf的下一条指令
retf, 返回到下一条指令继续执行,同时会pop ip和pop cs
32位转成64位的如下call $+5``mov dword [rsp + 4], 0x23``add dword [rsp], 0xD``retf
原理和上面的一样,利用retf将CS寄存器的值修改,这样就可以达到在程序实现32位代码和64位代码之间的转换。
二、例题
1、西湖论剑2023 Dual personality
下载附件,打开IDA,首先就发现一片数据和标红
在上面还调用了其他的函数,有一个函数是用来修改内存的,调试查看修改了什么
从f5反汇编之后得到的代码来看,这段代码不仅修改了返回地址,还修改了开辟出来的地址,返回地址,即call这个函数的下一个指令的地址在被修改之后变成了如下
这里是IDA反汇编错误,根据硬编码来看,应该是jmp far 33:0x4011d0
所以这里天堂之门真正跳转的地址是0x4011d0,而4011d0也是我们刚才修改过内存的地址,我们直接G跳过去发现这段地址ida在乱编,显然没识别出来是什么指令
因为一开始用的是IDA32位,现在转到64位的环境无法反汇编,所以我们需要对程序的十六进制修改
红色框住的是PE结构的magic魔术字
说明文件类型:10B 32位下的PE文件 20B 64位下的PE文件
修改魔术字为20B,然后放到64位IDA中,修改基址为0x40000,会自动修复基址,之后直接G跳转到11d0

看上去就很正确了,这里有个反调试,gs:60h是PEBeginDebugging反调试位,如果检测到正在调试,该位为1,反之为0,这段代码在没有调试的情况下会把0x5DF966AE赋值给dword_407058。
之后跳转到0x1E0000,从这里在跳转回去减去0x21524111得到一个加秘密钥

得到key之后跳转到下面进行第一次加密
很容易分析并逆向出解密脚本
1 2 3 4 5 6 7 8 9 | for (int i = 0; i < 8; ++i){ DWORD bak = ans[i];ans[i] = (ans[i] - key) & 0xFFFFFFFF;key ^= bak;}BYTE *p = (BYTE *)ans;for(int i=0;i<0x20;++i)printf("%c", *(p+i)); |
相同的跳转总的有3个
第二个
到40700地址上的值,直接返回去看32位程序的40700
跳转到了401200,而我们刚才就在64位的程序上看见了401200,P生成函数进行f5,就可以看见加密函数
这里也进行了PEBeginDebugging反调试,检测到调试器和没有检测到调试器的加密会不同,如下
第三个对应的跳转到401290,到64位上反汇编如下
所以总的解密代码如下
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 | def xor_reverse(data, key): for i in range(len(data)): data[i] ^= key return datadef rol_reverse(data, offset): tmp_arr=[] for i in range(4): tmp_arr.append((data[i*2+1] << 32) + data[i*2]) for i in range(4): tmp_arr[i] = ((tmp_arr[i] >> offset[i]) | ((tmp_arr[i] << (64-offset[i])) & 0xffffffffffffffff)) & 0xffffffffffffffff for i in range(4): data[2*i] = tmp_arr[i] & 0xffffffff data[2*i+1] = tmp_arr[i] >> 32 return datadef deffusion_reverse(data, factor): for i in range(len(data)): tmp = data[i] data[i] = (data[i] - factor) & 0xffffffff factor ^= tmp return datadef main(): cmp_data = [0xE20F4FAA, 0x549941E4, 0x7E842B2C, 0x788B8FBC, 0x5E8873D3, 0x708547AE, 0xCE09B331, 0xCA0DF513] final_key = 0x4a827704 rol_offset = [0xc, 0x22, 0x38, 0xe] diffusion_factor = 0x3CA7259D flag_arr = xor_reverse(cmp_data, final_key) flag_arr = rol_reverse(flag_arr, rol_offset) flag_arr = deffusion_reverse(flag_arr, diffusion_factor) print('DASCTF{', end='') for i in flag_arr: print(int.to_bytes(i, 4, 'little').decode(),end='') print('}')if __name__ == "__main__": main()#DASCTF{6cc1e44811647d38a15017e389b3f704} |
灵感、解密脚本来源 https://kamasammohana.github.io/
2、DubheCTF 2024
从西湖论剑的题型可以看出,天堂之门这个反调试技术单独使用还是比较容易看得出来,还需要结合其他混淆和反调试技术一起使用,由于跨架构指令不能直接动调,所以逆向难度会极大的上升。
这个题就采用了SEH异常处理反调试+天堂之门的手法,隐藏掉天堂之门的入口指令和没有进入天堂之门前的一次加密,极大的提升了题目的难度。
打开附件,在主函数上有一个很明显的SEH异常处理函数
如果我们这个时候进行f5反编译,那么会是这样
但是根据汇编层面的观察,不可能只有这么点代码,往上面观察,可以看出程序一开始就加载了异常处理句柄
在接收了字符串之后马上写入了一个了try块,保存完关键地址后就int 3造成中断异常
异常之后当然是进行异常处理,而回调函数地址就是被push进栈中的loc_4140d7,这个地址被引用到异常处理结构体中
我们跳转到4140d7观察,看到很多数据,做题时以为是移动返回地址,复现时看了出题人的出题笔记才知道是花指令
下个硬断单步跟,但是首先要把其他隐藏在程序里面的花指令去掉,使用AntiDebuggerSeeker查就行

这一处是加载ntdll,直接把eax改成0
这一处是要把调用ZwSetInformationThread反调试的调用号给去掉,改成0
保存整个程序,利用OD进行调试,打开OD,f9到0x4140D7,快捷键Alt+O打开调试选项面板,在异常中去掉勾选忽略(传递给程序)以下异常:INT 3中断
这样int 3中断之后不会跳飞,这个时候再下一个断点到异常处理回调函数4140d7上,就可以单步调试代码了
调试几步后发现压入了一些数,0x32在后面可以知道是循环次数,0x5B4B9F9E很明显是一个常数,这个时候就可以猜测是TEA族

这里是将输入按照四字节分组压入栈

左移5位
左移2位
进行异或
看到这里就已经知道是XXtea了,到这里需要找到密钥key,重新打开IDA,在密文下面翻得到
由于栈展开之后会回调两次异常处理函数,所以这里的xxtea也会执行两次
到这里总算算是把入天堂之门前的加密代码解析成功,之后回调函数回来时执行except块,也就是
这里就是典型的门了,直接转x32Dbg,打开sharpOD,把能勾选的反反调试全勾上
然后一路调试到jmp far这个指令,直接跳转进jmp far后面的地址
将该地址用scylla dump下来放进ida就可以看到门后的加密了
可以看出是crc算法
所以加密算法是两次xxtea+一次crc,直接解密就行,exp如下
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | #include <stdio.h>#include <stdlib.h>#include <string.h>#include <stdint.h>#define MX (((z>>5^y<<2) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z)))#define DELTA 0x5B4B9F9Evoid crcbreak(uint32_t* flagbuf, int key) { for (int i = 0; i < 12; i++) { uint32_t enc_num = *(flagbuf + i); for (int j = 0; j < 32; j++) { if (enc_num & 1) { enc_num ^= key; enc_num /= 2; enc_num |= 1 << 31; } else enc_num /= 2; } //printf("0x%X, ", enc_num); //0x9E549543, 0x5E7CB348, 0xD9A84A2F, 0x85EB99DE, 0xB6825884, 0xC4F74EA1, 0x22B1828A, 0x290D7296, 0x198EE473, 0x9655B529, 0x38AC196A, 0x192B6236, *(flagbuf + i) = enc_num; }}void btea(uint32_t* v, int n, uint32_t const key[4]){ uint32_t y, z, sum; unsigned p, rounds, e; //加密 if (n > 1) { rounds = 6 + 52 / n; sum = 0; z = v[n - 1]; do { sum += DELTA; e = (sum >> 2) & 3; for (p = 0; p < n - 1; p++) { y = v[p + 1]; z = v[p] += MX; } y = v[0]; z = v[n - 1] += MX; } while (--rounds); } //解密 else if (n < -1) { n = -n; rounds = 0x32; sum = rounds * ((~DELTA) + 1); y = v[0]; do { e = (sum >> 2) & 3; for (p = n - 1; p > 0; p--) { z = v[p - 1]; y = v[p] -= MX; } z = v[n - 1]; y = v[0] -= MX; sum -= ((~DELTA) + 1); } while (--rounds); }}int main() { uint32_t flagbuf[] = { 0xA790FAD6, 0xE8C8A277, 0xCF0384FA, 0x2E6C7FD7, 0x6D33968B, 0x5B57C227, 0x653CA65E, 0x85C6F1FC, 0xE1F32577, 0xD4D7AE76, 0x3FAF6DC4, 0x0D599D8C }; int key = 0x84A6972F; uint32_t const k[4] = { 0x6B0E7A6B, 0xD13011EE, 0xA7E12C6D, 0xC199ACA6 }; crcbreak(flagbuf, key); btea(flagbuf, -12, k); btea(flagbuf, -12, k); puts((const char *)flagbuf); printf("\n\n"); return 0; //DubheCTF{82e1e3f8-85fe469f-8499dd48-466a9d60}} |