Pinguw
Articles10
Tags3
Categories2

Categories

Archive

敲响天堂之门

敲响天堂之门

“天堂之门”介绍:

天堂之门技术(Heaven’s Gate)是依靠Windows提供的在不同位数CPU进行跨架构的指令调用(SysWoW64),使得32位和64位的指令环境可以放在同一个程序中,即在32位WoW64进程中执行64位代码,但是目前的调试器包括IDA在内很少有能跨架构调试的,这使得调试器的调试不能正常进行,以达到反调试的目的。

一个在动态调试层面恐怕是最有难度的逆向反调试

架构之间的切换:

首先我们要知道32位寄存器cs的值是0x23,64位cs寄存器的值是0x33,我们将cs的值在这两者间切换,便达到了跨架构运行程序的目的。

那么我们如何更改寄存器cs的值呢?
我们知道,mov指令是无法直接改变cs寄存器的值的,需要借助callretf来实现cs段的切换

call farretf(ret far)是可以同时改变cs和ip的:

1
2
3
4
push 0x33                ;0x33作为cs的新值
call $+5 ;下一条指令的地址入栈
add dword [esp], 5 ;+5后即是指向retf的下一条指令
retf ;跳转到下一条指令,同时pop is和pop cs

或者使用ljmp(jmp far)(仅在32位到64位时):

1
jmp far 33:地址

此外还有一种方式,在后面实现中将会提到;

这样就实现了将cs改为0x33的功能,即从32位跳转到64位。

对应的从64位跳转到32位的指令是:

1
2
3
4
call $+5
mov dword [rsp + 4], 0x23
add dword [rsp], 0xD
retf

天堂之门的实现建立在对cs的更改可以让程序在32位和64之间切换的基础上。

MSVC实现(Intel格式),推荐使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
_asm
{
_emit 0x6A // push 0x33
_emit 0x33

_emit 0xE8 // call $+5
_emit 0x00
_emit 0x00
_emit 0x00
_emit 0x00

_emit 0x83 // add dword [esp], 5
_emit 0x04
_emit 0x24
_emit 0x05

_emit 0xCB // retf
}

GCC实现(AT&T格式),不推荐用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__asm__ (
".byte 0x6A\n\t" // push 0x33
".byte 0x33\n\t"

".byte 0xE8\n\t" // call $+5
".byte 0x00\n\t"
".byte 0x00\n\t"
".byte 0x00\n\t"
".byte 0x00\n\t"

".byte 0x83\n\t"
".byte 0x04\n\t"
".byte 0x24\n\t"
".byte 0x05\n\t" // add dword [esp], 5

".byte 0xCB\n\t" // retf
);

实现示例

前面只介绍了对cs的更改,即cpu架构的切换,但是在我们的exe编译完成后,汇编的架构已经确定(32位),我们需要另嵌入64位的opcode到源代码中,然后让天堂之门的入口指向我们的x64_opcode才可正常执行。

下面给出一个简单的示例:

环境配置:

  • VSMicrosoft Visual C++ v.14 - 2015;
  • ida64+32;
  • Release版本;
  • x86和x64编译器结合使用;
  • 更改VS设置:

    项目-项目属性中更改以下设置:
    关闭随机基址:
    -链接器-高级-随机基址:

    保留未引用的函数/数据:
    -链接器-常规-启用增量链接:
    -链接器-优化-链接时间代码生成:使用链接时间代码生成/LTCG
    -链接器-优化-链接时间代码生成:引用:

    关闭其他优化:
    -C/C++-优化-优化:已禁用
    -C/C++-代码生成-运行库:多线程(/MT)
    -C/C++-高级-调用约定:__fastcall

