第37章 x64处理器
本文最后更新于:2022年5月27日 下午
第37章x64处理器
要在64位环境中进行代码逆向分析,需要具备x64CPU的基础知识,本章将与各位一起学习。
37.1 X64中新增或变更的项目
为了保持向下兼容,x64是在原有x86基础上扩展而来的。要在x64系统中进行代码逆向分析,必须先了解x64中新增或变更的内容。
我们平时很少接触IA-64,此处略去不谈。x64中新增的内容比我们想象的要多得多,这里只讲解与代码逆向分析有关的部分。更详细的内容请参考Intel用户手册以及MSDN等相关信息。
37.1.1 64位
64位系统中内存地址为64位(8个字节),使用64位大小的指针。所以含有绝对地址(VA)的指令大小比原来增加了4个字节。同样,寄存器的大小以及栈的基本单位也变为64位。
37.1.2内存
x64系统中进程虚拟内存的实际大小为16TB(Trea Byte: 10)(内核空间与用户空间各占
8TB)。与x86的4GB(GigaByte: 109)相比,大小增加了非常多。
用64位可以表示的数为2M=16EB(Exa Byte: 10),日常生活中不会看到这么大的数。所以64位的CPU理论上可以支持16EB大小的内存寻址(Memory Addressing),但是考虑到实际性能,x64与IA-64都不支持这么大的虚拟内存,因为它会导致巨大的系统开销耗费在内存管理上。
37.1.3通用寄存器
x64系统中,通用寄存器的大小扩展到64位(8个字节),数量也增加到18个(新增了R8~R15寄存器)。x64系统下的所有通用寄存器的名称均以字母“R”开头(x86以字母“E”开头),如 图37-1所示。
为了实现向下兼容,支持访问寄存器的8位、16位、32位(例:AL、AX、EAX)。
64位本地模式中不使用段寄存器:CS、DS、ES、SS、FS、GS,它们仅用于向下兼容32位程序。
37.1.4 CALL/JMP 指令
32位的x86系统中,CALL/JMP指令的使用形式为“地址指令CALL/JMP”。
1 |
|
FF15XXXXXXXX指令用于调用API,其中XXXXXXXX “绝对地址”指向IAT区域的某个位置。x64系统中仍使用相同指令,但解析方法不同。
1 |
|
首先指令地址由原来的4个字节变为8个字节,然后,x86中FF15指令后跟着4个字节的绝对地
址(VA)而是不想当然的8个字节。若x64中也采用与x86相同的方式,则FF15后面应该跟着8个字节的绝对地址(VA),这样指令的长度就增加了。为了防止增加指令长度,x64系统中指令后面仍然跟着4个字节大小的地址,只不过该地址被解析为“相对地址”(RVA)。所以上面的指令列表中,FF15后面的4个字节(3FFA)被识别为相对地址,并通过下面的方法将相对地址转换为绝对地址。
000000000401000+3FFA+6=00000001
00405000
□ 0000000`00401000: CALL 指令地址
□ 3FFA:相对(地址)
□ 6: CALL指令(FF15XXXXXXXX)长度
□ 0000000`00405000:变换后的绝对地址
由于000000000405000地址中存储着00000000
75CE1E12值,所以上面出现的第一个CALL
指令最后被解析为CALL 00000000`75CElE12。
关于指令解析方法请参考第49章“IA-32指令”。
37.1.5函数调用约定
x64系统中另一个重要的不同是函数调用约定。前面介绍过,32位系统中使用的函数调用约定包括cdecl、stdcall、fastcall等几种,但64位系统中它们统一为一种变形的fastcall。64位fastcall中最多可以把函数的4个参数存储到寄存器中传递。
参数 | 整数型 | 实数型 |
---|---|---|
1st | RCX | XMM0 |
2nd | RDX | XMM1 |
3rd | R8 | XMM2 |
4th | R9 | XMM3 |
各参数顺序由寄存器确定,比如第一个参数总是存储在RCX(实数时为XMM0)中。若函数的参数超过4个,则与栈并用。也就是说,从第五个参数开始会存入栈来传递。此外,函数返回 时传递参数过程中所用的栈由调用者清理。看上去x64系统下的fastcall就像是32位系统下函数调用约定cdecl与fastcall方式的混合(所以前面我们把它称为变形的fastcall)。使用这种新的fastcall可以大大加快函数调用的速度。还有一点比较有意思的是,函数的前4个参数明明使用寄存器传递,但是栈中仍然为这4个参数预留了空间(32个字节)(下面的栈部分会详细讲解)。
37.1.6 栈&栈帧
Windows 64位OS中使用栈与栈帧的方式也发生了变化。简言之,栈的大小比函数实际需要
的大小要大得多。调用子函数(Sub Function)时,不再使用PUSH命令来传递参数,而是通过MOV指令操作寄存器与预定的栈来传递。使用VC++创建的x64程序代码中几乎看不到PUSH/POP指令。并且创建栈帧时也不再使用RBP寄存器,而是直接使用RSP寄存器来实现。
使用Visual C++编译32位程序时,若开启了编译器的优化功能,则几乎看不到使用EBP寄存器的栈帧。
该方式的优点是,调用子函数时不需要改变栈指针(RSP),函数返回时也不需要清理栈指
针,这样能够大幅提升程序的运行速度。下面通过一个练习示例进一步了解32位与64位栈工作原理的不同。
37.2 练习:Stack32.exe & Stack64.exe
通过CreateFile() API简单了解一下栈在32位与64位环境下分别是如何工作的,并比较它们工
作方式的不同。
1 |
|
首先使用Visual Studio 2012具将代码37-1分别编译为32位程序(Stack32.exe)与64位程序(Stack64.exe)。
37.2.1 Stack32.exe
下面先调试32位Stack32.exe程序。使用OllyDbg工具打开Stack32.exe,转到main()函数处(401000),如图37-2所示。
首先分析Stack32.exe的main()函数特征。
特征一,不使用栈帧。由于代码比较简单,变量又少,开启编译器的优化选项后,栈帧会被省略。
特征二,调用子函数(CreateFileA、CloseHandle)时使用栈传递参数。
特征三,使用PUSH指令压入栈的函数参数不需要main()函数清理。在32位环境中采用stdcall方式调用Win32 API时,由被调用者(CreateFileA、CloseHandle)清理栈。在图37-2中跟踪代码到401017地址的CreateFileA()函数处,查看栈,如图37-3所示。
可以看到,函数的7个参数全部被压入栈。接着使用StepInto(F7)命令,进入CreateFileA() API,如图37-4所示。
从图37-4中可以看到,CreateFileA() API使用了栈帧。并在调用CreateFileW() API之前使用PUSH指令将接收的参数压入栈。这样栈中就有了2份相同的参数(它们的区别在于第一份的7个参数为ASCII字符串格式,第二份的7个参数为Unicode字符串形式),如图37-5所示。
图37-5描述的是调用CreateFileW() API时栈中的情形。从图中可以清楚看到,相同参数被重复存入栈。以上就是我们熟知的在32位环境中调用函数时栈的工作原理。
37.2.2 Stack64.exe
下面调试64位Stack64.exe程序。使用WinDbg(x64)分析Stack64.exe的反汇编代码,如图37-6所示。
正常运行/调试本示例文件需要Windows XP/Vista/7 64位环境支持
图37-6是在x64dbg 中调试Stack64.exe的画面。WinDbg下的64位程序的反汇编代码不带注释,且看上去比较复杂,简单整理如代码37-2所示。
1 |
|
Stack64.exe代码具有如下几个特征。
特征一,使用“变形”的栈帧。在代码起始部分分配48h(72d)字节大小的栈,最后在RET命令之前释放。这样大小的栈对于存储局部变量、函数参数等足够了。还有一点需要注意的是,栈操作并未使用RBP寄存器,而直接使用RSP寄存器。
特征二,几乎没有使用PUSH/POP指令。请认真看看调用CreateFileA()API时设置参数的代码(0000000140001004~00000001
40001028)。第 14个参数使用寄存器(RCX、RDX、R8、R9), 第57个参数使用栈。main()函数开始执行时,使用MOV指令将参数放入分配的栈。有意思的是,并未看到调用者清理栈(64位fastcall的特征)的代码,原因在于子函数使用的是分配给main()函数的栈,子函数本身不会分配到栈或扩展栈。main()函数的栈管理由main()函数自身负责,子函数不需要管理通过栈传递的参数。
特征三,第五个参数之后的参数在栈中的存储位置。调用CreateFileA() API前要设定参数,设置顺序比较混乱(函数参数在32位程序中会依序压入栈)。第1-4个参数使用寄存器(RCX、RDX、R8、R9),从第五个参数开始使用栈,但第五个参数在栈中的存储位置显得有些奇怪。
1 |
|
从以上代码可以看到,第五个参数在栈中的存储位置为[rsp+20h],并未指向栈顶([rsp])。原因在于,虽然x64系统中第1~4个参数使用寄存器传递,但栈中仍然为它们预留了同等大小的空间(20h=32d=4param*8字节)。所以,第五个参数开始的参数从栈的[rsp+20h]位置(非[rsp]位置)开始保存(这样预留的栈空间也可以在子函数中使用)。接下来进入CreateFileA() API查看。
1 |
|
先看一下由寄存器与栈传递来的参数(参考图37-7)。栈传递的参数之上是参数1-4的预留空间(000000000012FEE0~00000000
0012FEF8)(参考图37-8)。虽然未在传递函数参数时使用,但是从代码37-3的前3个指令可以看到向该空间赋值的操作(有时采用这种方式使用)。
最后谈谈代码37-3中的CreateFileA()的栈帧。由于该函数较长且复杂,所以分配了60h(96d)大小的栈。遇到上面这种情况,调CreateFileW()时使用寄存器和栈,与32位环境下的情形是一致的,重复的值被放入栈。
如果CreateFileA()是一个非常简单的函数,自身不需要使用栈帧,不向栈放入重复值的情形下,可以原样调用CreateFileW()函数。函数用中,64位的这种调用方式要比32位的调用方式好得多。
37.3小结
x64并不只是x86的扩展,设计时做了非常大的改变,Windows 64位OS与开发工具中也存在很多与32位系统不同的部分。现在正处于32位到64位的过渡期,各种信息稀少且杂乱。但制造商非常细心地考虑了HW/SW的向下兼容性,相信能够平稳渡过整个过渡期。64位时代真正到来时,与逆向分析工具及逆向分析方法相关的信息会得到更新升级,进行代码逆向分析会更轻松。