首页
社区
课程
招聘
[原创]天堂之门-调试器的末路
发表于: 2024-3-20 13:52 13859

[原创]天堂之门-调试器的末路

2024-3-20 13:52
13859

不知道你有没有发现,在我们电脑的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 0x33
call $+5
add 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或者其他值(这个数字的意思是每一行能够同时指令的几个硬编码)
下图是实例
image.png
image.png
这样就能在ida里面看见每条指令对应的硬编码了。
一旦理解了jmp far指令,push 0x33``call $+5add 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位代码之间的转换。

下载附件,打开IDA,首先就发现一片数据和标红
image.png
在上面还调用了其他的函数,有一个函数是用来修改内存的,调试查看修改了什么
image.png
从f5反汇编之后得到的代码来看,这段代码不仅修改了返回地址,还修改了开辟出来的地址,返回地址,即call这个函数的下一个指令的地址在被修改之后变成了如下
image.png
这里是IDA反汇编错误,根据硬编码来看,应该是
jmp far 33:0x4011d0
所以这里天堂之门真正跳转的地址是0x4011d0,而4011d0也是我们刚才修改过内存的地址,我们直接G跳过去发现这段地址ida在乱编,显然没识别出来是什么指令
image.png
因为一开始用的是IDA32位,现在转到64位的环境无法反汇编,所以我们需要对程序的十六进制修改
image.png红色框住的是PE结构的magic魔术字

说明文件类型:10B 32位下的PE文件 20B 64位下的PE文件

修改魔术字为20B,然后放到64位IDA中,修改基址为0x40000,会自动修复基址,之后直接G跳转到11d0
image.png
image.png
看上去就很正确了,这里有个反调试,gs:60h是PEBeginDebugging反调试位,如果检测到正在调试,该位为1,反之为0,这段代码在没有调试的情况下会把0x5DF966AE赋值给dword_407058。
之后跳转到0x1E0000,从这里在跳转回去减去0x21524111得到一个加秘密钥
image.png
image.png
得到key之后跳转到下面进行第一次加密
image.png
很容易分析并逆向出解密脚本

相同的跳转总的有3个
第二个
image.png
到40700地址上的值,直接返回去看32位程序的40700
image.png
跳转到了401200,而我们刚才就在64位的程序上看见了401200,P生成函数进行f5,就可以看见加密函数
这里也进行了PEBeginDebugging反调试,检测到调试器和没有检测到调试器的加密会不同,如下
image.png
第三个对应的跳转到401290,到64位上反汇编如下
image.png
所以总的解密代码如下

灵感、解密脚本来源 https://kamasammohana.github.io/

从西湖论剑的题型可以看出,天堂之门这个反调试技术单独使用还是比较容易看得出来,还需要结合其他混淆和反调试技术一起使用,由于跨架构指令不能直接动调,所以逆向难度会极大的上升。
这个题就采用了SEH异常处理反调试+天堂之门的手法,隐藏掉天堂之门的入口指令和没有进入天堂之门前的一次加密,极大的提升了题目的难度。
打开附件,在主函数上有一个很明显的SEH异常处理函数
如果我们这个时候进行f5反编译,那么会是这样
image.png
但是根据汇编层面的观察,不可能只有这么点代码,往上面观察,可以看出程序一开始就加载了异常处理句柄
image.png
在接收了字符串之后马上写入了一个了try块,保存完关键地址后就int 3造成中断异常
image.png
异常之后当然是进行异常处理,而回调函数地址就是被push进栈中的loc_4140d7,这个地址被引用到异常处理结构体中
image.png
我们跳转到4140d7观察,看到很多数据,做题时以为是移动返回地址,复现时看了出题人的出题笔记才知道是花指令image.png
下个硬断单步跟,但是首先要把其他隐藏在程序里面的花指令去掉,使用AntiDebuggerSeeker查就行
image.png
image.png
这一处是加载ntdll,直接把eax改成0
image.png
这一处是要把调用ZwSetInformationThread反调试的调用号给去掉,改成0
保存整个程序,利用OD进行调试,打开OD,f9到0x4140D7,快捷键Alt+O打开调试选项面板,在异常中去掉勾选忽略(传递给程序)以下异常:INT 3中断
image.png
这样int 3中断之后不会跳飞,这个时候再下一个断点到异常处理回调函数4140d7上,就可以单步调试代码了
image.png
调试几步后发现压入了一些数,0x32在后面可以知道是循环次数,0x5B4B9F9E很明显是一个常数,这个时候就可以猜测是TEA族
image.png
image.png
这里是将输入按照四字节分组压入栈
image.png
image.png
左移5位
image.png
左移2位
image.png
进行异或
image.png
看到这里就已经知道是XXtea了,到这里需要找到密钥key,重新打开IDA,在密文下面翻得到
image.png
由于栈展开之后会回调两次异常处理函数,所以这里的xxtea也会执行两次
到这里总算算是把入天堂之门前的加密代码解析成功,之后回调函数回来时执行except块,也就是
image.png
这里就是典型的门了,直接转x32Dbg,打开sharpOD,把能勾选的反反调试全勾上image.png
然后一路调试到jmp far这个指令,直接跳转进jmp far后面的地址
image.png将该地址用scylla dump下来放进ida就可以看到门后的加密了
image.png
可以看出是crc算法
所以加密算法是两次xxtea+一次crc,直接解密就行,exp如下

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));
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));
def xor_reverse(data, key):
    for i in range(len(data)):
        data[i] ^= key
    return data
 