代码实现:

  1. 用x64编译器编写一个简单的加密:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include <cstdio>

    void __fastcall XOR(int& a) // 统一使用__fastcall调用约定,方便传参
    {
    a += 2;
    a ^= 0x20;
    }

    int main()
    {
    int a = 0x30;

    XOR(a);

    printf("%d\n", a);

    return 0;
    }

    将编译出的exe放进ida64中扒出XOR函数的opcode:

    image-20240710162021732.png

    shift+E,提取出opcode

  2. 将代码中的XOR函数用opcode替换,main函数不作改动,用x86编译器编译运行,再次放入ida(此时运行会出错,正常现象)。

    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
    #include <cstdio>

    void __fastcall XOR(int& a)
    {
    __asm {
    _emit 0x48
    _emit 0x89
    _emit 0x4c
    _emit 0x24
    _emit 0x8
    _emit 0x48
    _emit 0x8b
    _emit 0x44
    _emit 0x24
    _emit 0x8
    _emit 0x8b
    _emit 0x0
    _emit 0x83
    _emit 0xc0
    _emit 0x2
    _emit 0x48
    _emit 0x8b
    _emit 0x4c
    _emit 0x24
    _emit 0x8
    _emit 0x89
    _emit 0x1
    _emit 0x48
    _emit 0x8b
    _emit 0x44
    _emit 0x24
    _emit 0x8
    _emit 0x8b
    _emit 0x0
    _emit 0x83
    _emit 0xf0
    _emit 0x20
    _emit 0x48
    _emit 0x8b
    _emit 0x4c
    _emit 0x24
    _emit 0x8
    _emit 0x89
    _emit 0x1
    _emit 0xc3
    }
    }

    int main()
    {
    int a = 0x30;

    XOR(a);

    printf("%d\n", a);

    return 0;
    }

    记录下这个32位程序的传参过程和opcode的起始地址,注意整个函数的起始地址跟我们的opcode的起始地址不同(也可以使用裸函数头避免这个问题)。

    image-20240710162618500.png

    image-20240710163010075.png

  3. 删去直接调用XOR的语句,使用jmp far进门,并提前push好返回地址(预留),再使用retf出门,其他部分不变。

    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
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    #include <cstdio>
    #include <windows.h>

    void __fastcall func(int& a)
    {
    __asm {
    _emit 0x48 // <---- address: 40108A
    _emit 0x89
    _emit 0x4c
    _emit 0x24
    _emit 0x8
    _emit 0x48
    _emit 0x8b
    _emit 0x44
    _emit 0x24
    _emit 0x8
    _emit 0x8b
    _emit 0x0
    _emit 0x83
    _emit 0xc0
    _emit 0x2
    _emit 0x48
    _emit 0x8b
    _emit 0x4c
    _emit 0x24
    _emit 0x8
    _emit 0x89
    _emit 0x1
    _emit 0x48
    _emit 0x8b
    _emit 0x44
    _emit 0x24
    _emit 0x8
    _emit 0x8b
    _emit 0x0
    _emit 0x83
    _emit 0xf0
    _emit 0x22
    _emit 0x48
    _emit 0x8b
    _emit 0x4c
    _emit 0x24
    _emit 0x8
    _emit 0x89
    _emit 0x1
    _emit 0xc3
    }
    }

    int main()
    {
    int a = 0x30;

    __asm {
    _emit 0x8D // lea ecx, [ebp + a] ; a
    _emit 0x4D
    _emit 0xF8
    }

    __asm {
    _emit 0x68 // push 00000000 因为64的retn弹出8字节,而32位下的push只入栈4字节,需要手动多push 4字节的填充
    _emit 0x00
    _emit 0x00
    _emit 0x00
    _emit 0x00

    _emit 0x68 // push ******** 预留好的返回地址
    _emit 0xAB
    _emit 0xCD
    _emit 0xEF
    _emit 0xAB

    _emit 0xEA // ljmp 0x33, 40108A (x64_opcode初地址)
    _emit 0x8A
    _emit 0x10
    _emit 0x40
    _emit 0x00
    _emit 0x33
    _emit 0x00
    }

    __asm {
    _emit 0xE8 // call $+5
    _emit 0x00
    _emit 0x00
    _emit 0x00
    _emit 0x00

    _emit 0xC7 // mov dword [rsp + 4], 0x23
    _emit 0x44
    _emit 0x24
    _emit 0x04
    _emit 0x23
    _emit 0x00
    _emit 0x00
    _emit 0x00

    _emit 0x83 // add dword [rsp], 0xD
    _emit 0x04
    _emit 0x24
    _emit 0x0D

    _emit 0xCB // retf
    }

    printf("%d\n", a);

    return 0;
    }

    32位编译后再次放进ida中,找到我们需要的返回地址,ida对jmp far的分析有误,不用在意

    image-20240710164249703.png

    注意这里变量a在内存中的地址改变了,我们也需要更改

  4. 完整代码:

    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
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    #include <cstdio>

    void __fastcall XOR(int& a)
    {
    __asm {
    _emit 0x48 // <--- address: 40108A
    _emit 0x89
    _emit 0x4c
    _emit 0x24
    _emit 0x8
    _emit 0x48
    _emit 0x8b
    _emit 0x44
    _emit 0x24
    _emit 0x8
    _emit 0x8b
    _emit 0x0
    _emit 0x83
    _emit 0xc0
    _emit 0x2
    _emit 0x48
    _emit 0x8b
    _emit 0x4c
    _emit 0x24
    _emit 0x8
    _emit 0x89
    _emit 0x1
    _emit 0x48
    _emit 0x8b
    _emit 0x44
    _emit 0x24
    _emit 0x8
    _emit 0x8b
    _emit 0x0
    _emit 0x83
    _emit 0xf0
    _emit 0x20
    _emit 0x48
    _emit 0x8b
    _emit 0x4c
    _emit 0x24
    _emit 0x8
    _emit 0x89
    _emit 0x1
    _emit 0xc3
    }
    }

    int main()
    {
    int a = 0x30;

    __asm {
    _emit 0x8D // lea ecx, [ebp + a] ; a
    _emit 0x4D
    _emit 0xFC // <--- 注意更改传参
    }

    __asm {
    _emit 0x68 // push 00000000 因为64的retn弹出8字节,而32位下的push只入栈4字节,需要手动多push 4字节的填充
    _emit 0x00
    _emit 0x00
    _emit 0x00
    _emit 0x00

    _emit 0x68 // push 4010E2 预留好的返回地址,指向jmp far后的第一条指令
    _emit 0xE2
    _emit 0x10
    _emit 0x40
    _emit 0x00

    _emit 0xEA // ljmp 0x33, 40108A (x64_opcode初地址)
    _emit 0x8A
    _emit 0x10
    _emit 0x40
    _emit 0x00
    _emit 0x33
    _emit 0x00
    }

    __asm {
    _emit 0xE8 // call $+5 <--- address: 4010E2
    _emit 0x00
    _emit 0x00
    _emit 0x00
    _emit 0x00

    _emit 0xC7 // mov dword [rsp + 4], 0x23
    _emit 0x44
    _emit 0x24
    _emit 0x04
    _emit 0x23
    _emit 0x00
    _emit 0x00
    _emit 0x00

    _emit 0x83 // add dword [rsp], 0xD
    _emit 0x04
    _emit 0x24
    _emit 0x0D

    _emit 0xCB // retf
    }

    printf("%d\n", a);

    return 0;
    }

    运行验证结果:

    image-20240710164805201.png

