Registers

常见的寄存器

8085:a、c、d、b、e、h、l
8086:ax、cx、dx、bx、sp、bp、si、di
x86:eax、ecx、edx、ebx、esp、ebp、esi、edi
amd64:rax、rcx、rdx、rbx、rsp、rbp、rsi、rdi、r8、r9、r10、r11、r12、r13、r14、r15
arm:r0、r1、r2、r3、r4、r5、r6、r7、r8、r9、r10、r11、r12、r13、r14
下一条指令的地址位于寄存器:eip (x86)、rip (amd64)、r15 (arm)

amd64寄存器

64 32 16 8H 8L
rax eax ax ah al
rcx ecx cx ch cl
rdx edx dx dh dl
rbx ebx bx bh bl
rsp esp sp spl
rbp ebp bp bpl
rsi esi si sil
rdi edi di dil
r8 r8d r8w r8b
r9 r9d r9w r9b
r10 r10d r10w r10b
r11 r11d r11w r11b
r12 r12d r12w r12b
r13 r13d r13w r13b
r14 r14d r14w r14b
r15 r15d r15w r15b

位数拓展

mov eax, -1
movsx rax, eax
movsx 执行符号扩展移动,保留二进制补码值(即,将最高位复制到寄存器的其余部分)。
eax 现在是 0xffffffff(4294967295 和 -1 都为)但是rax 现在是 0xffffffffffffffff(4294967295 和 -1 都为)!

寄存器算数

Instruction C / Math equivalent Description
add rax, rbx rax = rax + rbx Add rbx to rax
sub ebx, ecx ebx = ebx - ecx Subtract ecx from ebx
imul rsi, rdi rsi = rsi * rdi Multiply rsi with rdi, truncate to 64-bits
inc rdx rdx = rdx + 1 Increment rdx
dec rdx rdx = rdx - 1 Decrement rdx
neg rax rax = 0 - rax Negate rax in terms of numerical value
not rax rax = ~rax Negate each bit of rax
and rax, rbx rax = rax & rbx Bitwise AND between the bits of rax and rbx
or rax, rbx rax = rax rbx
xor rcx, rdx rcx = rcx ^ rdx Bitwise XOR (don’t confuse ^ for exponent!)
shl rax, 10 rax = rax << 10 Shift rax’s bits left by 10, filling with 10 zeroes on the right
shr rax, 10 rax = rax >> 10 Shift rax’s bits right by 10, filling with 10 zeroes on the left
sar rax, 10 rax = rax >> 10 Shift rax’s bits right by 10, with sign-extension to fill the now “missing” bits!
ror rax, 10 rax = (rax >> 10) | (rax << 54) rotate the bits of rax right by 10
rol rax, 10 rax = (rax << 10)| (rax >> 54) rotate the bits of rax left by 10

特殊的寄存器

不能直接读取或写入 rip,包含要执行的下一条指令的内存地址(ip = 指令指针)。
应该小心使用 rsp,包含用于存储临时数据的内存区域的地址(sp = 堆栈指针)。

Memory

stack

堆栈有多种用途。现在,我们来谈谈临时数据存储。 可以将寄存器和立即数推送到堆栈上以保存值:

mov rax, 0xc001ca75
push rax
push 0xb0bacafe # 警告:即使在 64 位 x86 上,也只能推送 32 位立即数
push rax (与 mov 一样,push 会使 src 寄存器中的值保持不变。)
# 可以将值从堆栈中弹出(弹出到任何寄存器)。
pop rbx # 将 rbx 设置为 0xc001ca75
pop rcx # 将 rcx 设置为 0xb0bacafe

堆栈地址:

CPU 知道堆栈在哪里,因为它的地址存储在 rsp 中。

push 0xb0bacafe
pop rcx

堆栈向较小的内存地址反向增长!
push 减少 rsp,pop 增加 rsp。

访问内存

可以使用mov在寄存器和内存之间移动数据。
这会将存储在内存地址 0x12345 的 64 位值加载到 rbx 中:

mov rax, 0x12345
mov rbx, [rax]
# 将 rbx 中的 64 位值存储到地址 0x133337 的内存中:
mov rax, 0x133337
mov [rax], rbx
# push rcx相当于:
sub rsp, 8
mov [rsp], rcx

每个寻址内存位置包含一个字节。
在地址 0x133337 处写入 8 字节将写入地址
0x133337 至 0x13333f。

控制写入大小

可以使用部分来存储/加载更少的位
从地址 0x12345 加载 64 位,并将低 32 位存储到地址 0x133337。

