文章总结: 这篇文章详细介绍了Heaven’sGate技术,这是一种通过修改CS段寄存器值在32位和64位模式间切换的跨架构技术。文章解释了技术原理,提供了具体代码示例展示如何在32位程序中调用64位API,并讨论了其在反调试、绕过安全防护、游戏外挂和恶意软件等场景的应用,增加了安全人员分析的难度。
综合评分: 87
文章分类: 二进制安全,漏洞分析,逆向分析,恶意软件,免杀
《天堂之门》技术
原创
MicroPest
MicroPest
2025年1月27日 16:13
安徽
这两天,中国的DeepSeek AI大模型火遍全球。
当我第一次用到它的时候就感受到了它的超然,因为我想要的结果它直接就给了,这是其它GPT无法体会到的胜人一筹,虽然我还无法领略到它的强大魅力全貌,但直觉告诉我它很智能很强大。果不其然,在我写下上篇(Pycharm接入DeepSeek-Coder)后第二天,梁总闪亮登场,刷爆全球。
此文,不是来写DeepSeek,我的认识还很肤浅。
今天,我们来聊聊这个:Heaven’s Gate技术(天堂之门),又一个脑洞大开、魅力非凡的技术点。天堂之门的实现主要依靠操作系统提供的在不同位数CPU进行跨架构的指令调用,使得32位和64位的指令环境可以放在同一个程序中,但目前的调试器几乎没有能够跨架构的,所以程序能够标准的在操作系统中进行操作,但是在调试器中,当走到跨架构的指令时,就会因为指令无法识别而跳飞。再换个场景又如,当在32位下Hook某函数时,安全防护软件一定会用32位的操作监管,但当此时切换进入64位,这时释放个文件,防护软件是监控不到,失效了。亲,晓得了吧,我被惊住了。
我们先来认识下:在代码层面实现之后,在反汇编层面会有一些比较明显的指令,其中分为从64位跳转到32位,该种指令如下:
jmp far 33:地址;//jmp far和jmp的区别是jmp far会比jmp多执行一个指令,即在修改ip的值之外还会修改CS的值,修改的值就是far后面跟的数字。
或者
push 0x33;//在栈中压入0x33,作为CS寄存器的新值。
call $+5;//下一条指令的地址入栈,并继续执行下一条指令
add dword [esp], 5;//栈顶的返回地址加5,指向retf的下一条指令
retf;//返回到下一条指令继续执行,同时会pop ip和pop cs
当我学习后,才发觉这项技术最早有文档出现于201?年,以下分析以github上一个程序为例(https://github.com/bluesadi/Heavens-Gate)来解读《天堂之门》这项技术。
一、基础知识铺垫
在 Windows 操作系统中,CS段寄存器的值确实可以用来判断当前代码是在32位模式还是64位模式下执行。以下是详细解释:
- CS段寄存器的值
CS段寄存器(Code Segment Register)是 x86/x64 架构中的一个段寄存器,用于指示当前代码段的段选择子(Segment Selector)。在保护模式下,CS段寄存器的值不仅用于选择段描述符,还包含当前代码的权限级别(CPL)和模式信息。
- CS段寄存器的值与模式
CS = 0x23:这是一个 32位代码段 的段选择子。当 CS = 0x23 时,CPU 处于 32位模式,执行 32 位指令。在 Windows 中,32 位应用程序或 32 位兼容模式下的代码会使用此值。
CS = 0x33:这是一个 64位代码段 的段选择子。当 CS = 0x33 时,CPU 处于 64位模式,执行 64 位指令。在 Windows 中,64 位应用程序或 64 位内核代码会使用此值。
- 段选择子的结构
段选择子(如 CS 的值)是一个 16 位的值,其结构如下:
Index(3..15位)、TI(2位)、RPL(0..1位)。
Index:索引,用于在 GDT(全局描述符表)或 LDT(局部描述符表)中查找段描述符(Index = 1:通常用于内核代码段/= 2:通常用于内核数据段/= 3:通常用于用户代码段/= 4:通常用于用户数据段/= 5 及以上:用于其他段(如 TSS、调用门、任务门等))。
TI(Table Indicator):指示使用 GDT(TI=0)还是 LDT(TI=1)。
RPL(Requested Privilege Level):请求权限级别,表示当前代码的运行权限。
对于 CS = 0x23 和 CS = 0x33:
0x23:Index = 4, TI = 0, RPL = 3(32 位用户模式代码段)。
0x33:Index = 6, TI = 0, RPL = 3(64 位用户模式代码段)。
- 总结
CS = 0x23:32 位模式。
CS = 0x33:64 位模式。
因此,通过检查CS段寄存器的值,可以确定当前代码是在32位模式还是64位模式下运行。
二、代码解读
1、代码方面的基础知识
这是CreateFileA的汇编代码,对照函数CreateFileA的参数如下:
HANDLE CreateFileA(
[in] LPCSTR lpFileName,
[in] DWORD dwDesiredAccess,
[in] DWORD dwShareMode,
[in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
[in] DWORD dwCreationDisposition,
[in] DWORD dwFlagsAndAttributes,
[in, optional] HANDLE hTemplateFile
);
所以,参数压入栈的顺序是自右向左;
2、示例为两个cpp,主程序test.cpp,函数程序heaven’s gate.cpp。
2-1、主程序test.cpp:
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”);
char path[MAX_PATH];
char hacked[] = “Hacked by 34r7hm4n”;
uint64_t hFile;
char buffer[100] = { 0 };
GetCurrentDirectoryA(MAX_PATH, path);
lstrcatA(path, “\test.txt”);
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);
X64Call(CloseHandle64, 1, hFile);
hFile = X64Call(CreateFile64, 7, (uint64_t)path, (uint64_t)GENERIC_WRITE, (uint64_t)NULL, (uint64_t)NULL, (uint64_t)OPEN_EXISTING, (uint64_t)FILE_ATTRIBUTE_NORMAL, (uint64_t)NULL);
X64Call(WriteFile64, 5, (uint64_t)hFile, (uint64_t)hacked, (uint64_t)lstrlenA(hacked), (uint64_t)NULL, (uint64_t)NULL);
X64Call(CloseHandle64, 1, hFile);
hFile = X64Call(CreateFile64, 7, (uint64_t)path, (uint64_t)GENERIC_READ, (uint64_t)NULL, (uint64_t)NULL, (uint64_t)OPEN_EXISTING, (uint64_t)FILE_ATTRIBUTE_NORMAL, (uint64_t)NULL);
X64Call(ReadFile64, 5, (uint64_t)hFile, (uint64_t)buffer, (uint64_t)sizeof(buffer), (uint64_t)NULL, (uint64_t)NULL);
X64Call(CloseHandle64, 1, hFile);
printf(“%s\n”, buffer);
system(“pause”);
}
2-2、函数程序heaven’s gate.cpp:
最核心的就是下面的(1)(2)两块,32位和64位的切换。
(1)x86->x64,切换到64位模式
[bits 32]
push ebx
mov ebx, esp
and esp, 0xFFFFFFF8;使rsp与8对齐,这是64位汇编的栈对齐要求,否则在执行某些系统调用时可能会出错:
push 0x33
push _next_x64_code
retf
-
功能:
-
push 0x33:将64位代码段选择子(0x33)压入栈中。 -
push _next_x64_code:将64位代码的入口地址压入栈中。 -
retf:远返回(Far Return),同时弹出栈顶的值作为返回地址,并弹出下一个值作为段选择子。 -
执行
retf后,CPU会切换到64位模式,并跳转到_next_x64_code处执行。
(2)x64->x86,切换到32位模式
[bits 64]
push 0x23
push _next_x86_code
retfq
-
功能:
-
push 0x23:将32位代码段选择子(0x23)压入栈中。 -
push _next_x86_code:将32位代码的入口地址压入栈中。 -
retfq:远返回(Far Return),同时弹出栈顶的值作为返回地址,并弹出下一个值作为段选择子。 -
执行
retfq后,CPU会切换回32位模式,并跳转到_next_x86_code处执行。
关于_next_x64_code和_next_x86_code的指向,在代码中进行动态的填充。
(3)重要的64位函数传参的调用方式
在x64的规范中,第一批传递子程序的四个参数依次存放于寄存器RCX、RDX、R8、R9。因此,如果只传递一个参数就会放到RCX寄存器中。如果还有第二参数就会将其放入RDX寄存器中,其他参数从右至左入栈。
push rsi
push rdi
mov rsi, args;参数位置
mov rcx, [rsi];参数第一个
mov rdx, [rsi+8];参数第二个
mov r8, [rsi+16];参数第三个
mov r9, [rsi+24];参数第四个
mov rax, argc;参数个数
args_start:
cmp rax, 4;参数是否多于四个
jle args_end
mov rdi, [rsi+8*rax-8];参数多于四个时,入栈方式
push rdi
dec rax
jmp args_start
args_end:
mov rax, proc;
sub rsp, 32
call rax
mov rdi, &ret
mov [rdi], rax
pop rdi
pop rsi
(4)X64Call调用
(4-1) 参数传递
X64Call 的第一个参数是 proc,即要调用的64位函数的地址。
-
其他参数通过
args数组传递: -
前4个参数通过寄存器(
rcx、rdx、r8、r9)传递。 -
剩余的参数通过栈传递。
(4-2) 函数调用
- 在64位模式下,
X64Call使用call rax指令调用指定的64位函数(proc)。 - 调用完成后,返回值存储在
rax寄存器中。
(4-3) 返回值处理
- 将
rax的值保存到ret中,供调用者使用。
形如: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);
-
参数传递:
-
CreateFile64是CreateFileA的64位地址。 -
7个参数通过
args数组传递: -
path:文件路径。 -
GENERIC_WRITE:访问模式。 -
NULL:共享模式。 -
NULL:安全属性。 -
CREATE_NEW:创建方式。 -
FILE_ATTRIBUTE_NORMAL:文件属性。 -
NULL:模板文件句柄。 -
函数调用:
-
X64Call切换到64位模式,调用CreateFileA。 -
调用完成后,返回值(文件句柄)存储在
hFile中。
(5). 总结
X64Call通过 Heaven’s Gate技术,在32位模式下切换到64位模式,调用指定的64位函数。它支持传递多个参数,并处理返回值。X64Call是HeavensGate.cpp的核心函数,用于实现32位程序调用64位API的功能。
3、两个文件的执行顺序:
(1) test.cpp 的调用链
main()函数开始执行。- 调用
GetKernel32():
- 调用
GetModuleHandle64(L"ntdll.dll"),获取ntdll.dll的64位基地址。 - 调用
MyGetProcAddress(ntdll, "LdrLoadDll"),获取LdrLoadDll函数的地址。 - 调用
X64Call(LdrLoadDll, ...),加载kernel32.dll。
- 调用
LoadLibrary64("user32.dll"):
- 调用
GetProcAddress64(GetKernel32(), "LoadLibraryA"),获取LoadLibraryA的地址。 - 调用
X64Call(LoadLibraryA, ...),加载user32.dll。
- 调用
GetProcAddress64(kernel32, "CreateFileA"),获取CreateFileA的地址。 - 调用
GetProcAddress64(kernel32, "WriteFile"),获取WriteFile的地址。 - 调用
GetProcAddress64(kernel32, "ReadFile"),获取ReadFile的地址。 - 调用
GetProcAddress64(kernel32, "CloseHandle"),获取CloseHandle的地址。 - 调用
GetCurrentDirectoryA,获取当前目录。 - 调用
X64Call(CreateFile64, ...),创建文件test.txt。 - 调用
X64Call(CloseHandle64, ...),关闭文件句柄。 - 调用
X64Call(CreateFile64, ...),以写入模式打开文件。 - 调用
X64Call(WriteFile64, ...),写入数据。 - 调用
X64Call(CloseHandle64, ...),关闭文件句柄。 - 调用
X64Call(CreateFile64, ...),以读取模式打开文件。 - 调用
X64Call(ReadFile64, ...),读取数据。 - 调用
X64Call(CloseHandle64, ...),关闭文件句柄。 - 调用
printf,输出读取的数据。 - 调用
system("pause"),暂停程序。
(2) HeavensGate.cpp 的调用链
X64Call是核心函数,负责在32位程序中调用64位函数。memcpy64、GetPEB64、GetModuleHandle64、MyGetProcAddress等函数为X64Call提供支持。
具体的看代码,不贴上来了,重要的讲完了。
三、应用场景
除了上面提到的反调试工具、脱离安全防护监管方面的问题外,还可以用到游戏外挂的修改内存数据、恶意软件在32位进程中注入64位代码并执行、在32位程序实现64位的加密算法或数据处理逻辑等场景中,着实增加了安全人员调试、分析的难度。
明天就是除夕,祝老师们、朋友们 新春快乐,阖家幸福,心想事成,万事如意!!
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:MicroPest MicroPest《《天堂之门》技术》