x64 汇编基础

😄

0 内核利用Github

awesome-windows-kernel-security-development。

1 VS内联汇编

在编译 32 位程序的时候,直接 __asm 内联汇编即可使用,但是当目标平台为 x64 时,微软不再允许直接内联汇编了,可通过在 64 位程序中内联汇编代码。

IDE:VS 2019

OS:Windows 10 21H2(19044.2130)

  1. 创建控制台项目。

  2. 源文件右键—添加—新建项—C++文件,命名为 First_Inline_Asm.asm。

  3. 在源文件 First_Inline_Asm.asm 右键—属性—项类型—自定义生成工具—应用(然后属性上就多出来了“自定义生成工具”)。

  4. 在上一步骤基础上,自定义常规工具—常规—命令行:ml64 /c %(filename).asm;输出:%(filename).obj;%(Outputs)。

  5. 在上一步骤基础上,平台:活动x64。

  6. 汇编文件的配置属性—从中生成排除:否。

  7. .asm 里面函数的写法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ; 指定代码编译(可省略)bit 16, bit 32, bit 64 
    .CODE

    GetPebLdr PROC
    mov rax, gs:[60h];
    add rax, 18h;
    mov rax, [rax];
    ret;
    GetPebLdr ENDP

    END
  8. 在主文件中:

    1
    extern "C"  ULONGLONG _fastcall GetPebLdr(void);

注意这种内联汇编源操作数如果是十六进制数,写法如下:

1
2
3
4
5
6
7
8
// 错误,不识别 0x
mov rax, 0xFFEEDDCC01234567;

// 错误,不识别 F
mov rax, FFEEDDCC01234567h;

//正确,在十六进制数前加 0
mov rax, 0FFEEDDCC01234567h;

完整 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// First_Inline_Asm.asm
.CODE

GetPebLdr PROC
mov rax, gs:[60h];
add rax, 18h;
mov rax, [rax];
ret;
GetPebLdr ENDP

END

// main.cpp
#include <iostream>
#include <windows.h>

extern "C" PVOID64 _fastcall GetPebLdr(void);

int main()
{
PVOID64 pRet = GetPebLdr();
printf("%p\n", pRet);
}

参考:使用VS2019写64位内联汇编。

2 寄存器

2.1 通用寄存器(GPRs)

x64 下指针和地址都是 8 字节(64位)。

  • x64 增加了8个通用寄存器:R8~R15,它们都是64位的。
  • 另外还增加了 8 个 128 位 XMM 寄存器。
  • 64 位寄存器与 x86 下的 32 位寄存器兼容,例如 RAX(64位)、EAX(低32)、AX(低16位)、AL(低8位)、AH(8到15位),相应的有R8、R8D、R8W和R8B。
  • 不要在程序中使用如 AH 之类的寄存器,因为在 AMD 的 CPU 上这种用法会与某些指令产生冲突。

1.png

2.png

通用寄存器(General-Purpose Registers, GPRs),参考 Intel 手册卷 1 3.4.1。

64 位通用寄存器共 16 个(不包含 RIP,Rflags)。在 x64 中关于 RSP、RBP:

  • RSP :用来进行相对寻址(RIP-relative addressing)来找到堆栈上的参数、局部变量。不像 x86 基于 EBP 寻址。
  • RBP :仍可做 Base Frame Point,但是常被用来当通用寄存器使用(GPRs)。

2.2 易失/非易失寄存器

易失寄存器是由调用方假想的临时寄存器,并要在调用过程中销毁。 非易失寄存器需要在整个函数调用过程中保留其值,并且一旦使用,则必须由被调用方保存(所以常会看到堆栈中保存 rbp, rdi)。

  • 易变寄存器:RAX, RCX, RDX, R8-R11
  • 非易变寄存器:RBP, RBX, RSI, RDI, R12-R15

2.3 RFLAGS

在 x64 中,将 EFLAGS 扩展成 64 位,但是高 32 位保留。所以对于 x64 来说,RFLAGS 和 EFLAGS 是一样的。

