随着技术的发展,经常会遇到需要在64位系统中运行32位代码的情况。本文将介绍一种可能的解决方案,虽然目前这个解决方案并不完美,但它为未来提供了一种思路。
故事始于在Windows 11上调试一个64位应用程序时,无意中打开了Visual Studio调试器中的“寄存器”窗口。发现CS寄存器的值为0x33,这在Intel汇编手册中表示长模式GDT的选择器。然而,在文章中也提到了x86-64 CPU具有“兼容模式”,这是一种无需模拟即可运行32位程序的方式。当调试32位版本的应用程序时,CS的值为0x23,显然是一个32位的平坦段。
这让想到了一个点子:既然已经有一个可用的32位平坦代码段,为什么应用程序不能在64位和32位模式之间切换呢?经过一番努力,成功实现了这个想法。
在本文中,将0x33(长模式)和0x23(兼容模式)的选择器硬编码到了source.asm中。稍后,将构建一个内核模式驱动程序,它可以检查GDT并确定应该使用的选择器值。要找出Windows中选择器的值,只需启动一个x64和x86应用程序并检查CS的值即可。
为了切换到兼容模式,需要使用带有0x23选择器的RETF技巧:
push 0x23
xor rcx, rcx
mov ecx, Back32
push rcx
retf
'Back32'是具有32位入口点的地址。无论这个入口点能做什么,它最终都需要通过长跳转回到64位段:
Back32:
USE32
; 执行32位操作
jump back to x64
USE64
db 0eah
ret_64:
dd 0
dw 0x33
nop
'ret_64'将包含一个地址,该地址在64位模式下通过RET操作码将控制权返回给调用者。
不能在Visual Studio中调试切换过程,但可以使用WinDbg。一旦执行了retf指令,WinDbg将切换到“x86”模式,寄存器窗口将显示EAX、EBX等,而不是RAX、RBX,32位代码将成为可能。
任务管理器也会有点困惑。它在某个时刻会显示应用程序的两个条目。很高兴让它感到困惑。
这还没有完全起作用,但正在努力。显然,不能使用LoadLibrary()。但是有一个名为MemoryModule的很好的从内存加载库,它将加载位于内存中的DLL文件并初始化它。
即使如此,也不能直接使用这个库,因为它在为x64编译时会使用64位结构,而需要它在为x64编译时使用32位结构。因此,修改了它,以便它将32位DLL加载到内存中。
手动加载DLL也意味着需要修补导入地址表。因为宿主是64位的,不能简单地通过LoadLibrary()和GetProcAddress()获取值,但必须创建一个32位助手Get32Imports,它会读取包含所需导入的XML文件并返回它们在内存中的指针。
然后,会使用PatchIAT函数将(32位!)指针写入加载的内存-DLL。不幸的是,它仍然无法调用任何导入的API函数。当尝试调用一个API,如MessageBeep时,它会抛出一个无效的执行异常。也许可以帮忙。
但到目前为止,对此感到满意。
Driver: 一个不完整的项目,稍后将为获取正确的GDT值,而不是使用硬编码的选择器
Get32Imports: 读取包含所需导入的XML文件并找到它们的值
Executable64: 尝试运行32位代码的64位可执行文件
Library和FasmDLL: 两个32位DLL,由Executable加载