天堂之门
本文最后更新于52 天前,其中的信息可能已经过时,如有错误请发送邮件到2624241828@qq.com

天堂之门 (Heaven’s Gate) 是一种在32位WoW64进程中执行64位代码,以及直接调用64位WIN32 API函数的技术。从安全角度看,天堂之门可以作为一种软件保护技术,用于防止静态分析以及跨进程的API Hook;从恶意代码角度看,该技术可以绕过沙盒对WIN32 API调用的检测。

实现原理

天堂之门的实现依靠操作系统提供的不同位数CPU在进行跨架构的指令调用,在用户空间完成,使得32位和64位架构可以放在同一程序中,但目前调试器无法跨架构,传统的ring3层应用调试器通常是架构绑定的,32位调试器只能处理32位上下文,64位调试器通常处理64位上下文,所以程序能标准在操作系统中进行操作,但是在调试器中,走到跨架构指令时,会出现指令集误读(出乱码),指针丢失(由于x86和x64的指令长度和解析方式不同,调试器计算的EIP(指令指针)与CPU实际执行的RIP完全不同步),导致出现跳飞情况(比如直接崩溃,程序瞬间执行完64位代码,跑到内存区域等)

为什么操作系统和cpu允许这种操作

现代的x64CPU(intel或者AMD)在硬件设计上双模式,cpu处于的模式比取决于代码段寄存器(CS,code segment)的值

  • 当cs选择子指向描述符0x23时,cpu处于兼容模式,像传统32位cpu一样工作
  • 当cs选择子指向描述符0x33时,cpu处于长模式,执行64位指令集
  • 指令jmp 0x33:offset是硬件指令,当程序执行这条指令时,cpu硬件直接响应,不需要内核每次介入
  • 触发天堂之门的指令通常是jmp far(远跳转)或call far(远调用)。格式是jmp <选择子>:<偏移地址>
    补:
  • 如jmp 0x33:0x12345678
    对应机器码可能是EA 78 56 34 12 33 00
    执行过程:
  1. cpu读到EA(jmp far)
  2. cpu读到33 00,把它塞进cs寄存器
  3. cs寄存器改变,cpu硬件用0x33去查GDT表第六行
  4. cpu发现第六行写着64-bit Code Segment
  5. cpu内部电路切换到64位
  6. cpu读到 78 56 34 12,把它塞进rip(指令指针)跳转到对应地址继续执行
  7. cpu按64位工作

正常32位程序调用api过程
假设ReadFile(file, buffer, …)

  1. 代码调用Kernel32.dll里的readfile
  2. Kernel32.dll检查参数,调用32位Ntdll.dll中的NtReadFile(或者ZwReadFile)
  3. Ntdll.dll不会执行syscall来进入内核,而是调用wow64cpu.dll,将系统调用号放进eax,执行call dword ptr fs:[0xC0],这个指针指向wow64cpu.dll中的X86SwitchTo64BitMode函数
  4. wow64cpu.dll执行jmp 0x33:Offset(far jump),cs改变,cpu切换到64位长模式
  5. 代码跳转到64位的wow64.dll,代码仍在wow64cpu.dll的地址空间,但已经是64位指令,调用wow64.dll中的系统服务分发函数(通常是Wow64SystemServiceEx)
  6. 参数转换,将32位参数变成64位格式
  7. wow64.dll调用系统中真正的64位的Ntdll.dll中的NtReadFile
  8. Ntdll.dll执行syscall,cpu进入ring0(内核),windows内核(ntoskrnl.exe)收到请求,驱动硬盘读取数据,将数据写入内存
  9. 64位wow64.dll拿到结果,把64位的返回值转换回32位格式
  10. wow64.dll返回给wow64cpu.dll。调用wow64cpu.dll,执行jmp 0x23:Offset
  11. 回到32位的Ntdll.dll到Kernel32.dll到程序。
  • 选择子
    在x86保护模式下,内存是分段管理的
  1. GDT(全局描述符表):操作系统会在启动时填好一个表,每一行(称为描述符)描述了一块内存区域的属性
    第4行:这是一个32位的代码段
    第6行:这是一个64位的代码段
  2. cs寄存器(代码段寄存器):CPU里有个专门存放“当前在执行哪个段”的寄存器。但是,CS寄存器里存的不是地址,而是菜单的索引号
  3. 描述符:在内存里,一个描述符占用 8个字节 (64位),记录内存地址和属性
  4. 选择子:存放在CS寄存器里的这个“索引号”,就叫选择子
  • 选择子规定格式
    window操作系统规定了选择子的数值,以0x33为例,把它变成二进制0000 0000 0011 0011
    最低2位 (RPL) :代表权限级别,11(二进制)=3,ring 3,即用户空间权限
    第3位 (TI) :0代表查GDT(全局表),1代表查LDT(局部表)。这里是0
    剩下的高13位 (Index) :代表菜单的行号。
    0x33的高13位算下来是6
    0x23的高13位算下来是4
    win x64内核本身是64位,为了运行32位程序,它构建了一个名为wow64的模拟层,正常32位程序调用api(比如readfile)时,系统在后台进行了32位到64位,进入内核,返回64位,切回32位的操作
  • 天堂之门相当于手动触发了本该由系统dll(wow64cpu.dll-负责执行那条jmp 0x33:offset指令)自动完成的切换动作
  • 对于操作系统来说,线程不变,只是模式改变
    天堂之门c语言实现,里面引用文章挺多的
    小概括