RFLAGS 的位受算术/逻辑运算影响,同时 RFLAGS 的位也影响 CPU 的执行。如 TF 置位时 CPU 单步运行。

具体标识为可看 Intel 手册卷 1 3.4.3 和 EFLAGS寄存器(标志寄存器)。

  • 将 EFLAG 移入和移出堆栈或 EAX 寄存器:LAHF、SAHF、PUSHF、PUSHFD、PUSHFQ、POPF、POPFD 和 POPFQ。
  • 修改 EFLAG 标识位的指令:BT、BTS、BTR 和 BTC。
  • IA-32e 模式下,不允许修改 VM、NT 位(Intel 手册卷3 2.3.1)。

58.png

3.png

下面罗列一些与算术、逻辑运算相关的位:

标志位 位置 解释
CF(Carry flag) Bit 0 进位标志。负数计算溢出时置位。
mov al, 0FFh ; CF = 0
add al, 1 ; CF = 1
OF(Overflow flag ) Bit 11 溢出标志。操作发生溢出,导致符号改变(仅对有符号数)。
mov cl, 07Fh ; OF = 0
add cl, 1 ; OF = 1
ZF(Zero flag) Bit 6 零标志。当算术或逻辑运算的结果为零时设置该标志。 这也可以使用比较指令进行设置。
SF(Sign flag) Bit 7 负数符号标志。运算结果是负数时置 1。

3 函数操作

函数调用约定:__fastcall。

  • 调用者在堆栈中会给 RCX、RDX、R8、R9 预留 0x20 字节的空间。

堆栈字节对齐:

  • x86:8 字节。
  • x64:16 字节。

x64 prolog 和 epilog:

分配堆栈空间、调用其他函数、保存非易失性寄存器或使用异常处理的每个函数都必须具有 prolog,其地址限制在与相应函数表条目关联的展开数据中进行了描述。 与之对应的是 epilog。有了这两部分,x64 函数和 x86 函数在堆栈平衡的处理上有了较大的区别。x64 的函数内在一开始使用 prolog 来开辟堆栈,在最后时才使用 epilog 来平衡堆栈。而不像是 x86 中的 __cdecl 调用约定那样每次调用完函数立即要平衡堆栈。x64 是在函数最后的 epilog 来平衡堆栈。

函数结构中典型 prolog 和 epilog 的代码可能是(类似于 x86 异常处理中的安装 SEH):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 prolog:
mov [RSP + 8], RCX
push R15 // 保存非易失性寄存器
push R14
push R13
sub RSP, fixed-allocation-size // 分配堆栈
lea R13, 128[RSP]

函数主体...

epilog:
add RSP, fixed-allocation-size
pop R13
pop R14
pop R15
ret

3.1 调用约定

x64 仅使用 __fastcall 函数调用约定。

x86 使用 __cdecl、__stdcall、__fastcall、__thiscall。

x64 中不管在函数前面声明哪一种调用约定,编译器都只会只用 __fastcall。

Visual C/C++ 编译器支持下列调用约定。

关键字 堆栈清理 参数传递
__cdecl 调用方 在堆栈上按相反顺序推送参数(从右到左)
__clrcall 不适用 按顺序将参数加载到 CLR 表达式堆栈上(从左到右)
__stdcall 被调用方 在堆栈上按相反顺序推送参数(从右到左)
__fastcall 被调用方 前 4 个参数依此通过 RCX、RDX、R8、R9寄存器传递,其余参数从右到左压入堆栈
__thiscall 被调用方 在堆栈上推送;存储在 ECX 中的 this 指针
__vectorcall 被调用方 存储在寄存器中,然后按相反顺序在堆栈上推送(从右到左)

7.png

3.2 堆栈对齐

堆管理器中的分配粒度相对较小:在 32 位系统上是 8 字节,在 64位 系统上是 16 字节。这部分在《x86 内存管理(二)私有内存-共享内存-物理内存》堆管理器章节提到过。

