天堂之门(Heaven’s Gate)是一种特殊的代码注入技术,主要用于在32位进程中执行64位代码,或者绕过 某些安全机制。这项技术得名于Windows系统中一个特殊的调用门机制。

关于windows系统32位程序在64位系统上的运行机制

在64位Windows系统上运行32位程序时,系统通过一套称为WOW64(Windows-on-Windows 64-bit)的兼容层机制实现无缝运行。

API调用转换:当32位程序调用Windows API时,WOW64将32位API调用动态转换为对应的64位API(例如将kernel32.dll重定向到kernel64.dll)。

注册表重定向:32位程序访问HKEY_LOCAL_MACHINE\SOFTWARE时,会被重定向到HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node,避免与64位程序冲突。

文件系统重定向:32位程序访问C:\Program Files时,实际指向C:\Program Files (x86)(可通过Wow64DisableWow64FsRedirection API禁用重定向)。

DLL加载:32位程序加载的DLL来自SysWOW64目录(如C:\Windows\SysWOW64),64位程序则使用System32目录(如C:\Windows\System32)。

效果作用

即便调用了API函数CreateFileA来创建文件,在Xdbg中仍然无法断下,这可以给软件逆向分析人员造成困难,使其没注意到关键操作

不过如果分析人员断下的是ntdll64中的zwcreatefie,就仍然能够断点断下

微步的在线沙箱分析无法获取天堂之门的操作,即使其创建了文件,仍然没有监测到

病毒分析常用的进程监控工具是可以捕获到文件创建的

关键实现