WoW64进程

WOW64(Windows-On-Windows 64bit)是X64 Windows操作系统的一个子系统,为32位应用程序提供运行环境。
X86指令集是一个指令集架构家族,最初在Intel 8086处理器中引入,开始它主要用于16位系统。从Intel 386处理器发布开始升级为32位,并且32位指令集保持了很久,32位CPU将内存空间限制到了4G(单一用户模式进程至少是这样)。随着RAM的越来越大,4G限制就成了瓶颈,系统无法使用更大的内存空间。于是Intel发布了64位的IA64架构,但是IA64处理器无法运行X86代码,AMD发明了x86-64(现在的64位架构,也是AMD64,后来intel也使用了同一套指令集)它是对X86架构的增量更新,用于添加64位支持。这种架构的X64处理器可以执行X86代码,所以用户可以在X64处理器上运行现有的程序和操作系统。
wow64,解决了CPU架构升级带来的兼容性问题,允许用户在现代X64处理器上无缝运行现有的32位X86程序。利用 CPU 的硬件特性进行模式切换。32 位代码由 CPU 直接解析执行,速度接近原生 32 位系统,但因中间层的转换会有轻微性能损耗。

模式切换

WOW64的实现依赖于X64处理器的硬件兼容性。

  • 指令集兼容:AMD64/Intel 64 架构是对X86的增量更新,原生支持执行X86代码。
  • 模式区分 (CS.L 字段):
    CPU通过代码段描述符中的CS.L字段来决定当前的工作模式。
    CS.L = 0 (且在 IA-32e 模式下):CPU运行在兼容模式,执行32位代码。
    CS.L = 1 (且在 IA-32e 模式下):CPU运行在X64 模式,执行64位代码。
  • 切换机制:WOW64利用这一特性,通过修改段选择子(Segment Selector)在Ring3层级即可完成32位与64位模式的切换。

WOW64 核心架构 (三个关键 DLL)

WOW64在用户模式下实现,作为32位ntdll.dll和64位内核之间的转换层,主要由以下三个DLL组成:

模块名称描述
Wow64.dll核心转换模块。实现了Ntoskrnl.exe的入口桩函数,负责在32位和64位调用之间进行转换(如指针和调用栈的操控),管理文件系统和注册表重定向。
Wow64cpu.dllCPU模式切换模块。负责管理处理器在32位和64位模式之间的上下文切换。
Wow64win.dllGUI支持模块。为32位GUI应用程序提供Win32k.sys的入口桩函数。

WOW64 进程空间布局与初始化

一个WOW64进程在操作系统眼里本质上是一个64位进程,但它构建了一个 32 位的虚拟空间

内存布局

  • 双环境共存:进程空间中同时包含64位环境(64位ntdll.dll)和32位环境(32位ntdll.dll及应用模块)
  • 地址映射:32位系统模块被映射到低地址空间(0x80000000以下)

进程初始化流程

  1. 启动:进程启动时,64位ntdll!LdrpInitializeProcess检查可执行文件头。
  2. 加载:如果发现是32位程序,则加载Wow64.dll等中间层模块。
  3. 环境构建 (Wow64!ProcessInit):
  • 栈设置:设置进程的提交栈大小。
  • 重定向:调用Wow64pInitializeFilePathRedirection设置文件路径重定向。
  • 共享信息 (Wow64SharedInformation):初始化一个函数指针表,将32位ntdll的关键函数(如 LdrInitializeThunk, KiUserExceptionDispatcher)映射到WOW64层,用于异常分发和APC处理。
  • 模式切换代码:调用CpuProcessInit修改X86向X64切换的代码逻辑。

TEB与PEB的特殊结构

在调试WOW64进程时,可以看到特殊的结构体关系:

  • 64 位TEB (线程环境块):包含一个指向32位TEB的指针 (ExceptionList字段通常被复用指向X86 TEB)。
  • TLS 扩展:64位TEB的TLS槽位中存储了WOW64_TLS_WOW64INFO,其中包含Wow64Info结构体,记录了模拟器的页面大小、CPU标志位和切换代码地址。

机制总结

  • 系统调用 (System Call):32位程序的系统调用请求被拦截,通过Wow64cpu.dll切换到64位模式,经由Wow64.dll转换参数格式后,最终通过64 位ntdll.dll进入内核。
  • 文件与注册表重定向:为了防止32位程序破坏64位系统配置,WOW64自动将特定路径(如System32)和注册表键值重定向到32位专用的存储区域。
    文章

32位进程的API调用过程(WoW64)

以ZwOpenProcess函数的调用为例

  1. a.exe首先调用32位ntdll.dll(以下简称ntdll32)中的ZwOpenProcess函数
  2. ntdll32调用wow64cpu.dll中的X86SwitchTo64BitMode,顾名思义,调用该函数后进程从32位模式切换到64位模式
  3. 由wow64.dll将32位的系统调用转化为64位
  4. 调用64位ntdll.dll的ZwOpenProcess函数
  5. 切换到内核态(Ring0)执行系统调用
    ![[Pasted image 20260122035511.png]]

32位进程的API调用过程(天堂之门)

大致流程

  1. 将cs段寄存器设为0x33,切换到64位模式
  2. 从gs:0x60读取64位PEB
  3. 从64位PEB中定位64位ntdll基址
  4. 遍历ntdll64导出表,读取ZwOpenProcess函数地址
  5. 构造64位函数调用