在 x64 中,为了保证 CPU 访问内存的高效性,堆栈必须 16 字节对齐。下一小节 3个参数Release 用实验来进行说明。

3.3 x64 数据类型大小

参考x64 类型和存储布局。

标量类型 C 数据类型 存储大小(以字节为单位) 建议的对齐方式
INT8 char 1 Byte
UINT8 unsigned char 1 Byte
INT16 short 2 字
UINT16 unsigned short 2 字
INT32 int, long 4 双字
UINT32 unsigned int, unsigned long 4 双字
INT64 __int64 8 四字
UINT64 unsigned __int64 8 四字
FP32 (单精度) float 4 双字
FP64 (双精度) double 8 四字
POINTER * 8 四字
__m64 struct __m64 8 四字
__m128 struct __m128 16 八倍长字

3.4 堆栈操作

3个参数Debug

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

__int64 Test3Parameters(__int64* arg_1, __int64 arg_2, __int64 arg_3)
{
__int64 iLocal = 0;
iLocal = *arg_1;
return iLocal + arg_2 + arg_3;
}

int main()
{
__int64 arg = 2;
arg = Test3Parameters(&arg, 5, 9);
printf("%lld\n", arg);
}

main:

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
42
43
44
45
46
47
48
49
50
// main
.text:00000001400118E0 ; =============== S U B R O U T I N E =======================================
.text:00000001400118E0
.text:00000001400118E0 ; Attributes: bp-based frame fpd=0F0h
.text:00000001400118E0
.text:00000001400118E0 sub_1400118E0 proc near ; CODE XREF: sub_1400112C1↑j
.text:00000001400118E0
.text:00000001400118E0 var_110 = byte ptr -110h
.text:00000001400118E0 var_F0 = byte ptr -0F0h
.text:00000001400118E0 var_E8 = qword ptr -0E8h
.text:00000001400118E0 var_18 = qword ptr -18h
.text:00000001400118E0
.text:00000001400118E0 ; __unwind { // 14001119F
.text:00000001400118E0 push rbp
.text:00000001400118E2 push rdi
.text:00000001400118E3 sub rsp, 108h
.text:00000001400118EA lea rbp, [rsp+20h]
.text:00000001400118EF lea rdi, [rsp+110h+var_F0] //给rcx,rdx,R8,R9预留的空间(0x20)
.text:00000001400118F4 mov ecx, 0Ah
.text:00000001400118F9 mov eax, 0CCCCCCCCh
.text:00000001400118FE rep stosd
.text:0000000140011900 mov rax, cs:__security_cookie
.text:0000000140011907 xor rax, rbp
.text:000000014001190A mov [rbp+0F0h+var_18], rax
.text:0000000140011911 lea rcx, unk_140023035//该地址的地址里面存放数字1
.text:0000000140011918 call sub_1400113CF //CheckForDebuggerJustMyCode
.text:000000014001191D mov [rbp+0F0h+var_E8], 2
.text:0000000140011925 mov r8d, 9
.text:000000014001192B mov edx, 5
.text:0000000140011930 lea rcx, [rbp+0F0h+var_E8]
.text:0000000140011934 call sub_140011136
.text:0000000140011939 mov [rbp+0F0h+var_E8], rax
.text:000000014001193D mov rdx, [rbp+0F0h+var_E8]
.text:0000000140011941 lea rcx, aLld ; "%lld\n"
.text:0000000140011948 call sub_14001143D
.text:000000014001194D xor eax, eax
.text:000000014001194F mov edi, eax
.text:0000000140011951 lea rcx, [rbp+0F0h+var_110]
.text:0000000140011955 lea rdx, unk_14001B850
.text:000000014001195C call sub_14001135C
.text:0000000140011961 mov eax, edi
.text:0000000140011963 mov rcx, [rbp+0F0h+var_18]
.text:000000014001196A xor rcx, rbp ; StackCookie
.text:000000014001196D call j___security_check_cookie
.text:0000000140011972 lea rsp, [rbp+0E8h]
.text:0000000140011979 pop rdi
.text:000000014001197A pop rbp
.text:000000014001197B retn
.text:000000014001197B ; } // starts at 1400118E0
.text:000000014001197B sub_1400118E0 endp

