第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
2
3
4
5
6
7
8
9
10
Address		Instruction		Disassembly
--------------------------------------------------------------
00401000 FF1500504000 CALL DWORD PTR DS:[00405000] ;CALL 75CE1E12 (LoadLibraryW)
...
00401030 FF2510504000 JMP DWORD PTR DS:[00405010] ;JMP 75CE10EF (Sleep)


00405000 121ECE75 ; 75CE1E12 (Kernel32!LoadLibraryW)
...
00405010 EF10CE75 ; 75CE10EF (Kernel32!Sleep)

FF15XXXXXXXX指令用于调用API,其中XXXXXXXX “绝对地址”指向IAT区域的某个位置。x64系统中仍使用相同指令,但解析方法不同。

1
2
3
4
5
6
7
8
9
10
Address		Instruction		Disassembly
--------------------------------------------------------------
00000001`00401000 FF15FA3F00O0 CALL DWORD PTR DS:[00000001`00405000] ;CALL 75CE1E12 (LoadLibraryW)
...
00000001`00401030 FF250A400O00 JMP DWORD PTR DS:[00000001`00405010] ;JMP 75CE10EF (Sleep)


00000001`00405000 121ECE7500000000 ; 00000000`75CE1E12 (Kernel32!LoadLibraryW)
...
00000001`00405010 EF10CE7500000000 ; 00000000`75CE10EF (Kernel32!Sleep)

首先指令地址由原来的4个字节变为8个字节,然后,x86中FF15指令后跟着4个字节的绝对地

址(VA)而是不想当然的8个字节。若x64中也采用与x86相同的方式,则FF15后面应该跟着8个字节的绝对地址(VA),这样指令的长度就增加了。为了防止增加指令长度,x64系统中指令后面仍然跟着4个字节大小的地址,只不过该地址被解析为“相对地址”(RVA)。所以上面的指令列表中,FF15后面的4个字节(3FFA)被识别为相对地址,并通过下面的方法将相对地址转换为绝对地址。

000000000401000+3FFA+6=0000000100405000

□ 0000000`00401000: CALL 指令地址

□ 3FFA:相对(地址)

□ 6: CALL指令(FF15XXXXXXXX)长度

□ 0000000`00405000:变换后的绝对地址

由于000000000405000地址中存储着0000000075CE1E12值,所以上面出现的第一个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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "stdio.h"
#include "windows.h"

void main()
{
HANDLE hFile = INVALID_HANDLE_VALUE;

hFile = CreateFileA("c:\\work\\ReverseCore.txt", // 1st - (string)
GENERIC_READ, // 2nd - 0x80000000
FILE_SHARE_READ, // 3rd - 0x00000001
NULL, // 4th - 0000000000
OPEN_EXISTING, // 5th - 0x00000003
FILE_ATTRIBUTE_NORMAL, // 6th - 0x00000080
NULL); // 7th - 0x00000000

if( hFile != INVALID_HANDLE_VALUE )
CloseHandle(hFile);
}

首先使用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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0000000140001000 | 48:83EC 48               | sub rsp,48                              
0000000140001004 | 45:33C9 | xor r9d,r9d ; 4th - pSecurity
0000000140001007 | 48:C74424 30 00000000 | mov qword ptr ss:[rsp+30],0 ; 7th - hTemplateFile
0000000140001010 | 48:8D0D A1110000 | lea rcx,qword ptr ds:[1400021B8] ; 1st - FileName
0000000140001017 | 45:8D41 01 | lea r8d,qword ptr ds:[r9+1] ; 3rd - ShareMode
000000014000101B | BA 00000080 | mov edx,80000000 ; 2nd - Access
0000000140001020 | C74424 28 80000000 | mov dword ptr ss:[rsp+28],80 ; 6th - Attributes
0000000140001028 | C74424 20 03000000 | mov dword ptr ss:[rsp+20],3 ; 5th - Mode
0000000140001030 | FF15 CA0F0000 | call qword ptr ds:[<&CreateFileA>]
0000000140001036 | 48:83F8 FF | cmp rax,FFFFFFFFFFFFFFFF
000000014000103A | 74 09 | je stack.140001045
000000014000103C | 48:8BC8 | mov rcx,rax
000000014000103F | FF15 C30F0000 | call qword ptr ds:[<&CloseHandle>]
0000000140001045 | 33C0 | xor eax,eax
0000000140001047 | 48:83C4 48 | add rsp,48
000000014000104B | C3 | ret

Stack64.exe代码具有如下几个特征。

特征一,使用“变形”的栈帧。在代码起始部分分配48h(72d)字节大小的栈,最后在RET命令之前释放。这样大小的栈对于存储局部变量、函数参数等足够了。还有一点需要注意的是,栈操作并未使用RBP寄存器,而直接使用RSP寄存器。

