CTF Pwn模块系列分享(二):汇编基础+Linux内存模型拆解
今天进入Pwn学习的关键前置关——汇编基础+Linux进程内存模型。
今天我不会讲复杂的底层原理,只挑Pwn解题必须用到的核心内容,用大白话+实操案例拆解,保证新手也能看懂、会用。毕竟这两块是后续学栈溢出、ROP的基础,打好基础,后面的学习会事半功倍!
一、先明确:为什么必须学汇编和内存模型?
在讲具体内容前,先解决大家的疑惑:Pwn是二进制漏洞利用,为什么非要学这些?
今天我们重点讲“x86_64架构”的汇编(CTF Pwn题90%以上是x86_64,也就是64位Linux环境),不用纠结32位,先把64位核心内容吃透。
二、汇编基础:新手必懂的核心寄存器+关键指令
汇编的核心是“寄存器”和“指令”——寄存器是CPU的“临时仓库”,指令是CPU的“操作命令”。新手不用记所有寄存器和指令,掌握下面这些Pwn高频内容就够了。
1. 高频寄存器:6个核心寄存器及其用途
x86_64有很多寄存器,但Pwn中最常用的是这6个,记住它们的分工:
小提醒:寄存器名前加“r”是64位(比如rax),加“e”是32位(比如eax),Pwn中重点关注64位。
2. 关键指令:5个高频指令,理解程序核心操作
指令是CPU的操作命令,Pwn中最常用的5个指令,拆解如下:
1. mov 目标, 源:
功能:把“源”的数据复制到“目标”(比如把内存中的数据放到寄存器,或寄存器之间传数据)。
例子:mov rax, 0x1 → 把数字1放到rax寄存器;mov rdi, [rbp+0x8] → 把rbp+0x8地址处的内存数据放到rdi。
2. push 数据:
功能:把数据压入栈中(栈是“先进后出”的结构),同时rsp会减8(64位系统,一个数据占8字节)。
例子:push rbp → 把rbp寄存器的值压入栈,rsp = rsp - 8。
3. pop 目标:
功能:把栈顶的数据弹出到“目标”,同时rsp会加8。
例子:pop rbp → 把栈顶数据放到rbp,rsp = rsp + 8。
4. call 函数地址:
功能:调用函数,核心操作有两个——① 把当前rip的值(下一条要执行的指令地址)压入栈(这就是“返回地址”);② 把rip指向函数地址。
关键:Pwn中栈溢出的核心,就是修改这个“返回地址”,让程序执行我们想执行的代码!
5. ret:
功能:函数执行结束后返回,核心操作——把栈顶的返回地址弹出到rip。
例子:函数执行完call后,执行ret,就会把之前压入栈的返回地址弹到rip,程序回到call之后的指令继续执行。
三、Linux进程内存模型:找到漏洞的内存地图
当程序运行时,操作系统会给它分配一块独立的内存空间,这块空间按功能分成不同区域——这就是“进程内存模型”。Pwn的漏洞都和“栈”相关,所以重点讲栈,其他区域简单了解即可。
1. 内存四大核心分区(从低地址到高地址)
代码段(.text):存放程序的指令(比如我们刚才讲的mov、call指令),只读(不能修改)。
数据段(.data/.bss):存放程序的全局变量、静态变量(比如程序里定义的int a=10)。
堆(Heap):动态分配内存的区域(比如程序里用malloc申请的内存),从低地址向高地址生长。
栈(Stack):存放函数的局部变量、参数、返回地址,从高地址向低地址生长(核心!Pwn漏洞高发区)。
关键记住:栈的生长方向是“从高地址到低地址”,而push指令是“往栈里加东西”,所以push会让rsp减小;pop是“从栈里拿东西”,会让rsp增大。
2. 栈帧结构:Pwn解题的核心重点
每个函数执行时,都会在栈上开辟一块独立的空间,这就是“栈帧”。栈帧由rbp和rsp界定:rbp指向栈帧底部(固定),rsp指向栈帧顶部(动态)。
一个完整的栈帧(函数调用时)结构如下(从高地址到低地址,也就是从栈底到栈顶):
函数参数(比如调用func(a,b),a是第1参数,存在rdi;b是第2参数,存在rsi,同时会压入栈)。
返回地址(call指令压入的,是函数执行完后要回到的指令地址)。
老rbp(函数开头会执行push rbp,把上一个栈帧的rbp压入栈,方便返回后恢复)。
局部变量(函数里定义的变量,比如int x=5,会存在栈帧的这个区域)。
用通俗的图理解(简化版):
[高地址] 函数参数 → 返回地址 → 老rbp → 局部变量 [低地址]
↑ ↑ ↑ rbp+0x10 rbp+0x8 rbpPwn核心逻辑:栈溢出漏洞,就是让“局部变量”的输入超出分配的内存空间,覆盖到“返回地址”——把返回地址改成我们想执行的代码地址,程序就会按我们的意愿执行!
四、实战小任务:用GDB看栈帧,直观理解(手把手教)
光说不练假把式,我们用一个简单的C程序,通过GDB调试,直观看看栈帧结构:
1. 准备简单C程序(保存为test.c)
#include <stdio.h> void func(int a, int b) { int x = 10; // 局部变量 printf("a=%d, b=%d\n", a, b); } int main() { func(1, 2); // 调用func,参数1和2 return 0; } }2. 编译程序(关闭保护,方便调试)
终端执行命令:gcc -g -fno-stack-protector -z execstack -o test test.c
参数说明:-g加调试信息;-fno-stack-protector关闭栈保护;-z execstack允许栈执行(新手调试用)。
3. GDB调试,查看栈帧
启动GDB:gdb ./test
在func函数处下断点:b func
运行程序:r
查看栈帧信息:info frame → 能看到rbp、rsp的地址,以及返回地址;
查看栈内数据:x/20wx $rsp → 查看rsp开始的20个4字节数据,能看到局部变量x、老rbp、返回地址、函数参数a和b。
通过这个操作,你能直观看到栈里的返回地址、局部变量的位置——这就是后续栈溢出漏洞利用的基础!
五、新手避坑&学习建议
不要死记硬背:汇编指令和栈帧结构,结合GDB调试理解,比死记快。
先掌握64位:CTF中64位Pwn题占绝大多数,32位可以后续再补。
多调试简单程序:把刚才的test.c改一改(比如加更多局部变量、多嵌套几个函数),用GDB看栈帧变化,加深理解。
推荐工具:除了GDB,还可以用pwndbg(GDB插件,更友好),终端执行git clone https://github.com/pwndbg/pwndbg && cd pwndbg && ./setup.sh即可安装。
六、下期预告
今天我们了解了汇编基础和栈帧结构,这是后续学习栈溢出的核心前提。下期我们将进入最关键的实战环节:栈溢出基础与ret2text实战,第一次亲手构造payload,控制程序执行流!
如果今天的内容对你有帮助,别忘了点赞、在看,转发给一起学CTF的小伙伴~
CTF学习资源
网上虽然也有很多的学习资源,但基本上都残缺不全的,这是我们和网安大厂360共同研发的的网安视频教程,内容涵盖了入门必备的操作系统、计算机网络和编程语言等初级知识,而且包含了中级的各种渗透技术,并且还有后期的CTF对抗、区块链安全等高阶技术。总共200多节视频,100多本网安电子书,最新学习路线图和工具安装包都有,不用担心学不全。
🐵这些东西我都可以免费分享给大家,需要的可以点这里自取👉:网安入门到进阶资源