子函数:

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
.text:0000000140011870 ; =============== S U B R O U T I N E =======================================
.text:0000000140011870
.text:0000000140011870 ; Attributes: bp-based frame fpd=0F0h
.text:0000000140011870
.text:0000000140011870 sub_140011870 proc near ; CODE XREF: .text:0000000140011087↑j
.text:0000000140011870 ; sub_140011136↑j
.text:0000000140011870
.text:0000000140011870 var_E8 = qword ptr -0E8h
.text:0000000140011870 arg_0 = qword ptr 10h
.text:0000000140011870 arg_8 = qword ptr 18h
.text:0000000140011870 arg_10 = qword ptr 20h
.text:0000000140011870
.text:0000000140011870 mov [rsp-8+arg_10], r8 //rsp+0x18
.text:0000000140011875 mov [rsp-8+arg_8], rdx
.text:000000014001187A mov [rsp-8+arg_0], rcx
.text:000000014001187F push rbp
.text:0000000140011880 push rdi
.text:0000000140011881 sub rsp, 108h
.text:0000000140011888 lea rbp, [rsp+20h] //给rcx,rdx,R8,R9预留的空间(0x20)
.text:000000014001188D lea rcx, unk_140023035//该地址的地址里面存放数字1
.text:0000000140011894 call sub_1400113CF //CheckForDebuggerJustMyCode
.text:0000000140011899 mov [rbp+0F0h+var_E8], 0 //局部变量
.text:00000001400118A1 mov rax, [rbp+0F0h+arg_0]
.text:00000001400118A8 mov rax, [rax]
.text:00000001400118AB mov [rbp+0F0h+var_E8], rax
.text:00000001400118AF mov rax, [rbp+0F0h+arg_8]
.text:00000001400118B6 mov rcx, [rbp+0F0h+var_E8]
.text:00000001400118BA add rcx, rax
.text:00000001400118BD mov rax, rcx
.text:00000001400118C0 add rax, [rbp+0F0h+arg_10]
.text:00000001400118C7 lea rsp, [rbp+0E8h]
.text:00000001400118CE pop rdi
.text:00000001400118CF pop rbp
.text:00000001400118D0 retn
.text:00000001400118D0 sub_140011870 endp

8.png

3 个参数Release

main

1
2
3
4
5
6
7
8
9
10
11
12
13
.text:0000000140001070 ; =============== S U B R O U T I N E =======================================
.text:0000000140001070
.text:0000000140001070
.text:0000000140001070 sub_140001070 proc near ; CODE XREF: sub_1400011A4+107↓p
.text:0000000140001070 ; DATA XREF: .pdata:000000014000400C↓o
.text:0000000140001070 sub rsp, 28h
.text:0000000140001074 mov edx, 10h
.text:0000000140001079 lea rcx, aLld ; "%lld\n"
.text:0000000140001080 call sub_140001010
.text:0000000140001085 xor eax, eax
.text:0000000140001087 add rsp, 28h
.text:000000014000108B retn
.text:000000014000108B sub_140001070 endp

Release 版本直接将答案计算好了。

注意:这里的 sub rsp, 28h 除了给 rcx,rdx,r8,r9 预留 0x20 字节大小的空间外,还多出来了 0x8 字节,为什么?

答案:为了和返回地址构成 16(0x10) 字节对齐!

观察此时的堆栈情况:

9.png

6个参数Debug

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

__int64 Test3Parameters(__int64* arg_1, __int64 arg_2, __int64 arg_3, __int64 arg_4, __int64 arg_5, __int64 arg_6)
{
__int64 iLocal = 0;
iLocal = *arg_1;
return iLocal + arg_2 + arg_3 - arg_4 + arg_5 + arg_6;
}