mov rax, 0x12345
mov rbx, [rax]
mov rax, 0x133337
mov [rax], ebx
# 从地址 0x12345 加载 8 位到 bh。
mov rax, 0x12345
mov bh, [rax]

更改 32 位部分(例如,通过从内存加载)会将整个 64 位寄存器清零。但是,将 32 位存储到内存中没有这样的问题。

字节顺序

大多数现代系统上的数据都是以小端序向后存储的。

mov eax, 0xc001ca75 # 将 rax 设置为c0 01 ca 75
mov rcx, 0x10000
mov [rcx], eax # 将数据存储为75 ca 01 c0
mov bh, [rcx] # 读取结果为 0x75

字节仅在多字节存储和将寄存器加载到内存时才会进行混洗!
各个字节的位永远不会被混洗。
是的,对堆栈的写入就像对内存的任何其他写入一样。

地址计算

可以对内存地址进行一些有限的计算。
使用 rax 作为某个基址(在本例中为堆栈)的偏移量。

mov rax, 0
mov rbx, [rsp+rax *8] # 读取堆栈指针右侧的 qword
inc rax
mov rcx, [rsp+rax *8] # 读取前一个 qword 右侧的 qword

# 可以使用加载有效地址 (lea) 获取计算出的地址。
mov rax, 1
pop rcx
lea rbx, [rsp+rax*8+5] # rbx 现在保存计算出的地址以进行双重检查
mov rbx, [rbx]

地址计算有限制。
reg+reg*(2 或 4 或 8)+value 是最好的。

RIP 相对寻址

lea 是少数几个可以直接访问 rip 寄存器的指令之一!

lea rax, [rip] # 将下一条指令的地址加载到 rax
lea rax, [rip+8] # 下一条指令的地址,加上 8 个字节
# 也可以使用 mov 直接从这些位置读取!
mov rax, [rip] # 从下一条指令地址指向的位置加载 8 个字节
# 甚至可以在那里写入!
mov [rip], rax # 在下一条指令上写入 8 个字节(注意事项适用)

这对于处理嵌入在代码附近的数据非常有用!
这就是使现代机器上的某些安全功能成为可能的原因。

将立即数写入内存

也可以写入立即数。但是,您必须指定它们的大小!
这会将 32 位 0x1337(用 0 位填充)写入地址 0x133337。

mov rax, 0x133337
mov DWORD PTR [rax], 0x1337

根据您的汇编程序,它可能需要 DWORD 而不是 DWORD PTR。

控制流

Jumps

CPU 按顺序执行指令,直到被告知不要执行。

中断序列的一种方法是使用 jmp 指令,jmp 跳过 X 个字节,然后恢复执行:

mov cx, 1337
jmp STAY_LEET
mov cx, 0
STAY_LEET:
push rcx

# jmp可以依赖条件!
mov cx, 1337
jnz STAY_LEET
mov cx, 0
STAY_LEET:
push rcx
Condition Mnemonic Description
Equal je Jump if equal
Not Equal jne Jump if not equal
Greater jg Jump if greater
Less jl Jump if less
Less or Equal jle Jump if less than or equal
Greater or Equal jge Jump if greater than or equal
Above (Unsigned) ja Jump if above (unsigned)
Below (Unsigned) jb Jump if below (unsigned)
Above or Equal (Unsigned) jae Jump if above or equal (unsigned)
Below or Equal (Unsigned) jbe Jump if below or equal (unsigned)
Signed js Jump if signed
Not Signed jns Jump if not signed
Overflow jo Jump if overflow
Not Overflow jno Jump if not overflow
Zero jz Jump if zero
Not Zero jnz Jump if not zero

System Calls

系统调用是调用操作系统的指令。
syscall 触发由 rax 中的值指定的系统调用。
rdi、rsi、rdx、r10、r8 和 r9 中的参数
rax 中的返回值
从 stdin 读取 100 个字节到堆栈:

n = read(0, buf, 100);

mov rdi, 0 # stdin 文件描述符
mov rsi, rsp # 将数据读入堆栈
mov rdx, 100 # 要读取的字节数
mov rax, 0 # read() 的系统调用编号
syscall # 执行系统调用
read 返回通过 rax 读取的字节数,因此我们可以轻松地将它们写出:
write(1, buf, n);
mov rdi, 1 # 标准输出文件描述符
mov rsi, rsp # 从堆栈写入数据
mov rdx, rax # 要写入的字节数(与我们读入的相同)
mov rax, 1 # write() 的系统调用编号
syscall # 执行系统调用