如果需要调用的是ntdll之外的函数,以kernel32.dll中的CreateFile函数为例,还需要

  1. 遍历ntdll64导出表,读取LdrLoadDll函数地址
  2. 调用LdrLoadDll(“kernel32.dll”)加载64位kernel32.dll
  3. 从64位的kernel32中读取GetProcAddress等函数,获取CreateFile函数地址
  4. 调用CreateFile函数
    ![[Pasted image 20260122035717.png]]
    从上述过程中我们可以发现通过天堂之门的API调用并没有调用ntdll32中的函数,而目前大多数沙箱在检测32位程序时仅仅会对32位函数进行Hook,通过天堂之门,我们成功绕过了沙箱的API检测
    ![[Pasted image 20260122035734.png]]

代码实现

仓库
实现天堂之门大概用到的函数

  1. memcpy64:在64位地址之间拷贝数据
  2. GetPEB64:获取64位的PEB地址
  3. GetModuleHandle64:获取64位的模块基址
  4. GetProcAddress64:获取64位模块中的函数地址
  5. X64Call:调用64位函数
  6. MakeUTFStr:构造UNICODE_STRING结构体
  7. GetKernel32:加载64位kernel32.dll及其依赖kernelbase.dll
  8. LoadLibrary64:在加载kernel32.dll后用于加载user32.dll

环境准备

  • Visual Studio 2019
  • WinDbg(x64)
  • Windows 10 x64 20H2
    VS项目属性中选择Release, Win32,关闭优化(不然可能出现指令重排,寄存器分配混乱,栈帧指针省略等情况)
    ![[Pasted image 20260121230226.png]]
    C/C++->代码生成->运行库改为“多线程(/MT)”,即静态编译
    ![[Pasted image 20260121230241.png]]

memcpy64

函数声明

void memcpy64(uint64_t dst, uint64_t src, uint64_t sz);

将64位src内容拷贝到dst,拷贝sz个字节,因为操作的地址是64位的,必须切换到64位用64位汇编实现
由32位切换到64位的代码如下,[bits 32]表示接下来的汇编要以32位模式编译,next_x64_code为64位汇编代码的地址

[bits 32]
push 0x33
push _next_x64_code
retf

retf表示远返回,该指令会从栈顶取出一个返回地址,再取出一个cs段选择子,在上述代码中,retf指令会跳转到0x33:next_x64_code,并将cs段寄存器置为0x33,此时程序切换到64位模式(Windows下cs段寄存器为0x23则以32位模式执行指令,为0x33则以64位模式执行指令)
注:普通的ret是近返回,只修改eip寄存器,retf是远返回,会同时修改cs和eip/rip(指令指针寄存器)
执行64位汇编指令,将src的数据拷贝到dst中

[bits 64]
push rsi
push rdi
mov rsi, src
mov rdi, dst
mov rcx, sz
rep movsb
pop rsi
pop rdi

注:

  1. rep movsb是cpu原生的高速内存拷贝指令,rep,重复执行后面指令知道rcx寄存器值减到0,movsb,把把RSI指向的1字节数据拷贝到RDI指向的地址,然后RSI和RDI自动+1(或-1,由DF标志位决定)
  2. 保存rsl,rdi原因,64位Windows调用约定规定:RSI、RDI属于非易失性寄存器,函数调用后必须保持原值不变,所以要先压栈保存,拷贝完再恢复。

执行完64位代码后,我们需要切回32位模式并返回。retfq中的q表示qword,即返回到64位的地址
注:
retf:32位模式下的远返回,弹出4位地址+2位段选择子
retfq:64位模式下的远返回,弹出8位地址+2位段选择子(q代表64位的QWORD)

[bits 64]
push 0x23
push _next_x86_code
retfq

[bits 32]

ret

汇编的编译我们可以用Python的keystone模块实现

from keystone import *

code = '''
push 0x33
push 0x12345678
retf
'''

ks = Ks(KS_ARCH_X86, KS_MODE_32)
asm, cnt = ks.asm(code)
print(code)
for b in asm:
    print('0x' + hex(b)[2:].upper(), end=', ')

输出得到shellcode,其中0x12345678我们要替换成_next_x64_code,也就是下一段64位汇编指令的地址

push 0x33     
push 0x12345678
retf

0x6A, 0x33, 0x68, 0x78, 0x56, 0x34, 0x12, 0xCB,

完整的shellcode

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
        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
};

要执行这段shellcode,我们需要在堆中开辟新的空间,属性为PAGE_EXECUTE_READWRITE,即可读可写可执行,将shellcode拷贝到这块区域,替换_next_x64_code、src、dst、next_x86_code的地址后执行
注:动态执行部分,Windows默认会开启DEP(数据执行保护) ,普通的堆内存或栈内存是不可执行的,直接运行会触发异常。必须用VirtualAlloc申请带有PAGE_EXECUTE_READWRITE属性的内存。
ptr + 8:64位代码段的起始地址(也就是Shellcode中第8个字节的位置)
ptr + 53:32位返回地址的起始地址(也就是Shellcode中第53个字节的位置)
src、dst、sz:调用memcpy64时传入的参数
用函数指针强制执行,把申请到的内存地址强制转换成void(*)()(无参数无返回值的函数指针),然后直接调用,就相当于把这块内存当成函数来执行

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)();