int main()
{
__int64 arg = 2;
arg = Test3Parameters(&arg, 3, 4, 5, 6, 8);
printf("%lld\n", arg);
}

main

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
42
43
44
45
46
47
48
49
50
51
52
53
54
.text:0000000140011AC0 ; =============== S U B R O U T I N E =======================================
.text:0000000140011AC0
.text:0000000140011AC0 ; Attributes: bp-based frame fpd=0F0h
.text:0000000140011AC0
.text:0000000140011AC0 sub_140011AC0 proc near ; CODE XREF: sub_1400112C1↑j
.text:0000000140011AC0
.text:0000000140011AC0 var_120 = byte ptr -120h
.text:0000000140011AC0 var_100 = qword ptr -100h
.text:0000000140011AC0 var_F8 = qword ptr -0F8h
.text:0000000140011AC0 var_F0 = byte ptr -0F0h
.text:0000000140011AC0 var_E8 = qword ptr -0E8h
.text:0000000140011AC0 var_18 = qword ptr -18h
.text:0000000140011AC0
.text:0000000140011AC0 ; __unwind { // 14001119F
.text:0000000140011AC0 push rbp
.text:0000000140011AC2 push rdi
.text:0000000140011AC3 sub rsp, 118h
.text:0000000140011ACA lea rbp, [rsp+30h]
.text:0000000140011ACF lea rdi, [rsp+120h+var_F0]
.text:0000000140011AD4 mov ecx, 0Ah
.text:0000000140011AD9 mov eax, 0CCCCCCCCh
.text:0000000140011ADE rep stosd
.text:0000000140011AE0 mov rax, cs:__security_cookie
.text:0000000140011AE7 xor rax, rbp
.text:0000000140011AEA mov [rbp+0F0h+var_18], rax
.text:0000000140011AF1 lea rcx, unk_140023035
.text:0000000140011AF8 call sub_1400113CF
.text:0000000140011AFD mov [rbp+0F0h+var_E8], 2
.text:0000000140011B05 mov [rsp+120h+var_F8], 8
.text:0000000140011B0E mov [rsp+120h+var_100], 6
.text:0000000140011B17 mov r9d, 5
.text:0000000140011B1D mov r8d, 4
.text:0000000140011B23 mov edx, 3
.text:0000000140011B28 lea rcx, [rbp+0F0h+var_E8]
.text:0000000140011B2C call sub_14001144C
.text:0000000140011B31 mov [rbp+0F0h+var_E8], rax
.text:0000000140011B35 mov rdx, [rbp+0F0h+var_E8]
.text:0000000140011B39 lea rcx, aLld ; "%lld\n"
.text:0000000140011B40 call sub_14001143D
.text:0000000140011B45 xor eax, eax
.text:0000000140011B47 mov edi, eax
.text:0000000140011B49 lea rcx, [rbp+0F0h+var_120]
.text:0000000140011B4D lea rdx, unk_14001B850
.text:0000000140011B54 call sub_14001135C
.text:0000000140011B59 mov eax, edi
.text:0000000140011B5B mov rcx, [rbp+0F0h+var_18]
.text:0000000140011B62 xor rcx, rbp ; StackCookie
.text:0000000140011B65 call j___security_check_cookie
.text:0000000140011B6A lea rsp, [rbp+0E8h]
.text:0000000140011B71 pop rdi
.text:0000000140011B72 pop rbp
.text:0000000140011B73 retn
.text:0000000140011B73 ; } // starts at 140011AC0
.text:0000000140011B73 sub_140011AC0 endp

