深入理解wow64 I
文章目录
揭开x86程序在x86-64架构下运行机制的神秘面纱。
注1:本系列文章完全按照笔者学习路线记录。
注2:本系列文章假定读者拥有和笔者相近或远超笔者的知识。
注3:本系列文章基于Intel架构。
0x0 起因
半年前做了NU1L CTF的一个逆向题目,大致的意思是32位程序里做了一个切换到64位的操作,使得大部分调试器都起不了作用(除了强大的WinDbg),当时因为水平不太行没有思路去深入了解题目背后的机制。近日做windows进程在用户态和内核态切换的研究时再次遇到了这个技术,经过手搓内核的洗礼(
还没写完),这一次分析起来虽饱含艰辛,却有星辰指路。
0x1 初见 retf
野生的汇编指令retf出现了!
平日里做逆向时,最常见的返回上一栈帧的指令是ret/retn,retf
使用频率很少,但是如果有跟踪系统函数相关的经历,就会经常看到这个汇编指令了。解释如下:
ret | retf |
---|---|
用栈中数据改IP内容,近转移。拆开表示为:pop ip | 用栈中数据同时改CS,IP,远转移。拆开表示为:pop ip,pop cs |
那么retf
除了修改ip之外,还修改了段寄存器cs,那么架构切换一定和cs寄存器有关系。经过查阅资料和实际调试,环境为Windows10 1903 x64;进程环境为32位时,cs寄存器的值为0x23;进程环境为64为时,cs寄存器的值为0x33。也就是说,当前32位进程想要切换到x64位环境时,需要利用retf
把修改为0x33。
0x2.1 线索1 Windows进程代码段映射规律
Windows x64在初始化进程的时候会把代码段映射到两个位置,便于切换。而实际在调试的时候笔者发现这两个位置其实是重合的,并没有所谓的在一份虚拟空间里找到两个相同的代码段。
0x2.2 线索2 段寄存器,全局描述符表,段描述符
说起段寄存器,有过一些汇编基础的人会想当然认为地址 = 段寄存器«4 + ip。但是进一步学习操作系统会发现,前者只适用于实模式下的寻址;开启保护模式后的操作系统,段寄存器的实际长度位96(32+32+16+16)位,前80位在processor内部不可见,只显示最后的16位。因此“16位的段寄存器”中的数据不再负责记录偏移用作寻址,而是作为选择子,充当GDT的index。(实际结构笔者并未在intel手册上查找到记载,在亲自逆向或查阅后会更新内容)
笔者查阅intel的手册后发现:在保护模式下段寄存器实际不止16位,还有一部分在processor内部,外部不可见,这部分不可见的部分有时会被作为段描述符的缓存。最后可见的16位作为Selector充当GDT的index。
注:保护模式下用户态每个进程的CS,DS等的BaseAddr都是0。
下图给出了16位Selector的结构细节以及作用:
这里需要解释一下,Windows只采用GDT而不采用LDT,GDT全局可见,因此TI始终为0。对于低2位而言,如果是CS,则为CPL。CPL,RPL,DPL的联系和区别
例如CS分别为0x23,0x33时,二进制来看就是:0b100011和0b110011。
介绍完低3位之后,就剩下Index没有说了,作用详见下图。
上图就是段描述符,它作为元素存在GDT中。每个段可以根据Selector在GDT中寻找并填充关于它的运行权限、状态等。
0x3 神秘的段寄存器值 0x23 0x33
经过0x2
的讲解,相信读者已经复习了一遍保护模式下段寄存器的结构和作用,Selector和GDT的关系。接下来就是探究为什么从32-bit切换到64-bit,CS的值要从0x23切换到0x33。将hex转换为bin或者dec更易理解。
arch | 32-bit | 64-bit |
---|---|---|
hex | 0x23 | 0x33 |
bin | 100011 | 110011 |
index | 100 | 110 |
当进程环境是32-bit时,Selector的值是4,也就是GDT的第五项;
当进程环境是64-bit时,Selector的值是6,也就是GDT的第七项。
0x4 追踪GDT,读取内存
那么接下来的工作就是找到GDT的地址,读取段描述符,对比CS值为0x23、0x33的差异。因为在R3调试无法看到gdtr寄存器,查看GDT内容时选择了双机内核调试的策略,可以让WinDbg调试虚拟机的内核,读到R3看不到的数据。
实验环境:宿主机 Windows10 1909 x64 , 虚拟机 Windows8.1 x64 ,调试工具 VirtualKD、Windbg。
1.使用简单Windows API VirtualAllocEx()切入x64
这个步骤我们需要的就是在x64环境下编译一个32位程序,32位程序中调用一个方便跟踪的函数,笔者选择了VirtualAllocEx(),代码如下:
|
|
在VS2019环境下编译,编译选项是x86 Release。接着用64位的WinDbg调试二进制文件。
跟进VirtualAllocEx函数
使用PCHunter查看进程加载的模块
负责x86切换到x64的dll组件:
Wow64.dll
,通往Windows NT内核的核心接口,它转换32位与64位调用,包括指针和调用栈操作。Wow64win.dll
,为32位应用程序提供适当的入口点。Wow64cpu.dll
,负责解决进程从32位切换到64位模式。
同时我们也注意到了进程模块还加载了位于system32和syswow64下的ntdll,众所周知ntdll是处理用户态和内核态的媒介。但上述dll的内部细节并不打算在本次文章详解,不妨留到后面。我们的任务仍然是探索CS改变背后的原理。
继续Step In,跟进ntdll里面。
在NtAllocateVirtualMemory里,我们终于看到进程环境切换的far jmp
语句:
这句话会把ip修改为0x774E6009,cs由0x23修改为0x33。接着看:
进程环境已经由32-bit切换为64-bit,连寄存器显示的名字都改变了。不妨我们就此打住,把进一步调试的内容留给下一篇文章。
2.查看GDT以及段描述符表
前文已经提到过:
- cs值作为Selector
- R3无法查看GDTR,只能将权限提升至R0
因此以下的WinDbg调试环境为:Windows 10 1909 x64为宿主机,WinDbg x64为宿主机上的调试器,调试虚拟机Win8.1 x64
WinDbg:
r gdtr //查看gdtr寄存器
dq gdtr+0x10*index //查看GDT附近内存
dg [Selector] //由WinDbg解析GDT表项
段描述符的长度大小为64位,换算成16进制也就是一个长度为16的数字,显然Selector 0x23和0x33 分别对应图中的00cffb00 0000ffff
和0020fb00 00000000
,由WinDbg解析之后的数据如下:
32-bit | 64-bit | |
---|---|---|
Flags(HEX) | 0xcfb | 0x2fb |
Flags(BIN) | 110011111011 | 001011111011 |
我们只需要注意前四位,他们的意义分别是:G D/B L AVL。下图来自Intel手册
根据Windows的规定,32-bit环境下G=1,D/B=1,L=0,AVL=0;64-bit环境下G=0,D/B=0,L=1,AVL=0。同时我们也注意到,CS的Base为0,这也验证了保护模式下各个段的布局。
看看DS段:
0x5 总结
通过一些调试手法我们终于窥探到wow64的一隅,但是想要深入理解wow64的运行机制,以及是否会有漏洞利用,还需进一步努力。