完整代码

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
            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)();
}

GetPEB64

函数声明

void GetPEB64(void* peb64);

在Windows x64架构下,FS段寄存器指向32位的TEB(线程环境块),而GS段寄存器指向64位的TEB。gs:[0x30]指向64位的TEB,gs:[0x60]指向64位的PEB(进程环境块)。要获取64位环境下的系统信息,必须通过切换CPU模式或直接访问GS段寄存器来获取PEB64的地址。
注:PEB:Windows操作系统中用于存储进程全局信息的核心数据结构,包含了加载的模块列表(Ldr)、进程堆、标志位等关键信息
这个函数的核心在于利用汇编代码在32位程序中执行64位指令。

  1. 模式切换:通过retf指令(远返回),将代码段选择子(CS)从0x23(32位兼容模式)切换到0x33(64位长模式)。
  2. 获取数据:进入64位模式后,执行mov rax, gs:[0x60]直接读取PEB64地址。
  3. 还原模式:读取完毕后,再次通过retfq切换回32位模式,并将结果保存到传入的指针中。
  4. 动态执行:代码通过VirtualAlloc分配可执行内存并动态写入机器码来运行。
    代码实现
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] # 段寄存器:[偏移量]
           mov [esi], rax   # 将RAX中的值(即刚才获取到的PEB64地址)写入到ESI指向的内存地址中
        */
        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 = 0;
    if (!ptr) {
        ptr = (uint32_t)VirtualAlloc(NULL, sizeof(code), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
        for (int i = 0; i < sizeof(code); i++) ((uint8_t*)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)();
}

注:为什么用ESI而不是RSI
虽然处于64位,esi寄存器的高32位通常清零,传入的指针本身是32位程序空间内的地址,所以使用32位的esi作为基址寄存器也可以正确指向目标变量内存位置

GetModuleHandle64

函数声明

uint64_t GetModuleHandle64(const WCHAR *moduleName);

获取名为moduleName的模块的基址
Windows的加载器数据存储在PEB的Ldr字段中(偏移0x18)。_PEB_LDR_DATA结构体包含三个双向链表,其中InLoadOrderModuleList(偏移0x10)按照加载顺序记录了所有模块。每个链表节点实际上是_LDR_DATA_TABLE_ENTRY结构,其中包含了模块基址(DllBase)和模块名称(BaseDllName)。

由于我们在32位程序中无法直接定义或使用64位的指针和结构体,该函数通过手动计算偏移量和memcpy64(自定义的内存拷贝函数,需自行实现或使用系统函数)来遍历链表。

  1. 获取Ldr:从PEB64 + 0x18读取Ldr地址。
  2. 获取链表头:从Ldr + 0x10读取加载顺序链表头。
  3. 遍历链表:循环读取每个节点的BaseDllName(偏移96字节处是Buffer指针)。遍历InLoadOrderModuleList获取模块基址
    注:之所以能直接把InLoadOrderModuleList的节点当作结构体基址去加偏移访问BaseDllName,InLoadOrderLinks位于LDR_DATA_TABLE_ENTRY结构体的最开头(偏移为 0)
  4. 比对名称:将读取到的DLL名称与目标名称比对,若匹配则读取偏移48字节处的DllBase并返回。通过模块基址获取模块名,并与moduleName比对,比对成功则返回该模块基址
    ![[Pasted image 20260122021800.png]]
    在WinDbg中使用dt指令(Display Type,显示类型)查看结构体,可以看到Ldr的地址在PEB中的偏移为0x018
0:000> dt _PEB
ntdll!_PEB
   +0x000 InheritedAddressSpace : UChar
   +0x001 ReadImageFileExecOptions : UChar
   +0x002 BeingDebugged    : UChar
   +0x003 BitField         : UChar
   +0x003 ImageUsesLargePages : Pos 0, 1 Bit
   +0x003 IsProtectedProcess : Pos 1, 1 Bit
   +0x003 IsImageDynamicallyRelocated : Pos 2, 1 Bit
   +0x003 SkipPatchingUser32Forwarders : Pos 3, 1 Bit
   +0x003 IsPackagedProcess : Pos 4, 1 Bit
   +0x003 IsAppContainer   : Pos 5, 1 Bit
   +0x003 IsProtectedProcessLight : Pos 6, 1 Bit
   +0x003 IsLongPathAwareProcess : Pos 7, 1 Bit
   +0x004 Padding0         : [4] UChar
   +0x008 Mutant           : Ptr64 Void
   +0x010 ImageBaseAddress : Ptr64 Void
   +0x018 Ldr              : Ptr64 _PEB_LDR_DATA

用之前实现的memcpy64函数拷贝Ldr的地址

uint64_t peb64;
uint64_t ldrData;

GetPEB64(&peb64);
memcpy64((uint64_t)&ldrData, peb64 + 0x18, 8);

打印_PEB_LDR_DATA结构体,可以看到InLoadOrderModuleList在Ldr中的偏移为0x10

0:000> dt _PEB_LDR_DATA
ntdll!_PEB_LDR_DATA
   +0x000 Length           : Uint4B
   +0x004 Initialized      : UChar
   +0x008 SsHandle         : Ptr64 Void
   +0x010 InLoadOrderModuleList : _LIST_ENTRY
   +0x020 InMemoryOrderModuleList : _LIST_ENTRY
   +0x030 InInitializationOrderModuleList : _LIST_ENTRY
   +0x040 EntryInProgress  : Ptr64 Void
   +0x048 ShutdownInProgress : UChar
   +0x050 ShutdownThreadId : Ptr64 Void

拷贝InLoadOrderModuleList的地址

uint64_t head;
uint64_t pNode;

head = ldrData + 0x10;
memcpy64((uint64_t)&pNode, head, 8);

InLoadOrderModuleList的实际类型为_LDR_DATA_TABLE_ENTRY,BaseDllName中存储了DLL的名称,类型为_UNICODE_STRING

0:000> dt _LDR_DATA_TABLE_ENTRY
ntdll!_LDR_DATA_TABLE_ENTRY
   +0x000 InLoadOrderLinks : _LIST_ENTRY
   +0x010 InMemoryOrderLinks : _LIST_ENTRY
   +0x020 InInitializationOrderLinks : _LIST_ENTRY
   +0x030 DllBase          : Ptr64 Void
   +0x038 EntryPoint       : Ptr64 Void
   +0x040 SizeOfImage      : Uint4B
   +0x048 FullDllName      : _UNICODE_STRING
   +0x058 BaseDllName      : _UNICODE_STRING

0:000> dt _UNICODE_STRING
ntdll!_UNICODE_STRING
   +0x000 Length           : Uint2B
   +0x002 MaximumLength    : Uint2B
   +0x008 Buffer           : Ptr64 Wchar

所以Buffer的偏移量在_LDR_DATA_TABLE_ENTRY中的偏移为0x58+0x08,即96
遍历链表的代码如下

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
}