子函数

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
.text:0000000140011A40 sub_140011A40   proc near               ; CODE XREF: sub_14001144C↑j
.text:0000000140011A40
.text:0000000140011A40 var_E8 = qword ptr -0E8h
.text:0000000140011A40 arg_0 = qword ptr 10h
.text:0000000140011A40 arg_8 = qword ptr 18h
.text:0000000140011A40 arg_10 = qword ptr 20h
.text:0000000140011A40 arg_18 = qword ptr 28h
.text:0000000140011A40 arg_20 = qword ptr 30h
.text:0000000140011A40 arg_28 = qword ptr 38h
.text:0000000140011A40
.text:0000000140011A40 mov [rsp-8+arg_18], r9
.text:0000000140011A45 mov [rsp-8+arg_10], r8
.text:0000000140011A4A mov [rsp-8+arg_8], rdx
.text:0000000140011A4F mov [rsp-8+arg_0], rcx
.text:0000000140011A54 push rbp
.text:0000000140011A55 push rdi
.text:0000000140011A56 sub rsp, 108h
.text:0000000140011A5D lea rbp, [rsp+20h]
.text:0000000140011A62 lea rcx, unk_140023035
.text:0000000140011A69 call sub_1400113CF
.text:0000000140011A6E mov [rbp+0F0h+var_E8], 0
.text:0000000140011A76 mov rax, [rbp+0F0h+arg_0]
.text:0000000140011A7D mov rax, [rax]
.text:0000000140011A80 mov [rbp+0F0h+var_E8], rax
.text:0000000140011A84 mov rax, [rbp+0F0h+arg_8]
.text:0000000140011A8B mov rcx, [rbp+0F0h+var_E8]
.text:0000000140011A8F add rcx, rax
.text:0000000140011A92 mov rax, rcx
.text:0000000140011A95 add rax, [rbp+0F0h+arg_10]
.text:0000000140011A9C sub rax, [rbp+0F0h+arg_18]
.text:0000000140011AA3 add rax, [rbp+0F0h+arg_20]
.text:0000000140011AAA add rax, [rbp+0F0h+arg_28]
.text:0000000140011AB1 lea rsp, [rbp+0E8h]
.text:0000000140011AB8 pop rdi
.text:0000000140011AB9 pop rbp
.text:0000000140011ABA retn
.text:0000000140011ABA sub_140011A40 endp

6个参数Release

main

1
2
3
4
5
6
7
8
9
10
.text:0000000140001070 sub_140001070   proc near               ; CODE XREF: sub_1400011A4+107↓p
.text:0000000140001070 ; DATA XREF: .pdata:000000014000400C↓o
.text:0000000140001070 sub rsp, 28h
.text:0000000140001074 mov edx, 12h
.text:0000000140001079 lea rcx, aLld ; "%lld\n"
.text:0000000140001080 call sub_140001010
.text:0000000140001085 xor eax, eax
.text:0000000140001087 add rsp, 28h
.text:000000014000108B retn
.text:000000014000108B sub_140001070 endp

4 其他知识点