特征二,几乎没有使用PUSH/POP指令。请认真看看调用CreateFileA()API时设置参数的代码(0000000140001004~0000000140001028)。第 14个参数使用寄存器(RCX、RDX、R8、R9), 第57个参数使用栈。main()函数开始执行时,使用MOV指令将参数放入分配的栈。有意思的是,并未看到调用者清理栈(64位fastcall的特征)的代码,原因在于子函数使用的是分配给main()函数的栈,子函数本身不会分配到栈或扩展栈。main()函数的栈管理由main()函数自身负责,子函数不需要管理通过栈传递的参数。

特征三,第五个参数之后的参数在栈中的存储位置。调用CreateFileA() API前要设定参数,设置顺序比较混乱(函数参数在32位程序中会依序压入栈)。第1-4个参数使用寄存器(RCX、RDX、R8、R9),从第五个参数开始使用栈,但第五个参数在栈中的存储位置显得有些奇怪。

1
0000000140001028 | C74424 20 03000000       | mov dword ptr ss:[rsp+20],3         ; 5th - Mode

从以上代码可以看到,第五个参数在栈中的存储位置为[rsp+20h],并未指向栈顶([rsp])。原因在于,虽然x64系统中第1~4个参数使用寄存器传递,但栈中仍然为它们预留了同等大小的空间(20h=32d=4param*8字节)。所以,第五个参数开始的参数从栈的[rsp+20h]位置(非[rsp]位置)开始保存(这样预留的栈空间也可以在子函数中使用)。接下来进入CreateFileA() API查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
00000000778731F0 	 48:895C24 08             	 mov qword ptr ss:[rsp+8],rbx            
00000000778731F5 48:896C24 10 mov qword ptr ss:[rsp+10],rbp
00000000778731FA 48:897424 18 mov qword ptr ss:[rsp+18],rsi
00000000778731FF 57 push rdi
0000000077873200 48:83EC 60 sub rsp,60
0000000077873204 8BFA mov edi,edx
0000000077873206 48:8BD1 mov rdx,rcx
0000000077873209 48:8D4C24 50 lea rcx,qword ptr ss:[rsp+50]
000000007787320E 49:8BF1 mov rsi,r9
0000000077873211 41:8BE8 mov ebp,r8d
0000000077873214 FF15 26950700 call qword ptr ds:[<&RtlInitAnsiStringE
000000007787321A 85C0 test eax,eax
000000007787321C 0F88 EC5A0100 js kernel32.77888D0E

...

0000000077873263 8BD7 mov edx,edi
0000000077873265 E8 86FEFFFF call kernel32.778730F0
000000007787326A 48:85C0 test rax,rax
000000007787326D 0F85 765A0100 jne kernel32.77888CE9
0000000077873273 48:8B8424 A0000000 mov rax,qword ptr ss:[rsp+A0]
000000007787327B 4C:8BCE mov r9,rsi
000000007787327E 44:8BC5 mov r8d,ebp
0000000077873281 48:894424 30 mov qword ptr ss:[rsp+30],rax
0000000077873286 8B8424 98000000 mov eax,dword ptr ss:[rsp+98]
000000007787328D 8BD7 mov edx,edi
000000007787328F 894424 28 mov dword ptr ss:[rsp+28],eax
0000000077873293 8B8424 90000000 mov eax,dword ptr ss:[rsp+90]
000000007787329A 48:8BCB mov rcx,rbx
000000007787329D 894424 20 mov dword ptr ss:[rsp+20],eax
00000000778732A1 E8 E2FEFFFF call <JMP.&CreateFileW>
00000000778732A6 48:8BD8 mov rbx,rax
00000000778732A9 48:8D4C24 40 lea rcx,qword ptr ss:[rsp+40]
00000000778732AE FF15 1C990700 call qword ptr ds:[<&RtlFreeUnicodeStri
00000000778732B4 48:8BC3 mov rax,rbx
00000000778732B7 48:8B5C24 70 mov rbx,qword ptr ss:[rsp+70]
00000000778732BC 48:8B6C24 78 mov rbp,qword ptr ss:[rsp+78]
00000000778732C1 48:8BB424 80000000 mov rsi,qword ptr ss:[rsp+80]
00000000778732C9 48:83C4 60 add rsp,60
00000000778732CD 5F pop rdi
00000000778732CE C3 ret

先看一下由寄存器与栈传递来的参数(参考图37-7)。栈传递的参数之上是参数1-4的预留空间(000000000012FEE0~000000000012FEF8)(参考图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位时代真正到来时,与逆向分析工具及逆向分析方法相关的信息会得到更新升级,进行代码逆向分析会更轻松。


第37章 x64处理器
https://m0ck1ng-b1rd.github.io/1999/03/07/逆向工程核心原理/第37章 x64处理器/
作者
何语灵
发布于
1999年3月7日
许可协议