代码实现

uint64_t GetModuleHandle64(const WCHAR *moduleName) {
    uint64_t peb64;
    uint64_t ldrData;
    uint64_t head;
    uint64_t pNode;

    GetPEB64(&peb64);
    memcpy64((uint64_t)&ldrData, peb64 + 0x18, 8); // 获取Ldr
    head = ldrData + 0x10;                        // InLoadOrderModuleList
    memcpy64((uint64_t)&pNode, head, 8);

    while (pNode != head) {
        uint64_t buffer;
        memcpy64((uint64_t)(unsigned)(&buffer), pNode + 96, 8); // 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); // DllBase
                return base;
            }
        }
        memcpy64((uint64_t)&pNode, pNode, 8); // Flink (next node)
    }
    return 0;
}

注:
在64位Windows下,指针是8字节,LIST_ENTRY 是16字节(前向指针8+后向指针8)
_LDR_DATA_TABLE_ENTRY 的布局如下(括号内为十进制偏移):

偏移 (Hex)偏移 (Dec)成员类型成员名称大小 (字节)说明
0x000LIST_ENTRYInLoadOrderLinks16加载顺序链表节点
0x1016LIST_ENTRYInMemoryOrderLinks16内存顺序链表节点
0x2032LIST_ENTRYInInitializationOrderLinks16初始化顺序链表节点
0x3048void*DllBase8模块基址
0x3856void*EntryPoint8入口点
0x4064ULONGSizeOfImage8包含4字节对齐填充
0x4872UNICODE_STRINGFullDllName16完整路径名
0x5888UNICODE_STRINGBaseDllName16模块文件名 (结构体起始)

MyGetProcAddress

函数声明

uint64_t MyGetProcAddress(uint64_t hModule, const char* func);

通过GetModuleHandle64获取模块地址后,此时还无法通过kernel32.dll中的GetProcAddress函数获取模块中函数的地址,可以通过遍历模块的导出表获取函数地址作为过渡方案
![[Pasted image 20260122023147.png]]

PE文件格式(Portable Executable)中,导出表(Export Directory)记录了模块导出的函数名称和地址。通过解析DOS头找到NT头,再通过数据目录表(DataDirectory)的第0项找到导出表。导出表包含三个重要的数组:函数地址表、函数名称表和名称序号表。
在尚未获取系统GetProcAddress函数地址之前,我们需要手动解析PE结构来查找函数。

  1. 定位导出表:读取IMAGE_DOS_HEADER和IMAGE_NT_HEADERS64,定位到IMAGE_EXPORT_DIRECTORY。
  2. 遍历名称表:遍历AddressOfNames指向的数组,获取每个导出函数的名称。
  3. 匹配与获取地址:如果名称匹配,通过索引在AddressOfNameOrdinals获取序号,再用序号在AddressOfFunctions中获取函数的RVA(相对虚拟地址)。
  4. 计算绝对地址:返回hModule + RVA。
    获取导出表地址
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));

遍历导出表,从导出表中读取函数的名称和地址,将函数名称与func进行比对,比对成功则返回函数地址

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;
    }
}

代码实现

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 0;
}

X64Call

函数声明

uint64_t X64Call(uint64_t proc, uint32_t argc, ...);

由于32位与64位的函数调用的传参方式不同,以及在上一步中我们通过MyGetProcAddress函数获取的函数地址为64位,肯定不能直接转化为函数指针调用,所以我们需要用64位汇编实现一个64位函数的调用