def 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 data
 
def deffusion_reverse(data, factor):
    for i in range(len(data)):
        tmp = data[i]
        data[i] = (data[i] - factor) & 0xffffffff
        factor ^= tmp
    return data
 
def 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}
def xor_reverse(data, key):
    for i in range(len(data)):
        data[i] ^= key
    return data
 
def 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 data
 
def deffusion_reverse(data, factor):
    for i in range(len(data)):
        tmp = data[i]
        data[i] = (data[i] - factor) & 0xffffffff
        factor ^= tmp
    return data
 
def 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}
#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 0x5B4B9F9E
void 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}
}
#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 0x5B4B9F9E
void 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;

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 9
支持
分享
最新回复 (13)
雪    币: 4112
活跃值: (5812)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
2
2024-3-20 14:21
0
雪    币: 3870
活跃值: (3122)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
3
Cheat Engine:你好,我支持x32和x64的混合调试哦
2024-3-20 15:28
0
雪    币: 1030
活跃值: (202)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
4
ANormalUser Cheat Engine:你好,我支持x32和x64的混合调试哦
CE还没试过,但想当标题党
2024-3-20 15:35
0
雪    币: 1030
活跃值: (202)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
5
ANormalUser Cheat Engine:你好,我支持x32和x64的混合调试哦
大牛子带我学CE
2024-3-20 15:39
0
雪    币: 1460
活跃值: (1921)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
6
nian哥哥好猛,好爱好爱
2024-3-20 16:15
0
雪    币: 2466
活跃值: (4550)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
windbg也支持
2024-3-20 16:59
0
雪    币: 1030
活跃值: (202)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
8
Shangwendada nian哥哥好猛,好爱好爱
✌带带
2024-3-20 17:00
0
雪    币: 265
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
9
神!
2024-3-20 17:42
0
雪    币: 1030
活跃值: (202)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
10
默NJ windbg也支持
那个试过,但是还是会跳飞,可能是调试的姿势不对吧
2024-3-20 17:44
0
雪    币: 2086
活跃值: (1864)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
用windbg,之前vmp也是这样反调试的,32位切cs跳到64位vm 直接可以跟出来
2024-3-20 18:34
0
雪    币: 1030
活跃值: (202)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
12

更正:无法跨架构的调试器也能动调,但是需要调试时修改成正确的指令,而且保证数据不会溢出

最后于 2024-3-20 20:14 被浮虚千年编辑 ,原因:
2024-3-20 19:38
0
雪    币: 572
活跃值: (65)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
nian✌神中神,已经转发给全体校队成员学习了,争取务必理解这篇文章的精妙思想
2024-3-20 19:40
0
雪    币: 1030
活跃值: (202)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
14
yixinBC nian✌神中神,已经转发给全体校队成员学习了,争取务必理解这篇文章的精妙思想
学我又菜又爱乱写十八
2024-3-20 20:13
0
游客
登录 | 注册 方可回帖
返回
//