其他实现

上面是使用了jmp far进门方式,下面介绍另外两种进门方式:

利用call far进门

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
87
88
89
90
91
92
93
94
95
96
#include <cstdio>

void __fastcall XOR(int& a)
{
__asm {
_emit 0xC6 // <--- address: 40108A
_emit 0x44 // call far 会将cs也入栈,我们直接改掉
_emit 0x24 // mov byte [rsp + 4], 0
_emit 0x04
_emit 0x00

_emit 0x48
_emit 0x89
_emit 0x4c
_emit 0x24
_emit 0x8
_emit 0x48
_emit 0x8b
_emit 0x44
_emit 0x24
_emit 0x8
_emit 0x8b
_emit 0x0
_emit 0x83
_emit 0xc0
_emit 0x2
_emit 0x48
_emit 0x8b
_emit 0x4c
_emit 0x24
_emit 0x8
_emit 0x89
_emit 0x1
_emit 0x48
_emit 0x8b
_emit 0x44
_emit 0x24
_emit 0x8
_emit 0x8b
_emit 0x0
_emit 0x83
_emit 0xf0
_emit 0x20
_emit 0x48
_emit 0x8b
_emit 0x4c
_emit 0x24
_emit 0x8
_emit 0x89
_emit 0x1
_emit 0xc3
}
}

unsigned char target[8] = {0x8A, 0x10, 0x40, 0x00, 0x33, 0x00, 0x00, 0x00}; // 这里存好opcode的地址

int main()
{
int a = 0x30;

__asm {
lea ecx, [ebp - 4]
}

__asm {
call fword ptr target // call fword ptr target(4468C0) <=> call far 33:40108A
}

__asm {
_emit 0xE8 // call $+5
_emit 0x00
_emit 0x00
_emit 0x00
_emit 0x00

_emit 0xC7 // mov dword [rsp + 4], 0x23
_emit 0x44
_emit 0x24
_emit 0x04
_emit 0x23
_emit 0x00
_emit 0x00
_emit 0x00

_emit 0x83 // add dword [rsp], 0xD
_emit 0x04
_emit 0x24
_emit 0x0D

_emit 0xCB // retf
}

printf("%d\n", a);

return 0;
}

这种方法需要注意的是:call far会将cs一起入栈,一共入栈8字节,

image-20240710171556802.png

所以我们直接改掉就好,也不需要再push 0了

利用retf进门

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
87
88
89
90
91
92
93
94
95
96
97
98
99
#include <cstdio>

void __fastcall XOR(int& a)
{
__asm {
_emit 0x48 // <--- address: 40108A
_emit 0x89
_emit 0x4c
_emit 0x24
_emit 0x8
_emit 0x48
_emit 0x8b
_emit 0x44
_emit 0x24
_emit 0x8
_emit 0x8b
_emit 0x0
_emit 0x83
_emit 0xc0
_emit 0x2
_emit 0x48
_emit 0x8b
_emit 0x4c
_emit 0x24
_emit 0x8
_emit 0x89
_emit 0x1
_emit 0x48
_emit 0x8b
_emit 0x44
_emit 0x24
_emit 0x8
_emit 0x8b
_emit 0x0
_emit 0x83
_emit 0xf0
_emit 0x20
_emit 0x48
_emit 0x8b
_emit 0x4c
_emit 0x24
_emit 0x8
_emit 0x89
_emit 0x1
_emit 0xc3
}
}