x64 Windows调用约定(Microsoft x64 Calling Convention)与x86有显著不同:
参数传递:前4个整数/指针参数依次存入RCX, RDX, R8, R9寄存器。浮点数存入XMM0 – XMM3。
栈空间:剩余参数从右向左入栈。
Shadow Space:调用者必须在栈上为被调函数预留32字节(0x20)的空间(即Shadow Store),紧挨着返回地址。
栈对齐:在CALL指令执行前,RSP必须是16字节对齐的。
这是实现Heaven’s Gate的核心函数,用于在32位程序中发起64位函数调用。

  1. 保存环境:将esp保存到ebx中
  2. 栈对齐:使用and esp, 0xFFFFFFF8确保栈对齐。
  3. 模式切换:利用retf跳入64位代码段。
  4. 参数设置:在64位模式下,将参数从栈中取出,分别放入RCX, RDX, R8, R9。如果参数超过4个,将剩余参数压入64位栈中。(64位WINAPI调用协定传参)
  5. 预留空间:sub rsp, 32为被调函数预留Shadow Space。
  6. 执行调用:call rax执行目标64位函数。函数返回值保存到rax
# 在shellcode执行前后保存和复原rsi和rdi

[bits 64]

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

  1. 恢复环境:保存返回值,恢复栈指针,通过retfq切回32位模式,最后恢复32位的栈帧并返回。在shellcode执行前后保存和复原rsi和rdi,最后切换回32位模式,并还原esp和ebx
# 还原esp和ebx

[bits 64]

push 0x23 push _next_x86_code retfq

[bits 32]

mov esp, ebx pop ebx ret

