pwncollege汇编入门
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 |
堆栈地址:
CPU 知道堆栈在哪里,因为它的地址存储在 rsp 中。
push 0xb0bacafe |
堆栈向较小的内存地址反向增长!
push 减少 rsp,pop 增加 rsp。
访问内存
可以使用mov在寄存器和内存之间移动数据。
这会将存储在内存地址 0x12345 的 64 位值加载到 rbx 中:
mov rax, 0x12345 |
每个寻址内存位置包含一个字节。
在地址 0x133337 处写入 8 字节将写入地址
0x133337 至 0x13333f。
控制写入大小
可以使用部分来存储/加载更少的位
从地址 0x12345 加载 64 位,并将低 32 位存储到地址 0x133337。
mov rax, 0x12345 |
更改 32 位部分(例如,通过从内存加载)会将整个 64 位寄存器清零。但是,将 32 位存储到内存中没有这样的问题。
字节顺序
大多数现代系统上的数据都是以小端序向后存储的。
mov eax, 0xc001ca75 # 将 rax 设置为c0 01 ca 75 |
字节仅在多字节存储和将寄存器加载到内存时才会进行混洗!
各个字节的位永远不会被混洗。
是的,对堆栈的写入就像对内存的任何其他写入一样。
地址计算
可以对内存地址进行一些有限的计算。
使用 rax 作为某个基址(在本例中为堆栈)的偏移量。
mov rax, 0 |
地址计算有限制。
reg+reg*(2 或 4 或 8)+value 是最好的。
RIP 相对寻址
lea 是少数几个可以直接访问 rip 寄存器的指令之一!
lea rax, [rip] # 将下一条指令的地址加载到 rax |
这对于处理嵌入在代码附近的数据非常有用!
这就是使现代机器上的某些安全功能成为可能的原因。
将立即数写入内存
也可以写入立即数。但是,您必须指定它们的大小!
这会将 32 位 0x1337(用 0 位填充)写入地址 0x133337。
mov rax, 0x133337 |
根据您的汇编程序,它可能需要 DWORD 而不是 DWORD PTR。
控制流
Jumps
CPU 按顺序执行指令,直到被告知不要执行。
中断序列的一种方法是使用 jmp 指令,jmp 跳过 X 个字节,然后恢复执行:
mov cx, 1337 |
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 文件描述符 |
系统调用具有定义非常明确的接口,很少发生变化。
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 值写入堆栈 |
open() 返回 rax 中的文件描述符编号
常量参数
一些系统调用需要古老的“常量”。
例如:open() 有一个 flags 参数来确定如何打开文件。
我们可以在 C 中找出这些参数的值!
|
打印结果:O_RDONLY is: 0
Building Programs
从汇编到二进制文件
构建一个汇编文件:
.intel_syntax noprefix |
┌──(mikannse㉿kali)-[~] |
┌──(mikannse㉿kali)-[~] |
反汇编
┌──(mikannse㉿kali)-[~] |
提取二进制代码
┌──(mikannse㉿kali)-[~] |
┌──(mikannse㉿kali)-[~] |
Debugging
调试是通过调试器(例如 gdb)完成的。
调试器使用(除其他方法外)一种特殊的调试指令:
mov rdi, 42 // 我们程序的返回代码(例如,对于 bash 脚本) |
当 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