天堂之门(Heaven's Gate)逆向
Heaven's Gate
原理及POC
通过在32位WoW进程中执行64位代码,实现静态反编译以及干扰对Win32Api的检测实现免杀。
详见[原创]天堂之门 (Heaven's Gate) C语言实现-软件逆向-看雪-安全社区|安全招聘|kanxue.com
常见的打开天堂之门的代码块
// convert x86 to x64
6A 33 push 0x33 ; cs寄存器的新值
E8 00 00 00 00 call $+5 ;push 下一条指令的地址入栈,并继续执行下一条指令
83 04 24 05 add dword [esp], 5 ;栈顶的返回地址加5,指向retf的下一条指令
CB retf ; 通过retf,程序返回到下一条指令继续执行,但cs 寄存器已经被修改为0x33, 执行的代码是64位
// convert x64 to x86
E8 00 00 00 00 call $+5
C7 44 24 04 23 00 00 00 mov dword [rsp + 4], 0x23
83 04 24 0D add dword [rsp], 0xD
CB retf
前置知识
32位程序中调用64位程序的逆向方法
参考CTF中32位程序调用64位代码的逆向方法-安全客 - 安全资讯平台 (anquanke.com)深度好文,解答了在复现OddCode时遇到的种种odd问题的疑惑!
识别
retf是切换32位和64位的关键指令。
retf前有push 0x33(33h)类似的指令。
push 33h
add dword ptr [esp], 5
retf
或者
mov dword ptr [eax], 33h
leave
retf
retf后CS寄存器从0x23变为0x33。
程序中可能有进行支持64位的检查,如GWoC。
当一块可执行的内存,调试时无法识别汇编或者几步一跳时,有可能在是执行64位的代码。
32位代码调用函数的方式和64位代码有差异,32位程序大多通过入栈方式传参,64位程序一般用寄存器传参。
32位和64位的syscall的含义和参数有所不同。
静态
dump出64位代码后拖进ida64分析,在ida64中注意设置基址和源程序中相同便于分析。
动态
用ida64调试时,在进入64位代码所在区域前,在Edit->Segments->Edit segment处设置64位代码所在段为64位模式,可读可写可运行
天堂之门解题trick
根据call far、jmp far定位是天堂之门,找到切换64位模式的代码,然后对64位部分代码进行静态or动态分析。静态的话就是dump出64位部分代码,然后设置基址和段属性。动态就是windows下ida+windbg。
题目
OddCode
本题参考
2021年羊城杯官方Writeup公布(Reverse) (qq.com)
羊城杯_2021 OddCode 天堂之门 + unicorn反混淆 - TLSN - 博客园 (cnblogs.com)
[原创]羊城杯OddCode题解(unicorn模拟调试+求解)-CTF对抗-看雪-安全社区|安全招聘|kanxue.com
这题看起来好炫,跟着wp做做,然后就做了好久好久。用ida打开后发现有很多花指令干扰,但是略向下翻可发现此处远跳转,配合上下面的call指令序列可知此题属于heaven's gate,远跳转和call $+5指令序列分别是32位转64位和64位转32位的代码。ida对远跳转支持不好,可参考汇编中的jmp转移指令:jmp short、jmp near ptr、jmp far ptr-CSDN博客。在这里可看远跳转的机器码,小端序0033:00405313,即设置cs为0x33,地址为0x405310,也就是他下面一行call sub_401010指令。
可以从x32dbg的调试中看出远跳转是在设置cs为0x33
可以看出,程序切换到64位模式后跳转下一行,执行的是call sub_401010
执行完后经下列指令切换回32位模式。
最后根据sub401010的返回值判断flag正误,可知关键函数即sub401010
同时观察可知,跳转前将input放入esi,将一个可能是key的数组放入edi
下面就是要分析64位部分的代码,可以dump后静态分析,也可以动态分析。这个题dump出来后简单观察下就可以发现充满花指令混淆,实在是难以分析,只能动态调试下看看。
思路一:用unicorn来调试代码块,但是我一直搞不懂切换64位前寄存器的状态是怎么获得的,我的EIP指向0x2e1010时寄存器的值和搜到的这个思路的wp都不太一样,不知道是不是因为64/32位切换我没弄好。
from unicorn import *
from unicorn.x86_const import *
ADDRESS = 0x2E1000 # 程序加载的地址,可通过ida,exeinfoPe,x32dbg调试等获得
INPUT_ADDRESS = 0x2E701D # 输入的地址
KEY_ADDRESS = 0x2E705C # 16字节key的地址
with open('OddCode.exe', 'rb') as file:
file.seek(0x400)#PE文件结构代码开始部分
X64_CODE = file.read(0x4269) # 读取代码,除去32位程序剩下的所有代码
class Unidbg:
def __init__(self, flag):
mu = Uc(UC_ARCH_X86, UC_MODE_64) # 基址为0x2E1000,分配16MB内存
mu.mem_map(ADDRESS, 0x1000000)
mu.mem_write(ADDRESS, X64_CODE)
mu.mem_write(INPUT_ADDRESS, flag) # 随便写入一个flag
mu.mem_write(KEY_ADDRESS, b'\x90\xF0\x70\x7C\x52\x05\x91\x90\xAA\xDA\x8F\xFA\x7B\xBC\x79\x4D')
#跳转前存入的key
# 初始化寄存器,寄存器的状态就是切换到64位模式之前的状态,可以通过动调得到
mu.reg_write(UC_X86_REG_RAX, 1)
mu.reg_write(UC_X86_REG_RBX, 0x51902D)
mu.reg_write(UC_X86_REG_RCX, 0xD86649D8)
mu.reg_write(UC_X86_REG_RDX, 0x2E701C)
mu.reg_write(UC_X86_REG_RSI, INPUT_ADDRESS) # input参数
mu.reg_write(UC_X86_REG_RDI, KEY_ADDRESS) # key参数
mu.reg_write(UC_X86_REG_RBP, 0x6FFBBC)
mu.reg_write(UC_X86_REG_RSP, 0x6FFBAC)
mu.reg_write(UC_X86_REG_RIP, 0x2E1010)
mu.hook_add(UC_HOOK_CODE, self.trace) # hook代码执行,保存代码块执行轨迹
self.mu = mu
self.except_addr = 0
self.traces = [] # 用来保存代码块执行轨迹
def trace(self, mu, address, size, data):
if address != self.except_addr:
self.traces.append(address)
self.except_addr = address + size
def start(self):
try:
self.mu.emu_start(0x2E1010, -1)
except:
pass print([hex(addr)for addr in self.traces]) Unidbg(b'SangFor{00000000000000000000000000000000}').start()
下面就可以用unicorn去hook访问input和key的代码块,同时借助unicorn还可以鉴别花指令和正常执行的指令,详见地球人的博客。后续观察出来了判断的逻辑直接爆破。
另外值得注意,在windows下调试时windbg对32/64位切换的支持较好,其他如ida、x32dbg等调试器在retf语句后都无法调试。
因为这点耽误了很多时间,比如ida虽可以通过断点进入64位部分,但是会一直报错不能调试。稍等研究下ida和windbg结合使用再更下。
思路二:官方wp里把64位部分dump出来重新编译成exe然后运行调试。通过观察寄存器的值发现的变换逻辑。但是这里不太了解dump出来之后那个masm格式怎么写的,再学下之后补上。
西湖论剑2023 Dual personality
挖坑尽快填上。这个题就是直接识别出64位的部分dump出来然后直接分析代码逻辑就好。