系统调用具有定义非常明确的接口,很少发生变化。
Linux 中有 300 多个系统调用。以下是一些示例:
int open(const char *pathname, int flags) - 返回打开文件的新文件描述符(也显示在 /proc/self/fd 中!)
ssize_t read(int fd, void *buf, size_t count) - 从文件描述符读取数据
ssize_t write(int fd, void *buf, size_t count) - 将数据写入文件描述符
pid_t fork() - 分叉相同的子进程。如果您是子进程,则返回 0;如果您是父进程,则返回子进程的 PID。
int execve(const char *filename, char **argv, char **envp) - 替换您的进程。
pid_t wait(int *wstatus) - 等待子进程终止,返回其 PID,将其状态写入 *wstatus。

字符串参数

一些系统调用采用“字符串”参数(例如,文件路径)。
字符串是内存中的一串连续字节,后跟一个 0 字节。
让我们在堆栈上为 open() 构建一个文件路径:

mov BYTE PTR [rsp+0], '/' # 将 / 的 ASCII 值写入堆栈
mov BYTE PTR [rsp+1], 'f'
mov BYTE PTR [rsp+2], 'l'
mov BYTE PTR [rsp+3], 'a'
mov BYTE PTR [rsp+4], 'g'
mov BYTE PTR [rsp+5], 0 # 写入将终止字符串的 0 字节
# 现在,我们可以 open() /flag 文件了!
mov rdi, rsp # 从堆栈读取数据到寄存器
mov rsi, 0 # 以只读方式打开文件
mov rax, 2 # open() 的系统调用编号
syscall # 执行系统调用

open() 返回 rax 中的文件描述符编号

常量参数

一些系统调用需要古老的“常量”。
例如:open() 有一个 flags 参数来确定如何打开文件。
我们可以在 C 中找出这些参数的值!

#include <stdio.h>
#include <fcntl.h>
int main() {
printf("O_RDONLY is: %d\n", O_RDONLY);
}

打印结果:O_RDONLY is: 0

Building Programs

从汇编到二进制文件

构建一个汇编文件:

.intel_syntax noprefix
.global _start
_start:
mov rdi, 42 # 程序的返回代码
mov rax, 60 # exit() 的系统调用号
syscall
┌──(mikannse㉿kali)-[~]
└─$ gcc -nostdlib -o test test.s
/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000000001000
┌──(mikannse㉿kali)-[~]
└─$ ./test

┌──(mikannse㉿kali)-[~]
└─$ echo $?
42

反汇编

┌──(mikannse㉿kali)-[~]
└─$ objdump -M intel -d test

test: file format elf64-x86-64


Disassembly of section .text:

0000000000001000 <_start>:
1000: 48 c7 c7 2a 00 00 00 mov rdi,0x2a
1007: 48 c7 c0 3c 00 00 00 mov rax,0x3c
100e: 0f 05 syscall

提取二进制代码

┌──(mikannse㉿kali)-[~]
└─$ objcopy --dump-section .text=test_binary_code test
┌──(mikannse㉿kali)-[~]
└─$ hd test_binary_code
00000000 48 c7 c7 2a 00 00 00 48 c7 c0 3c 00 00 00 0f 05 |H..*...H..<.....|
00000010

Debugging

调试是通过调试器(例如 gdb)完成的。
调试器使用(除其他方法外)一种特殊的调试指令:

mov rdi, 42 // 我们程序的返回代码(例如,对于 bash 脚本)
mov rax, 60 // exit() 的系统调用号
int3 // 使用断点触发调试器!
syscall // 执行系统调用

当 int3 断点指令执行时,被调试的程序被中断,您可以检查其状态!
当然,调试器本身可以设置断点:
用 int3 覆盖断点地址处的指令。
模拟执行断点时的效果!

其他工具

GDB 是您的首选调试工具。
您一定会和它成为非常好的朋友。
strace 可让您了解程序如何与操作系统交互。
这是调试的绝佳第一站。
Rappel 可让您探索指令的效果。
https://github.com/yrp604/rappel 获取它,或者直接使用 dojo 中的预安装版本!
可通过 https://github.com/zardus/ctf-tools 轻松安装。
x86 文档:
按字节值列出的操作码:http://ref.x86asm.net/coder64.html
指令文档:https://www.felixcloutier.com/x86/
Intel 的 x86_64 架构手册:https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-instruction-set-reference-manual-325383.pdf