逆核原理No.17--x64_Processor

学习完PE格式在32bit OS和64bit OS的差异之后,本节探讨64bit & 32bit处理器在逆向工程中一些需要了解的差异性,方便在你想过程中根据实际场景做分析思路的变动。


0x00

32位系统和64位系统有什么区别? 32位和64位表示CPU一次能处理的最大位数,理论上来说,64位系统处理的数据效率比32位更高,相当于单车道和双车道开车似得,双车道单位时间可以有更多的车辆通行。 但需要内存跟上,而且程序本身也是64位编译才能发挥64位系统的优势。

那么问题来了32位编译的程序 & 64位编译出来的程序有些什么需要掌握的差异呢?


差异

CALL|JMP指令

32位的x86系统中,CALL/JMP指令的使用形式为“地址 指令 CALL/JMP”

注意到0x00401000 的FF15 XXXXXXXX指令用于调用API,其中XXXXXXXX位绝对地址,指向IAT区域的某个位置。这在x64中形式一致,但指令解析不同

  • 首先在指令地址的部分,由x86中的4字节变为8个字节
  • X86中FF15之后跟的4个字节的绝对地址,若在64位中若也采取一样的操作则会造成指令长度的增加(根据64位处理器索引范围正常也应该是8字节),故为了避免指令长度的增加,64处理器在FF15后的内容也是采取4字节**,不过为了兼容索引范围,将这四个字节的内容解析为相对地址**,相对地址换算的过程: 0x0000000100401000 FF15FA3F0000 0x0000000100401000+3FFA+6 = 0x0000000100405000,计算的是相对于当前指令地址的,指令地址➕偏移➕指令长度(6)

(PS:截图取自《逆向工程核心原理》书中内容)



函数调用约定*

32bit os

32位下的函数调用包含:cdecl stdcallfastcall

cdecl方式:C/C++默认方式,参数从右往左依次入栈,主调函数负责栈平衡

stdcall方式:Windows API默认方式,参数从右往左依次入栈,被调函数负责栈平衡

fastcall方式:这种方式优先选择将参数传入ECX,EDX,多的参数从右往左依次入栈,由于栈位于内存区域,寄存器则是硬件存取,故称fastcall


64bit os

在64位系统中将他们归为一种变形后的fastcall,至多可将4个参数存储到寄存器中传递

若参数个数超过4个,则从第五个开始会存入栈中,此外函数返回时传递参数过程中使用的栈由调用者清理。ps:尽管函数前四个参数由寄存器传递,可在栈中依旧会保留着四个参数对应的空间。(32bytes)



栈&栈桢

(这块理解还不是很深)

64bit OS中栈的大小比函数实际使用的空间要大得多。在调用子函数(Sub Function)时,不再使用PUSH命令来传递参数,而是通过MOV指令操作寄存器与预定的栈位置来传递。

使用**VC++**创建的x64程序代码中几乎看不到PUSH/POP指令,并且创建栈桢时也不再使用RBP寄存器,而是直接使用RSP,优点是:调用子函数时不需要改变栈指针(RSP),函数返回时也不需要清理栈指针,如此可大幅提升程序的运行速度。

处于深化对二者不同的理解,将如下源代码编译成不同的版本可执行程序

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

32bit version

前边提到32位系统下,调用windows API默认使用的stdcall方式,并且栈由被调用API处理。这里需要关注的过程是API调用过程中的32位调用特征:一是栈桢的设置,二是函数的参数使用过程中栈的清理方。

main函数的入口在0x0040116A特征是main需要三个参数,找到连续三个push,在此之前的指令是系统在设置程序需要的环境,只有完成这一步,才能由main接管程序执行,否则会出现程序致命性错误。

跟进main里边

首先可以看到指令地址还是属于用户区域的,另外API CreateFIleA是ascii的版本,所用的7个参数都使用栈来传递。

push指令进行过后的栈情况

跟进0x00401017,来到0x765D3F40,该区域不属于用户区,是核心区

跟进

1
2
3
75E2F9D2    55              push ebp
75E2F9D3 8BEC mov ebp,esp
;这是在设置栈桢的指令,这是关注点一

接着后边的指令调用了CreateFileW API这是unicode版本,参数一致,还是那7个,不同的是存储方式使用unicode

并且相比第一次入栈的方式不同,这里使用的是第一次入栈后用栈桢来定位的,ss:[ebp+0xXX],运行至0x0075E2F9FC,观察栈的情况:

随后在API进行retn前有个leave指令,用于清理栈,恢复栈桢到调用函数前的状态



64bit version

Windbg 64bit下的视图:

从CreateFile API的位置看得到栈桢操作并不是32bit下的操作ebp的方式:

1
2
3
;32bit OD下视图
75E2F9D2 55 push ebp
75E2F9D3 8BEC mov ebp,esp

64bit是直接操作RSP栈顶指针的,使用sub rsp, 48h 开辟的栈空间大小是48H,也就是72bytes,在函数retn前使用平栈操作 add rsp, 48h ,在观察传参的行为:xor r9d,r9d 清空r9寄存器的低四字节内容。

ps:64处理器中对于通用寄存器R8-R15,使用在寄存器后加flag标识高低位,b表示一字节,w表示二字节,d表示四字节。

从入栈的位置可以看出即使前四个参数使用寄存器传递,但栈中依旧保留了空间,rsp~rsp+20h

Author: Victory+
Link: https://cvjark.github.io/2022/05/14/逆核原理No-17-x64-Processor/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.