int main()
{
int a = 0x30;

__asm {
lea ecx, [ebp - 4]
}

__asm {
push 0x33
call $ + 5
add dword ptr [esp], 5
retf
}

__asm {
_emit 0xE8 // call的相对跳转 call 40108A
_emit 0xA8 // 目标地址与call的后一条指令地址的差
_emit 0xFF
_emit 0xFF
_emit 0xFF
}

__asm {
_emit 0xE8 // call $+5 <--- address: 4010E2
_emit 0x00
_emit 0x00
_emit 0x00
_emit 0x00

_emit 0xC7 // mov dword [rsp + 4], 0x23
_emit 0x44
_emit 0x24
_emit 0x04
_emit 0x23
_emit 0x00
_emit 0x00
_emit 0x00

_emit 0x83 // add dword [rsp], 0xD
_emit 0x04
_emit 0x24
_emit 0x0D

_emit 0xCB // retf
}

printf("%d\n", a);

return 0;
}

这种方式最为简单,只需要预留好偏移量即可,但也最容易被发现。

以上三种跳转方式结合使用可以进一步增加天堂之门的逆向难度

出题技巧

  • 单独起一个天堂之门跳转后检测调试,如果检测到调试更改掉 key(用反调试隐藏反调试);
  • 利用花指令、SEH异常处理、SMC等混淆手段隐藏掉天堂之门入口处的跳转指令;
  • 多种跳转方式结合使用;

实战

西湖论剑2023 Dual personality

这里对题目的分析更偏向于各种跨架构的跳转,具体的加密代码分析及逆向过程仅作简略说明

32位无壳,拖进ida,

不出意外一片红,在输入后下断点:

image-20240711105323082.png

单步跳,4013E3处call完sub_401120这个函数后发现更改了从4013E8开始的后面一部分的内存,undefine + make code让ida重新识别后发现jmp far指令,这就是门的入口了:

image-20240711105726601.png

那么sub_401120就是利用了SMC开门的函数,标记一下,

image-20240711105853960.png

看字节码找到jmp到的位置:4011D0也就是上面push进栈的地址,

image-20240711105925909.png

ida反汇编失败,因为jmp far已经更改了cs,所以4011D0这里都是x64的opcode了,ida32显然不能正确识别,

需要我们更改exe的魔术子,改用ida64分析,

image-20240711110409016.png

PE文件里这个位置的010B是32位文件标志字,020B是64位文件标志字,我们把01改成02,放进ida64:

image-20240711110612713.png

在这里更改基地址为400000,然后go 0x4011D0,C+P识别为函数:

image-20240711112536004.png

看到这里检测调试,然后如果不在调试才给dword_407058赋正确的初始值,然后jmp到407000的内存内容的位置,

回到ida32中查看407000内存的内容:

image-20240711111218184.png

那么这个4011D0函数运行完后会jmp到5E0000的位置,跳过去看看,

image-20240711111508765.png

如果eax的值不为零就跳转到4013EF,那么下面就go 4013EF,

image-20240711111618932.png

这里把dword_407058减去了一个值,然后继续jmp,继续跟,

401417这里是第一个加密函数,是对dword_407058做的操作,我们把dword_407058更名成key:

image-20240711112150530.png

上面的操作执行完后会跳转到这个位置:

image-20240711112413001.png

这里看到了我们熟悉的call far,跳转的目的地就是byte_40700C的内容:401200

可见这里也同时改了cs,是第二个门,我们在ida64中go 401200看第二段加密:

image-20240711112548433.png

声明函数后可以看到简陋的反汇编,这里也判断了调试的标志位,那么正确的加密应该是else里的内容,

这个函数进行完后就直接retf了,我们回到上一张图的位置继续看:

image-20240711112413001.png

又用到了我们第一次进门用的entry_gate函数,push的地址为401290,那么第三个门就是401290,到ida64中go 401290:

image-20240711112949827.png

进行了一些位运算,注意这里的操作是错位的,

然后回到汇编层面:

image-20240711121421595.png

把0x4014C5赋给dword_407000,那么5E0000的地方就是jmp到4014C5,

image-20240711121612927.png

注意此时dword_407000里带着0x23,那么下次jmp就会把cs改回0x23也就是回到32位,

image-20240711121743651.png

image-20240711121751522.png

最后就是check flag了。

参考链接

Author:Pinguw
Link:https://pinguw.github.io/2024/07/10/Reverse/Heaven-Gate/
看完了吗,再去看看博主的其他文章叭:)