记录一下天堂之门技术。还在补充
天堂之门技术
0x00 简介
天堂之门技术(Heaven’s Gate)是一种病毒反检测技术。该技术基于32位应用程序的基础上,向64位进程注入恶意代码,从而隐藏API调用,使其更加难以被检测到。
在最近的羊城杯Re题OddCode
中,就出现了天堂之门技术的身影。再加上大量混淆,无效指令和跳转,使得这道题的难度变得十分大。(不过万能的地球人👴还是把它给gay出来了,salute)
之后我在CTFShow上面翻的时候,发现其“月饼杯”的第一个Re题,EasyTea
,也利用了这个技术。不过由于这个题目本身难度不大,且题目有暗示加密方式,所以也有不少人解出来了。
0x01 流程
理论基础
微软在Windows平台上嵌入了一个名叫WoW64(Windows 32 on Windows 64)
的翻译层,用于在win-x64平台上跑x86程序。
这个Wow
,基本上将每一个32位的PE程序当作一个本地独立的64位进程(native standalone 64bit process)托管在里面,并将每一个32位的系统中断翻译成64位的系统调用。
换句话说:在64位系统中,32位的应用程序运行在一个WoW64的子系统中,这个子系统为32位程序提供了一个类似沙箱的运行环境。事实上这个32位程序运行的环境也是一个64位的运行环境,系统在创建32位进程时,首先创建一个64位的进程,然后再在其中创建一个32位的子环境。32位程序所调用的系统函数,最终都要通过64位的动态链接库而实现。
恶意代码注入流程
- 从32位运行模式转入64位模式
- 从PEB64结构中获取64位NTDLL模块
- 从NTDLL中获取64位函数
- 向64位进程中完成注入
注释:PEB是Process Environment Block的缩写,其中存储了进程用户态重要的数据结构。
主要的核心技术就在于实现32位至64位的一个转换。
而就这仅仅第一步,32位跳转至64位模式,就可以实现反调试;IDA
,甚至Windbg
都无法继续调试。
0x02 32至64
Windows判别位的方式,是根据cs
段寄存器的。
在32位程序中,cs
的值是0x23;
在64位程序中,cs
的值是0x33。
所以只要修改cs
的值,就能实现切换。而实现这个切换,使用的是ljmp
长跳转指令(在Intel语法中也可以叫做jmp far ptr
)。
其实我一开始有点懵逼,因为这个jmp far
指令我也就在16位8086CPU里面见过,通过cs
配合ip
实现长转移,但是现在32位和64位的jmp
指令的索引空间已经够大了,再加上Windows推荐的平坦模式(flat mode),直接将整个4gb内存空间看成一个段使用,cs
,ss
等段寄存器全部不需要使用;而且cs
的职责也发生了变化,变成了段选择子。
16位8086下jmp far ptr
长跳转的表现
看个16位8086的例子:
1 | assume cs:code |
中间搞了一个32768字节的空数据,是因为16位下,jmp near ptr
只能在-32768~32767内有效这么长的跳转,只能由jmp far
实现,通过对cs
和ip
的共同修改实现段间跳转。
编译出来的指令是:
1 | B82795 mov ax, 9527 |
天堂之门技术中的实现
EasyTea
我选择直接使用最近做出来的EasyTea
进行讲解。
IDA32反汇编,一看
会发现,这条指令,IDA的反汇编器都没法正常反汇编出来。
且不难发现从jmp far
开始后面的指令,都变得邪门了起来,一大堆反汇编错误,因为变成64位指令了,而IDA32仍然把它当成32位的看。
于是乎我们选择使用高质量开源反汇编器capstone
来帮我们解决这个问题。
1 | from capstone import * |
得到
Capstone
反汇编的就很对了,ljmp
声明是长跳转,同时0x33:0x401258
分别对应着对cs
和eip
的修改。
而cs
改成0x33,也就是Windows对64位的描述,从而实现了模式切换。
补充:
这些是EasyTea.exe
加载时的dll
。可以发现调用了wow64cpu.dll
0x03 64至32
跳转进64位模式后,还可以选择重新回到32位。
实现方式就是以下代码
首先call
了下一个指令,也就是相当于将下一个指令的地址推进栈中。
向栈中添加了一个0x23,用于retf
时修改cs
的,这样能重新回到32位
然后add 0xD
,调整刚才推入的地址,至retf
的后面(数一下即可得知,从mov
开始至retf
,字节码的长度正好是0xD)
retf
长返回,首先从栈中弹出ip
值,然后弹出cs
值,cs:ip
共同作用实现返回。
0x04 Intel开发手册中的描述
简简单单翻了一下Intel Developer Manual
,找到了jmp
指令的详细描述。
Opcode | Instruction | Op/En | 64-Bit Mode | Compat/Leg Mode | Description |
---|---|---|---|---|---|
EA cd | JMP ptr16:16 | S | Inv. | Valid | Jump far, absolute, address given in operand |
EA cp | JMP ptr16:32 | S | Inv. | Valid | Jump far, absolute, address given in operand |
编码模式:
S模式,也就意思是段+绝对地址
配合进行跳转。且已经声明了是far jmp
长跳转了。
比如刚刚的ljmp 0x33:0x401258
,编码是
1 | EA 58 12 40 00 33 00 |
能发现
58 12 40 00
就是地址0x401258,33 00
就是cs
的0x33。
进一步解释
简单翻译了一下手册中对保护模式中长跳转的描述(注意不是8086的实模式了!)
当处理器在保护模式下运行时,JMP指令可以用来执行以下三种类型的长跳转。
- 远跳到一个符合或不符合要求的代码段。(A far jump to a conforming or non-conforming code segment.)
- 通过一个调用门的远跳(A far jump through a call gate.)
- 一个任务转换(A task switch)
(JMP指令不能用于执行权限级之间的远跳。)
后面的看不懂了,等我看懂了再补充 :p
0x05 WoW Hell: Rebuilding Heavens Gate
这是HITB2021 SecConf
中针对此技术的一个二次开发,阐明了一个全新的,通过天堂之门技术绕过杀软API Hook的攻击方法。
概要
- 32-bit Hell & Userland HIPS Design
- 通过逆向工程分析
Wow64
的设计思路- WOW64 进程初始化
- Path to The Heaven
- The Paradise Translator
- The 32-bit Hell v.s. 64-bit Heaven
wowGrail
: Rebuild the Heaven’s GatewowInjector
: One Gadget to Take Over the Wall
What “The Hell”
Host-based Instruction Prevention System(HIPS)
这展示了在没有被hooked的情况下,wow64对一个API进行的翻译操作。
杀毒软件通常会hook一些API,使得在病毒想干恶俗之事的时候被及时发现
这张图片,32位的API被反病毒软件Hook了。
这个Heaven's Gate
技术,就是假设64位的API没有被反病毒软件Hook住,那么就可以通过直接执行64位的系统调用API来绕过病毒检测机制。
总体思想
- 跳转至64位模式
- 通过
gs
段选择子(gs:0x30
)获取环境结构体(PEB64) - 通过PEB64下的
Ldr
枚举所有加载的64位模块 - 定位
NtDll64
的基址(imageBase) - 得到从DLL中获取导出API
ntdll!LdrGetProcedureAddress
- BOOM! We got the key of Heaven’s Gate!
补充:PEB64结构体
64位的PEB结构体一直没有被微软公开,这个是网上人们通过逆向工程获得的PEB64结构体信息。
其中位移0x18就有我们需要的Ldr
1 |
|
参考
2011 – Mixing x86 with x64 code
2012 – Knockin’ on Heaven’s Gate
2012 – KERNEL: Creation of Thread Environment Block (TEB)
2015 – WoW64 and So Can You Bypassing EMET
2018 – WoW64 Internals
2020 – WoW64 Subsystem Internals and Hooking Techniques
虽然但是,这写前人的文章里面的技术都不太稳定,难以实现。
有一个原因便是Windows不同版本中dll的内存分布差异:
在Win7中,比较容易,因为ntdll在4GB内存段之内;而8.1以上版本则在4GB外地址空间,发生了内存布局的改变,这就会让事情变得麻烦。
Wow64 Process Init
32-bit Program Managed in 64-bit Process
BTCpuSimulate()
这是每一个wow64进程的入口函数。
这个函数比较简单,就是在一个无限循环中调用另外一个函数RunSimulatedCode
。
这是我从自己的Windows系统中的system32/wow64cpu.dll
中反编译出来的结果。
这是RunSimulatedCode
函数的开头
r12
指向gs:0x30
,也就是TEB64结构体
r15
指向一个表,名叫TurboThunk Table
r13
指向TEB64结构体中的一个成员,WoW64 Thread Context
补充:TEB64结构体
又是一个没公开的结构体 :(
Terminus Project - _TEB64 - x64 view (rewolf.pl)
查一下表,发现偏移0x1488是
1 | _TEB64.TlsSlots+8 |
指向了TlsSlots
数组的第二个元素
这是TurboThunk
表,这是一序列的函数指针。我们需要注意的就是头尾两个元素,TurboDispatchJumpAddressEnd
和CpupReturnFromSimulatedCode
CpupReturnFromSimulatedCode
这个函数是最重要的,它是第一个将32位代码拷贝到64位的函数。
TurboDispatchJumpAddressEnd
也是一个拷贝函数。
NtAPI Trampoline
这里我们研究32位向64位的一个转变。
可以发现,这个Wow64Transition
里面,使用了ljmp 0x33:wow64cpu.dll+6009
这个代码,也就是刚刚阐述的长跳转代码,从而实现改变cs
,进而变成64位模式。
于是ljmp
下面的代码都是用64位反汇编器查看的。
上面我们刚讲到,r15
指向了一个TurboThunkDispatch
表,这里就根据r15
和一个偏移进行jmp
,进入表内函数中。这个jmp
正好是跳到了最后一个函数,CpupReturnFromSimulatedCode
中。
CpupReturnFromSimulatedCode
的开头,先尽可能地为线程体做快照,将大部分信息存储在r13
指向的结构体内(刚刚的TlsSlots
数组中)。
然后他会跳转到TurboDispatchJumpAddressEnd
函数中。
这边调用了Wow64SystemServiceEx
函数,实现一个对32位系统调用的模拟(或者叫翻译,把32位系统调用翻译成64位)。
结束后,它会从刚刚保存的信息中恢复原来的寄存器状态。
Heaven’s Translator
将x86系统调用转化成x64的
这张截图显示了ntdll
下ZwOpenProcess
函数的流程。
其中eax
获得的0xBE,实际上是一个16位大小的结构体。
1 | typedef struct _WOW64_SYSTEM_SERVICE{ |
一开始我先去System32
文件夹下找到了ntdll
,但是反编译出来的结果不一样。
事实上,这个应该是给64位exe
直接提供服务的64位的dll
,而我们需要的dll
,是为32为程序提供翻译的,所以应该在SysWoW64
下找ntdll
。两个同名的dll
实际上服务对象不一样,位数也不同。
这个才是我们需要的,不过发现结构体数值发生了一些变化。小问题,暂时不用管。
可以发现这个函数的第一个参数确实就是_WOW64_SYSTEM_SERVICE
,再加上下面有(syscall >> 12) & 3
这样的掩码,即可证实。
右边的列表,是在wow64cpu.dll
中的函数指针表。这些函数都是用来将32位系统中断翻译成64位系统中断的。
这个是直接看表执行翻译函数。
OverView
最后一个h
阶段是RestoreStatus
,就是病毒注入完后恢复状态。
Recap
A New Path to the Heaven
根据刚刚的分析,我们可以总结出这个函数调用流程。
鉴于wow64
层的稳定性,我们可以尝试根据以上流程,自己直接实现对wow64SystemServiceEx
的一个调用,从而实现绕过。
wowGrail
一个PoC代码,用于证实其绕过的有效性。
https://github.com/aaaddress1/wowGrail
wowInjector
WOW64 Thread Snapshot
One Gadget to Take Over the 32-bit Hell
想想刚刚的流程,会发现32位向64位之间的转换前,会将上下文保存。返回到32位的时候,又会重新弹出信息。如果我们能够修改这个信息,就能实现最后返回的时候跳转到我们想要的恶意代码上。
这个是一个内核函数,是用于创建PEB和TEB结构体的。
只要我们想办法找到一个块,那么剩下3各块都可以获取到。
https://github.com/aaaddress1/wowInjector
0x06 Heaven’s Gate原作者文章
0x07 实现
尝试自己实现一个有天堂之门技术的代码
暂时只准备使用32位与64位之间的切换,实现反调试与混淆。
而关于网上能找到的一堆什么32位注入64位进程,以及Ntdll API获取,总体来说都是病毒技术,没有必要搞那么复杂。
代码实现
在Visual Studio 2019
上简单实现了一下代码。
1 |
|
解析
1 | //char Input[100] = { 0 }; |
输入一个数字,然后将数字所在地址存在ecx
寄存器中。
其他寄存器似乎不太好用,因为可能会在进入x64层的时候发生修改。
当然其他寄存器也没怎么仔细试验过,有兴趣的可以每一个寄存器都试试。
1 | // X64Enter |
也就是
1 | push 0x33 |
进入x64模式
1 | // x64 mov eax, [ecx] |
这里是我实现的x64代码,由于是个demo,所以每个代码都是用keystone慢慢编译出来的。
具体来嗦就是一个循环左移3位。
1 | // X64Exit |
退出x64模式,回到x86。
改进
直接在进入x64后调用x64代码感觉太直球了,我觉得可以call
一个函数,在里面搞些操作之后再回来。
编译环境:32位主程序由
VisualStudio2019
,在x86+Debug版本下编译;x64函数代码由
Mingw
在x64+Debug版本下编译。
1 | #define _CRT_SECURE_NO_WARNINGS |
分析
1 | __asm { |
在x64模式中调用func
函数。
至于为什么能直接写call
,是的编译器用x86的代码来编译,是因为64和86下的call
指令字节码是一样的。
1 | __declspec(naked) void func(char* Input) { |
这个func
使用__declspec(naked)
来取消编译器默认的函数头和函数尾,单纯的只包含了你的代码。
于是乎,单独写一份func.c
文件,然后mingw
64模式下编译出纯净的代码,然后将其字节码拷贝进来。
又因为x64模式下的fastcall
,正好第一个传参是rcx
,与刚刚传进去的相对应(这个fastcall
的寄存器传递顺序与不同编译器有关系,mingw
可能是先rcx
吧。
1 |
|
成功调用,并返回。
0xFF 参考
主要是参考最新HITB2021 SecConf对此技术的重构
https://conference.hitb.org/hitbsecconf2021ams/sessions/wow-hell-rebuilding-heavens-gate/
https://github.com/aaaddress1/wowGrail
https://conference.hitb.org/hitbsecconf2021ams/sessions/wow-hell-rebuilding-heavens-gate/
https://github.com/aphage/wow64-gate-hook
https://www.calder-systems.com/articles/web/209983.html
https://blog.csdn.net/weixin_41890599/article/details/99556319
BYTE* / Undocumented 64-bit PEB and TEB Structures (bytepointer.com)
- 本文作者: Taardis
- 本文链接: https://taardisaa.github.io/2021/09/25/Heaven'sGate/
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!