4.0 x64寄存器使用注意

  1. 寄存器的使用。

    1. 对 32 位寄存器的写操作(包括运算结果),会自动对相应的 64 位寄存器的高 32 位清 0。
    2. 但是对 16 位的寄存器进行操作,是不会影响高位的,不会自动清零。

    如下指令:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 操作 32 位寄存器,如下两个 xor 指令等价
    xor eax, eax;
    xor rax, rax;

    // 操作 32 位寄存器,如下指令最后 rbx 寄存器的值为 1
    // 结果为 0x0000000000000001
    mov rbx, 0xFFEEDDCCAABB2230;
    mov ebx, 0x1;

    // 操作 16 位寄存器,如下指令最后 rbx 寄存器的值不为 1
    // 结果为 0xFFEEDDCCAABB0001
    mov rbx, 0xFFEEDDCCAABB2230;
    mov bx, 1;
  2. 立即数的使用。

    立即数的使用,优先使用 32 位寄存器(因为 64 位指令较长)。如操作数位宽不超过 32 位,硬尽量使用 32 位的寄存器。

    参数长度不超过4个字节的时候,分别使用32位的寄存器进行传参,这个是编译器做的处理,长度足够存储数据的情况下,优先使用32位寄存器,因为操作64位寄存器(如MOV指令)的指令占4个字节,多一个48前缀指令,说明操作数是64位的,而32位寄存器只需要3个字节的指令长度《x64的一些特性》

  3. x64 指令限制。

    • push/pop 的操作数只能是 32 位,无法操作 64 位操作数。pushad/popad 在 x64 中就完全不支持。但是可以使用 pushfq/popfq。

    • 如下:

      1
      2
      3
      4
      5
      6
      7
      push 0x123456789;	// 该指令不支持
      // 可以替换为
      mov rax, 0x123456789;
      push rax;

      mov qword ptr ds:[rax], 0x123456789; // 不支持
      mov qword ptr ds:[rax], 0x12345678; // 支持
    • 访问 RIP-2GB~RIP+2GB 范围的地址都会使用 RIP-Relative(这里的 RIP 为当前指令的地址)来进行寻址。

      如48:8B05 前缀使用 RIP-Relative(这里的 RIP 为当前指令的地址):x64 中,如果要访问的内存地址范围在 RIP-2GB~RIP+2GB ,则硬编码会使用相对寻址的方式编码。

      1
      2
      3
      4
      5
      6
      // 前缀后面是 00000000 则访问的地址为:当前指令地址+当前指令长度(7) +00000000
      // 前缀后面是 01000000 则访问的地址为:当前指令地址+当前指令长度(7) +00000001
      00007FF92E0D0F20 | 48:8B05 00000000 | mov rax,qword ptr ds:[7FF92E0D0F27]
      00007FF92E0D0F27 | 0048 85 | add byte ptr ds:[rax-7B],cl
      00007FF92E0D0F2A | 48:8B05 01000000 | mov rax,qword ptr ds:[7FF92E0D0F32]
      00007FF92E0D0F31 | 81C1 F0040000 | add ecx,4F0
    • jmp 硬编码。

      可看 Intel 手册卷1

      EB:jmp rel8,Jump short,RIP = RIP+当前指令长度 +(FF~7F)。

      E9:jmp rel16,在 x64 下不支持。

      E9:jmp rel32,Jump near,RIP = RIP+当前指令长度 +(-2G~2G)。

  4. 64位下的inline Hook、R3 下常用Hook技术。

  5. 在 x86 CPU 上,FS 段寄存器指向线程环境块 (TEB) 和处理器控制区域 (KPCR),但在 x64 上,GS 寄存器在用户模式下指向 TEB,在内核中指向 KPCR 的新模式。然而,当运行 WOW64 应用程序(即 x64 系统上的 32 位应用程序)时,FS 寄存器继续指向 32 位版本的 TEB。x64上的陷阱帧结构体nt!_KTRAP_FRAME不包含非易失性寄存器的有效内容。如果打算修改非易失性寄存器,X64函数的prolog会保存非易失性寄存器的值。调试器始终可以从堆栈中提取这些非易失性寄存器的保存值,而不必从陷阱帧中获取它们。 在x64上的内核模式调试期间,.trap命令的输出会打印一条注释,突出显示从陷阱中检索到的所有寄存器的值可能不准确的事实,如下所示。此规则有例外,例如,为用户到内核模式转换生成的陷阱帧确实包含所有寄存器的正确值。

  6. 内存优先使用相对偏移寻址,直接寻址指令较少。

4.1 ASCII

ASCII 表的 Github 项目:

14.png

4.2 小端存储

13.png

4.3 汇编编译器

  1. nasm, fasm, yas:免费开源汇编编译器,都是使用 Intel 语法。
  2. yasm 在 nasm 基础上开发,使用 Intel 语法。
  3. masm:微软发布的汇编编译器,已集合在 VS 中,使用 Intel 语法。
  4. gas:linux 免费开源汇编编译器,使用 AT&T 语法。

4.4 整数取值范围

在计算机处理中,整数会区分 signed(有符号数)和 unsigned(无符号数)两种情 况,数值的 MSB 值被作为符号位。每个数值类型有自己的取值范围,如下所示。

15.png

4.5 x64地址范围

x64 体系里只实现了48 位的虚拟地址,高16 位被用做符号扩展。这高 16 位必须全是 0 或者全是 1,这种形式的地址被称为 canonical 地址。