代码实现

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] 保存栈帧,对齐栈,切换到x64
        0x53, 0x89, 0xE3, 0x83, 0xE4, 0xF8,
        0x6A, 0x33, 0x68, 0x78, 0x56, 0x34, 0x12, 0xCB,

        // [bits 64] 保存寄存器,设置参数(RCX, RDX, R8, R9)
        0x56, 0x57,
        0x48, 0xBE, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, // mov rsi, args_placeholder
        0x48, 0x8B, 0xE, 0x48, 0x8B, 0x56, 0x8, 0x4C, 0x8B, 0x46, 0x10, 0x4C, 0x8B, 0x4E, 0x18,

        // 处理超过4个的参数
        0x48, 0xB8, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, // mov rax, argc_placeholder
        0x48, 0x83, 0xF8, 0x4, 0x7E, 0xB, 
        0x48, 0x8B, 0x7C, 0xC6, 0xF8, 0x57, 0x48, 0xFF, 0xC8, 0xEB, 0xEF,

        // 预留Shadow Space并调用
        0x48, 0xB8, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, // mov rax, proc_placeholder
        0x48, 0x83, 0xEC, 0x20, 0xFF, 0xD0,

        // 保存返回值
        0x48, 0xBF, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, // mov rdi, ret_ptr_placeholder
        0x48, 0x89, 0x7,
        0x5F, 0x5E,

        // [bits 64] 切换回x86
        0x6A, 0x23, 0x68, 0x78, 0x56, 0x34, 0x12, 0x48, 0xCB,

        // [bits 32] 恢复栈帧并返回
        0x89, 0xDC, 0x5B,
        0xC3
    };

    static uint32_t ptr = 0;
    if (!ptr) {
        ptr = (uint32_t)VirtualAlloc(NULL, sizeof(code), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
        for (int i = 0; i < sizeof(code); i++) ((uint8_t*)ptr)[i] = code[i];
    }
    // 动态修正shellcode中的占位符地址
    *(uint32_t*)(ptr + 9) = ptr + 14;
    *(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;

    ((void(*)())ptr)();
    return ret;
}

MakeUTFStr

函数声明

char* MakeUTFStr(const char* str);

构造一个_UNICODE_STRING结构体并返回64位的地址

Windows内核及Native API(如ntdll中的函数)通常使用UNICODE_STRING结构体来表示字符串,而不是简单的以null结尾的字符数组。该结构体包含Length(字节长度)、MaximumLength(缓冲区大小)和Buffer(指向宽字符串的指针)。
为了调用LdrLoadDll等函数,我们需要将普通的ANSI字符串转换为UNICODE_STRING结构。此函数分配内存,将ANSI字符串转为宽字符,并填充结构体,返回指向该结构的指针。
代码实现

char* MakeUTFStr(const char* str) {
    uint32_t len = lstrlenA(str);
    char* out = (char*)malloc(16 + (len + 1) * 2);
    *(uint16_t*)(out) = (uint16_t)(len * 2);           // Length
    *(uint16_t*)(out + 2) = (uint16_t)((len + 1) * 2); // MaximumLength

    uint16_t* outstr = (uint16_t*)(out + 16);
    for (uint32_t i = 0; i <= len; i++) outstr[i] = str[i];
    *(uint64_t*)(out + 8) = (uint64_t)(out + 16);      // Buffer指针
    return out;
}

GetKernel32

函数声明

uint64_t GetKernel32();

加载kernel32.dll以及kernelbase.dll

kernel32.dll是Windows子系统的核心DLL。在某些环境下,它可能尚未加载到64位地址空间中,或者我们需要确保获取的是正确的64位版本句柄。LdrLoadDll是ntdll.dll导出的底层函数,用于加载DLL。
在WOW64环境下,32位进程默认只加载了32位的kernel32。为了使用64位API,我们需要:

  1. 找到64位的ntdll.dll(它总是被映射的)。
  2. 在ntdll中找到LdrLoadDll函数。
  3. 构造参数调用LdrLoadDll来显式加载64位的kernel32.dll。
    代码实现
uint64_t GetKernel32() {
    static uint64_t kernel32 = 0;
    if (kernel32) return kernel32;

    uint64_t ntdll = GetModuleHandle64(L"ntdll.dll");
    uint64_t LdrLoadDll = MyGetProcAddress(ntdll, "LdrLoadDll");
    char* str = MakeUTFStr("kernel32.dll");
    // 调用LdrLoadDll(NULL, 0, &ModuleName, &Handle)
    X64Call(LdrLoadDll, 4, (uint64_t)0, (uint64_t)0, (uint64_t)str, (uint64_t)(&kernel32));
    return kernel32;
}

补:0xC0000018的冲突:在WOW64环境下,32位程序运行在64位系统的子系统中。32位程序只能访问低4GB的内存。但是,为了维持WOW64的运行,系统必须在进程空间的高位地址(通常是4GB以上)加载64位的ntdll.dll、wow64.dll、wow64cpu.dll等,默认情况下,WOW64进程不会加载64位的kernel32.dll。它只加载32位的kernel32.dll供应用程序使用,在旧版Windows中,64位kernel32.dll有一个固定的首选基址(Preferred Base Address),当你手动调用LdrLoadDll试图加载它时,加载器会尝试把它放在那个首选地址。但在某些window版本中,WOW64子系统(或者32位的堆/栈映射)可能刚好占用了那个位置,Windows的加载器在某些严格模式下,如果无法满足首选地址且没有开启重定位(Relocation)或者重定位失败,就会报0xC0000018(地址冲突)。
但新的ASLR(地址空间布局随机化),让系统dll的加载地址不再那么固定,wow64也在win10有大量重构(引入wow64log.dll等机制),ldr更加智能

GetProcAddress64

函数声明

uint64_t GetProcAddress64(uint64_t hModule, const char* func);

这是标准的Win32 API GetProcAddress的64位版本。一旦我们有了64位kernel32.dll的句柄,就可以获取其中导出的GetProcAddress函数地址,进而使用它来获取任何其他64位DLL中的函数地址。
这是一个封装函数。它首先通过GetKernel32和MyGetProcAddress找到真正的系统级GetProcAddress,然后通过X64Call调用它。这比我们自己实现的MyGetProcAddress更稳定、兼容性更好(例如处理Forwarded Exports)。
代码实现

uint64_t GetProcAddress64(uint64_t module, const char* func) {
    static uint64_t K32GetProcAddress = 0;
    if (!K32GetProcAddress)
        K32GetProcAddress = MyGetProcAddress(GetKernel32(), "GetProcAddress");

    return X64Call(K32GetProcAddress, 2, module, (uint64_t)func);
}

LoadLibrary64

函数声明

uint64_t LoadLibrary64(const char* moduleName);

LoadLibraryA是用于将指定模块加载到调用进程的地址空间的标准API。在64位上下文中,它用于加载64位DLL。
这是对GetProcAddress64的补充。有了它,我们就可以随意加载任何64位DLL并调用其中的函数了。实现逻辑很简单:从kernel32中获取LoadLibraryA的地址,然后通过X64Call调用它。
代码实现

uint64_t LoadLibrary64(const char* moduleName) {
    static uint64_t K32LoadLibraryA = 0;
    if (!K32LoadLibraryA)
        K32LoadLibraryA = MyGetProcAddress(GetKernel32(), "LoadLibraryA");

    // LoadLibraryA接受一个指向ANSI字符串的指针
    return X64Call(K32LoadLibraryA, 1, (uint64_t)moduleName);
}

MessageBox测试

void Test() {
    uint64_t kernel32 = GetKernel32();
    uint64_t user32 = LoadLibrary64("user32.dll");
    uint64_t MessageBox64 = GetProcAddress64(user32, "MessageBoxA");
    X64Call(MessageBox64, 4, (uint64_t)NULL, (uint64_t)"Wowowowowow", (uint64_t)"Wowowowowow", (uint64_t)NULL);
}

int main() {
    Test();
}

实现

有些部分有只能运行exe,在DLL中无法加载user32.dll的问题,不能直接运行那里的问题改了半天没成功,另一个有环境冲突问题,加载了Kernel32.dll,调用GetTickCount64,没用MessageBox了,这里有点不太理解

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <winternl.h>

void memcpy64(uint64_t dst, uint64_t src, uint64_t sz) {
    static uint8_t code[] = {
        0x6A, 0x33,
        0x68, 0x78, 0x56, 0x34, 0x12,
        0xCB,
        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,
        0x6A, 0x23,
        0x68, 0x78, 0x56, 0x34, 0x12,
        0x48, 0xCB,
        0xC3
    };

    static uint32_t ptr = 0;
    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)();
}

void GetPEB64(void *peb64) {
    static uint8_t code[] = {
        0xBE, 0x78, 0x56, 0x34, 0x12,
        0x6A, 0x33,
        0x68, 0x78, 0x56, 0x34, 0x12,
        0xCB,
        0x65, 0x48, 0xA1, 0x60, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
        0x67, 0x48, 0x89, 0x6,
        0x6A, 0x23,
        0x68, 0x78, 0x56, 0x34, 0x12,
        0x48, 0xCB,
        0xC3
    };

    static uint32_t ptr = 0;
    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)();
}

