写在前面:在 PWN 的世界里,汇编语言就是我们的“母语”。无论是用
objdump看反汇编,还是在 GDB 里单步调试,或者是手工挑选 ROP Gadget,都离不开对每一条汇编指令的精准理解。本文将为你梳理最核心的 6 条指令,特别是新手最容易混淆的lea和mov。
📑 目录
movvslea:取值与取址的终极对决- 栈操作双雄:
push与pop - 控制流转移:
call与ret(ROP 的灵魂) - 模拟实战:一段汇编的栈帧变化推演
1.movvslea:取值与取址的终极对决
这两个指令是看懂反汇编的第一道门槛。记住一句话:mov看重内容,lea看重地址。
1.1mov(Move Data)
- 作用:把源操作数的值复制给目的操作数。
- 格式:
mov dest, src - PWN 场景:
mov rax, 0x10—— 将常数0x10赋值给rax。mov rdi, rsi—— 将rsi寄存器的值赋给rdi(64位传参常用)。mov rax, [rbp - 0x10]——注意这里的方括号!方括号代表解引用(像 C 语言的*)。这条指令的意思是:把内存地址rbp - 0x10处存放的数据读出来,放进rax。
1.2lea(Load Effective Address)
- 作用:计算源操作数的地址,并把这个地址值赋给目的操作数。不访问内存!
- 格式:
lea dest, src - PWN 场景:
lea rax, [rbp - 0x10]—— 这里没有解引用!这条指令的意思是:计算出rbp - 0x10这个地址值,然后把这个地址值本身放进rax。- 用途:通常用于把一个局部变量(比如
char buf[64])的首地址传给函数。比如lea rax, [rbp-0x40]然后mov rdi, rax,接着call gets,这就是把buf的地址传给gets函数的第一个参数。
2. 栈操作双雄:push与pop
栈是向下生长的(高地址向低地址),RSP(栈顶指针)始终指向栈顶元素。这两个指令会自动修改RSP。
2.1push(压栈)
- 动作:
RSP = RSP - 8(64位) 或RSP = RSP - 4(32位),然后将操作数存入RSP指向的新内存地址。 - PWN 场景:函数序言中的
push rbp,就是把主调函数的栈底保存起来,以便函数返回时恢复。
2.2pop(出栈)
- 动作:将
RSP当前指向的内存地址中的数据读入目标操作数,然后RSP = RSP + 8(或 +4)。 - PWN 场景:函数结语中的
pop rbp,就是恢复主调函数的栈底。更重要的是,ROP 攻击中,我们要寻找大量的pop rdi; ret这样的 Gadget,用来把栈上的数据弹入寄存器,从而控制函数参数!
3. 控制流转移:call与ret(ROP 的灵魂)
这两个指令决定了程序执行到哪里去,也是我们劫持控制流的核心目标。
3.1call(调用函数)
- 动作:等价于
push 下一条指令的地址+jmp 目标函数地址。 - 本质:它把返回地址(
call指令后面的那条指令地址)压入栈中,然后跳转。这就解释了为什么发生栈溢出时,我们覆盖的正是这个被call压入栈中的返回地址。
3.2ret(函数返回)
- 动作:等价于
pop rip。 - 本质:从栈顶弹出一个 8 字节的数据,直接塞进指令指针寄存器
RIP中,CPU 接着就会跳转到这个地址执行。 - PWN 启示:这就是栈溢出能拿 Shell 的灵魂!只要我们通过溢出覆盖了栈顶的返回地址,当函数执行到
ret时,就会乖乖跳到我们指定的地址去执行。如果是连续的ret,就会不断从栈上弹出地址并跳转,这就是 ROP(Return-Oriented Programming)链的底层逻辑。
4. 模拟实战:一段汇编的栈帧变化推演
我们模拟一段常见的函数调用汇编,推演一下栈和寄存器的变化:
0x401100 <main>: call 0x401200 <vulnerable> ; 1. 压入返回地址 0x401105,RSP-8,跳转 0x401200 <vulnerable>: push rbp ; 2. 压入旧 rbp,RSP-8 mov rbp, rsp ; 3. 设置当前栈底,此时 rbp == rsp sub rsp, 0x40 ; 4. 开辟 0x40 字节栈空间,RSP-0x40 lea rax, [rbp-0x40] ; 5. 计算 buf 地址,存入 rax mov rdi, rax ; 6. 把 buf 地址传给 rdi (作为 gets 参数) call 0x401040 <gets@plt> ; 7. 压入返回地址,调用 gets ; 假设此时输入了 72 个 'A' + p64(0xdeadbeef) leave ; 8. 等价于 mov rsp, rbp; pop rbp ret ; 9. 此时栈顶正好是 0xdeadbeef,pop rip,跳转!假设性说明(GDB 模拟推演):
当程序执行到ret指令前,如果你在 GDB 中查看栈顶 (x/gx $rsp),你会看到:
0x7fffffffde08: 0x00000000deadbeef执行ret后,查看寄存器 (info registers rip),你会看到:
rip 0xdeadbeef 0xdeadbeef控制流被完美劫持!
5. 结语
汇编并不难,难在建立“寄存器与内存联动”的空间感。牢记lea算地址、mov拿数据、ret弹栈跳转,你就已经掌握了 PWN 所需的 80% 的汇编底子。
下一部分,我们将面对实战中常见的“拦路虎”——反调试检测,教你如何用ptrace绕过它。如果本文对你有帮助,请点赞收藏支持!🙏