编写一个x64call函数,将要调用的64位API名称传入进行调用

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
uint64_t X64Call(uint64_t proc, uint32_t argc, ...) {
uint64_t* args = (uint64_t*)(&argc + 1);
uint64_t ret = 0;
static uint8_t code[] = {
/* [bits 32]
push ebx
mov ebx, esp
and esp, 0xFFFFFFF8

push 0x33
push _next_x64_code(0x12345678)这里是乱填的,也是后面不整的第一个点
retf
进入x64调用模式,retf(Far Return)会修改 CS 为 0x33,进入 长模式(64位)
*/
0x53, 0x89, 0xE3, 0x83, 0xE4, 0xF8,
0x6A, 0x33, 0x68, 0x78, 0x56, 0x34, 0x12, 0xCB,
/* [bits 64]
push rsi
push rdi ; 保存非易失寄存器

mov rsi, args ; 加载参数列表指针 mov rsi, 0x1122334455667788 (占位符)这里是后面补正的第二个点
mov rcx, [rsi] ; 第1个参数 -> rcx
mov rdx, [rsi+8] ; 第2个参数 -> rdx
mov r8, [rsi+16] ; 第3个参数 -> r8
mov r9, [rsi+24] ; 第4个参数 -> r9

mov rax, argc ; 参数总数 argc也是预先设定了占位符,后面补正的第三个点
args_start:cmp rax, 4 ; 前4个参数已加载
jle args_end ; 如果 <=4,跳过栈传参
mov rdi, [rsi+8*rax-8] ; 取第N个参数(N>4)
push rdi ; 压栈(x64调用约定)dec rax
jmp args_start
args_end:

sub rsp, 32 ; 影子空间(Shadow Space)
call rax ; 调用目标函数(proc) 预先占位符,猴戏补正的第四个点

mov rdi, &ret ; 存储返回值
mov [rdi], rax

pop rdi
pop rsi ; 恢复非易失寄存器
*/
0x56, 0x57,
0x48, 0xBE, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x48, 0x8B, 0xE, 0x48, 0x8B, 0x56, 0x8, 0x4C, 0x8B, 0x46, 0x10, 0x4C, 0x8B, 0x4E, 0x18,
0x48, 0xB8, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x48, 0x83, 0xF8, 0x4, 0x7E, 0xB, 0x48, 0x8B, 0x7C, 0xC6, 0xF8, 0x57, 0x48, 0xFF, 0xC8, 0xEB, 0xEF,
0x48, 0xB8, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x48, 0x83, 0xEC, 0x20, 0xFF, 0xD0,
0x48, 0xBF, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x48, 0x89, 0x7,
0x5F, 0x5E,
/* [bits 64]
push 0x23
push _next_x86_code 预先占位符,后续补正的第五个点
retfq
retf(Far Return)会修改 CS 为 0x23,进入 短模式(32位)
*/
0x6A, 0x23, 0x68, 0x78, 0x56, 0x34, 0x12, 0x48, 0xCB,
/* [bits 32]
mov esp, ebx ; 恢复32位栈指针
pop ebx
ret ; 返回调用者
*/
0x89, 0xDC, 0x5B,
0xC3
};

static uint32_t ptr = NULL;
if (!ptr) {
ptr = (uint32_t)VirtualAlloc(NULL, sizeof(code), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
for (int i = 0; i < sizeof(code); i++) ((PBYTE)ptr)[i] = code[i];
}
//上面是一个示例的x64call,下面需要与传递的参数对应起来
*(uint32_t*)(ptr + 9) = ptr + 14;// push _next_x64_code(0x12345678)这里是乱填的,也是后面不整的第一个点,ptr + 14是x64call的真正地址
*(uint64_t*)(ptr + 18) = (uint64_t)args;//补正参数内容
*(uint64_t*)(ptr + 43) = (uint64_t)argc;//补正参数个数
*(uint64_t*)(ptr + 70) = proc; //补正函数地址
*(uint64_t*)(ptr + 86) = (uint64_t)&ret;//接收返回地址用于最后的返回
*(uint32_t*)(ptr + 102) = ptr + 108;//补正32位模式返回地址
((void(*)())ptr)();
return ret;
}

在调用x64call时,传入的函数地址也得是x64下获取的函数地址,这点可以通过访问64位系统的PEB来实现。

在windows操作系统中 32位程序(WoW64) 下,进程确实同时拥有 32位PEB(_PEB) 和 64位PEB(_PEB64)

PEB存放的位置

首先切换到64位模式下,通过x64汇编获取GS:[0x60]处的PEB64结构,其他部分手法与上个函数相同,就不写注释了

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
void GetPEB64(void *peb64) {
static uint8_t code[] = {
/* [bits 32]
mov esi, peb64
push 0x33
push _next_x64_code
retf
*/
0xBE, 0x78, 0x56, 0x34, 0x12, 0x6A, 0x33, 0x68, 0x78, 0x56, 0x34, 0x12, 0xCB,
/* [bits 64]
mov rax, gs:[0x60] 获取64位PEB结构
mov [esi], rax
*/
0x65, 0x48, 0xA1, 0x60, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x67, 0x48, 0x89, 0x6,
/* [bits 64]
push 0x23
push _next_x86_code
retfq
*/
0x6A, 0x23, 0x68, 0x78, 0x56, 0x34, 0x12, 0x48, 0xCB,
/* [bits 32]
ret
*/
0xC3
};

static uint32_t ptr = NULL;
if (!ptr) {
ptr = (uint32_t)VirtualAlloc(NULL, sizeof(code), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
for (int i = 0; i < sizeof(code); i++) ((PBYTE)ptr)[i] = code[i];
}
*(uint32_t*)(ptr + 1) = (uint32_t)peb64;
*(uint32_t*)(ptr + 8) = ptr + 13;
*(uint32_t*)(ptr + 31) = ptr + 37;
((void(*)())ptr)();
}

memcopy函数的32转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
41
42
43
44
45
46
47
48
void memcpy64(uint64_t dst, uint64_t src, uint64_t sz) {
static uint8_t code[] = {
/* [bits 32]
push 0x33
push _next_x64_code
retf
*/
0x6A, 0x33, 0x68, 0x78, 0x56, 0x34, 0x12, 0xCB,
/* [bits 64]
push rsi
push rdi
mov rsi, src
mov rdi, dst 64位汇编复制内存
mov rcx, sz
rep movsb
pop rsi
pop rdi
*/
0x56, 0x57,
0x48, 0xBE, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11,
0x48, 0xBF, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11,
0x48, 0xB9, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11,
0xF3, 0xA4,
0x5E, 0x5F,
/* [bits 64]
push 0x23
push _next_x86_code
retfq
*/
0x6A, 0x23, 0x68, 0x78, 0x56, 0x34, 0x12, 0x48, 0xCB,
/* [bits 32]
ret
*/
0xC3
};

static uint32_t ptr = NULL;
if (!ptr) {
ptr = (uint32_t)VirtualAlloc(NULL, sizeof(code), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
for (int i = 0; i < sizeof(code); i++) ((PBYTE)ptr)[i] = code[i];
}
*(uint32_t*)(ptr + 3) = ptr + 8;
*(uint64_t*)(ptr + 12) = src;
*(uint64_t*)(ptr + 22) = dst;
*(uint64_t*)(ptr + 32) = sz;
*(uint32_t*)(ptr + 47) = ptr + 53;
((void(*)())ptr)();
}

经典动态获取指定模块的地址,从PEB+0x18获取Ldr的地址从Ldr+0x10获取InLoadOrderModuleList地址遍历InLoadOrderModuleList获取模块基址通过模块基址获取模块名,并与moduleName比对,比对成功则返回该模块基址

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
uint64_t GetModuleHandle64(const WCHAR *moduleName) {
uint64_t peb64;
/* nt!_PEB_LDR_DATA
+0x000 Length : Uint4B
+0x004 Initialized : UChar
+0x008 SsHandle : Ptr64 Void
+0x010 InLoadOrderModuleList : _LIST_ENTRY
*/
uint64_t ldrData;
/*
ptr to InLoadOrderModuleList
*/
uint64_t head;
/*
typedef struct _LDR_MODULE {
+0x000 LIST_ENTRY InLoadOrderModuleList;
+0x010 LIST_ENTRY InMemoryOrderModuleList;
+0x020 LIST_ENTRY InInitializationOrderModuleList;
+0x030 PVOID BaseAddress;
+0x038 PVOID EntryPoint;
+0x040 ULONG SizeOfImage;
+0x048 UNICODE_STRING FullDllName;
+0x058 UNICODE_STRING BaseDllName;
...
} LDR_MODULE, *PLDR_MODULE;
*/
uint64_t pNode;
GetPEB64(&peb64);
memcpy64((uint64_t)&ldrData, peb64 + 0x18, 8);
head = ldrData + 0x10;
memcpy64((uint64_t)&pNode, head, 8);
while (pNode != head) {
uint64_t buffer;
memcpy64((uint64_t)(unsigned)(&buffer), pNode + 96, 8); // tmp = pNode->BaseDllName->Buffer
if (buffer) {
WCHAR curModuleName[32] = {0};
memcpy64((uint64_t)curModuleName, buffer, 60);
if (!lstrcmpiW(moduleName, curModuleName)) {
uint64_t base;
memcpy64((uint64_t)&base, pNode + 48, 8);
return base;
}
}
memcpy64((uint64_t)&pNode, pNode, 8); // pNode = pNode->Flink
}
return NULL;
}

从kernelbase中获取getprocessadress的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
uint64_t MyGetProcAddress(uint64_t hModule, const char* func) {
IMAGE_DOS_HEADER dos;
memcpy64((uint64_t)&dos, hModule, sizeof(dos));
IMAGE_NT_HEADERS64 nt;
memcpy64((uint64_t)&nt, hModule + dos.e_lfanew, sizeof(nt));
IMAGE_EXPORT_DIRECTORY expo;
memcpy64((uint64_t)&expo, hModule + nt.OptionalHeader.DataDirectory[0].VirtualAddress, sizeof(expo));

for (uint64_t i = 0; i < expo.NumberOfNames; i++) {
DWORD pName;
memcpy64((uint64_t)&pName, hModule + expo.AddressOfNames + (4 * i), 4);
char name[64] = {0};
memcpy64((uint64_t)name, hModule + pName, 64);
if (!lstrcmpA(name, func)) {
WORD ord;
memcpy64((uint64_t)&ord, hModule + expo.AddressOfNameOrdinals + (2 * i), 2);
uint32_t addr;
memcpy64((uint64_t)&addr, hModule + expo.AddressOfFunctions + (4 * ord), 4);
return hModule + addr;
}
}
return NULL;
}

创建文件测试

1
2
3
4
5
6
7
8
9
10
int main() {
uint64_t kernel32 = GetKernel32();
uint64_t user32 = LoadLibrary64("user32.dll");
uint64_t CreateFile64 = GetProcAddress64(kernel32, "CreateFileA");
uint64_t WriteFile64 = GetProcAddress64(kernel32, "WriteFile");
uint64_t ReadFile64 = GetProcAddress64(kernel32, "ReadFile");
uint64_t CloseHandle64 = GetProcAddress64(kernel32, "CloseHandle");
hFile = X64Call(CreateFile64, 7, (uint64_t)path, (uint64_t)GENERIC_WRITE, (uint64_t)NULL, (uint64_t)NULL, (uint64_t)CREATE_NEW, (uint64_t)FILE_ATTRIBUTE_NORMAL, (uint64_t)NULL);
system("pause");
}

总结

天堂之门主要作用于反沙箱,对于hook在ntdll.dll(64位版本)的EDR等杀毒产品用处不大

这里有一点要注意,虽然ntdll也有32位版本,但是32位版本经过转换后仍然会调用64位版本的ntdll