处理器会对软件上的 non-canonical 地址形式进行检查,程序中访问了non-canonical 地址会产生 #GP 异常。

16.png

4.6 线性地址

在 x86 保护模式下:

逻辑地址:0x23:00400000。0x23 为段选择子,0x00400000 为段偏移。

线性地址:在分页模式下使用,MMU 将逻辑地址可转换为线性地址。由于除了 FS 其余段基址为 0,所以逻辑地址等于线性地址。

虚拟地址:逻辑地址和线性地址都是虚拟地址。

在 x64 下,只有线性地址(虚拟地址)。

  • 实模式、保护模式下的逻辑地址:$segment:offset$。
    • 实模式:$linear\_address = segment << 4 + offset$
    • 保护模式:$linear\_address = segment\_base + offset$
  • x64:$linear\_address = offset$

在 x64 下,除了 FS 与GS 段可以使用非 0 值的 base 外,其余的 ES、 CS、 DS 及 SS 段的 base 值强制为 0 值。因此,实际上线性地址就等于代码中的 offset 值。

4.7 RIP-Relative

4.8 内存对齐

  • x86:8 字节对齐,ESP & 0xFFFFFFF8。
  • x64:16 字节对齐,RSP & 0xFFFFFFFF'FFFFFFF0。

5 64 位模式的指令

5.0 REX prefix

REX prefix 是不定值,它的取值范围是:0x40 - 0x4F (共 16 个),高 4 位固定为 4,低 4 位为变化值。

7 6 5 4 3 2 1 0
0 1 0 0 W R X B

低 4 位表述为:

  • REX.W
  • REX.R
  • REX.X
  • REX.B

当默认操作数大小为 32 位是, REX.W = 1,即前缀取值为 0x48 时,则会将操作数大小改为 64 位。

5.1 操作数大小(operand Size)

在 64 位模式下,仅有少数指令的默认操作数是 64 位,大多数指令的默认操作数仍是 32 位的。在默认操作数为 32 位的指令中,如果要将其变成 64 位的操作数必须使用 REX prefix。

在 64 位模式下指令可以使用 16 位的操作数,但是:如果指令默认操作数大小是 64 位时,不能通过指令前缀来将操作数变成 32 位,但是可以变成 16 位。
52.png

示例一:默认操作数大小 64 位

在 x64 dbg 调试器下,push 指令默认操作数大小为 64 位,如果要使用 16 位操作数需加前缀 0x66。不支持 32 位操作数。

53.png

示例二:默认操作数大小 32 位

在 x64 dbg 调试器下,mov 指令默认操作数大小为 32 位,如果要使用 16 位操作数需加前缀 0x66。使用 64 位操作数需加前缀 0x48。

54.png

默认操作数为64 位的指令
少数指令的默认操作数是 64 位的,这些指令分为两大类:

  1. 依赖于 RIP 指针的 near branch(近程分支)指令。
  2. 依赖于 RSP 指针的 stack 操作指令。

55.png

5.2 64 位模式无效指令

56.png

5.3 寻址地址大小(operand address size)

  • 在 64 位模式下,指令的 default operand address size(默认地址大小)是 64 位的,指的是操作数的寻址大小。
  • 在 64 位模式下,指令的 default operand size(默认操作数大小)为 64 位或 32 位。

指令的默认地址大小同样由 CS.L 和 CS.D 标志决定。
① CS.L = 1 并且 CS.D = 0,默认的地址是 64 位。
② CS.L = 0 并且 CS.D = 1,在 compatibility 模式下,使用 32 位的默认地址。
③ CS.L = 0 并且 CS.D = 0,在 compatibility 模式下,使用 16 位的默认地址。

57.png

如下,在 64-bit 模式下,使用 32 位 寻址时会使用 0x67 指令前缀:

1
2
00007FFA2C7E0960  | 6748:8B01                | mov rax,qword ptr ds:[ecx]    
00007FFA2C7E0964 | 67:FF30 | push qword ptr ds:[eax]