uint64_t GetModuleHandle64(const WCHAR *moduleName) {
    uint64_t peb64 = 0;
    uint64_t ldrData = 0;
    uint64_t head = 0;
    uint64_t pNode = 0;

    GetPEB64(&peb64);
    if(peb64 == 0) return 0;

    memcpy64((uint64_t)&ldrData, peb64 + 0x18, 8);
    head = ldrData + 0x10;
    memcpy64((uint64_t)&pNode, head, 8);

    int safe = 0;
    while (pNode != head && pNode != 0 && safe < 200) {
        safe++;
        uint64_t buffer = 0;
        memcpy64((uint64_t)(unsigned)(&buffer), pNode + 0x60, 8); 

        if (buffer) {
            WCHAR curModuleName[256] = {0};
            memcpy64((uint64_t)curModuleName, buffer, 255);

            if (lstrcmpiW(moduleName, curModuleName) == 0) {
                uint64_t base = 0;
                memcpy64((uint64_t)&base, pNode + 0x30, 8);
                return base;
            }
        }
        memcpy64((uint64_t)&pNode, pNode, 8); 
    }
    return 0;
}

uint64_t MyGetProcAddress(uint64_t hModule, const char* func) {
    if(!hModule) return 0;

    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));

    uint32_t exportRVA = nt.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    if(exportRVA == 0) return 0;

    IMAGE_EXPORT_DIRECTORY expo;
    memcpy64((uint64_t)&expo, hModule + exportRVA, sizeof(expo));

    for (uint64_t i = 0; i < expo.NumberOfNames; i++) {
        DWORD pName = 0;
        memcpy64((uint64_t)&pName, hModule + expo.AddressOfNames + (4 * i), 4);

        char name[64] = {0};
        memcpy64((uint64_t)name, hModule + pName, 63);

        if (lstrcmpA(name, func) == 0) {
            WORD ord = 0;
            memcpy64((uint64_t)&ord, hModule + expo.AddressOfNameOrdinals + (2 * i), 2);
            uint32_t addr = 0;
            memcpy64((uint64_t)&addr, hModule + expo.AddressOfFunctions + (4 * ord), 4);
            return hModule + addr;
        }
    }
    return 0;
}

uint64_t X64Call(uint64_t proc, uint32_t argc, ...) {
    uint64_t* args = (uint64_t*)(&argc + 1);
    uint64_t ret = 0;

    static uint8_t code[] = {
        0x53, 0x89, 0xE3, 0x83, 0xE4, 0xF8,
        0x6A, 0x33, 0x68, 0x78, 0x56, 0x34, 0x12, 0xCB,

        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,

        0x6A, 0x23, 0x68, 0x78, 0x56, 0x34, 0x12, 0x48, 0xCB,

        0x89, 0xDC, 0x5B,
        0xC3
    };

    static uint32_t ptr = 0;
    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 + 9) = ptr + 14;
    *(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;

    ((void(*)())ptr)();
    return ret;
}

char* MakeUTFStr(const char* str) {
    uint32_t len = lstrlenA(str);
    char* out = (char*)VirtualAlloc(NULL, 16 + (len + 1) * 2, MEM_COMMIT, PAGE_READWRITE);
    *(uint16_t*)(out) = (uint16_t)(len * 2);
    *(uint16_t*)(out + 2) = (uint16_t)((len + 1) * 2);

    uint16_t* outstr = (uint16_t*)(out + 16);
    for (uint32_t i = 0; i <= len; i++) outstr[i] = str[i];

    *(uint64_t*)(out + 8) = (uint64_t)(out + 16);
    return out;
}

uint64_t GetKernel32() {
    static uint64_t kernel32 = 0;
    if (kernel32) return kernel32;

    uint64_t ntdll = GetModuleHandle64(L"ntdll.dll");
    printf("ntdll base: 0x%llX\n", ntdll);

    if(!ntdll) return 0;

    uint64_t LdrLoadDll = MyGetProcAddress(ntdll, "LdrLoadDll");
    printf("LdrLoadDll: 0x%llX\n", LdrLoadDll);

    if(!LdrLoadDll) return 0;

    char* str = MakeUTFStr("kernel32.dll");

    uint64_t status = X64Call(LdrLoadDll, 4, (uint64_t)0, (uint64_t)0, (uint64_t)str, (uint64_t)(&kernel32));

    printf("LdrLoadDll Status: 0x%llX, Handle: 0x%llX\n", status, kernel32);
    return kernel32;
}

uint64_t GetProcAddress64(uint64_t module, const char* func) {
    static uint64_t K32GetProcAddress = 0;
    if (!K32GetProcAddress) {
        K32GetProcAddress = MyGetProcAddress(GetKernel32(), "GetProcAddress");
    }
    return X64Call(K32GetProcAddress, 2, module, (uint64_t)func);
}

void Test() {
    printf("Start Test\n");

    uint64_t kernel32 = GetKernel32();
    if(!kernel32) {
        printf("Failed to load Kernel32\n");
        return;
    }

    printf("Kernel32 Loaded\n");

    uint64_t pGetTickCount64 = GetProcAddress64(kernel32, "GetTickCount64");

    if(!pGetTickCount64) {
        printf("Failed to find GetTickCount64\n");
        return;
    }

    printf("Found GetTickCount64 at 0x%llX. Calling it...\n", pGetTickCount64);

    uint64_t tick = X64Call(pGetTickCount64, 0);

    printf("\n[SUCCESS] 64-bit Tick Count: %llu\n", tick);
    printf("[SUCCESS]\n");
}

int main() {
    Test();
    getchar();
    return 0;
}
文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