Linux基础与汇编入门:从命令行到栈帧
各位同学,欢迎来到CTF Pwn的第一站。很多人觉得Pwn的门槛高,其实说白了,就是两件事:看懂程序在干什么,以及找到控制程序的方法。而这两件事,都离不开Linux基础和汇编知识。
我个人习惯把这一章叫做“地基”。地基打不牢,后面堆栈溢出、ROP链什么的,你学起来会非常痛苦。我见过太多选手,漏洞原理讲得头头是道,结果连chmod和chown都分不清,调试时连寄存器都看不懂——嗯,那基本就是白给。
Linux命令行与权限管理
CTF的Pwn题目,99%跑在Linux上。你至少得会这几样:
- 文件操作:
ls、cd、cp、mv、rm、cat、file——别笑,我真见过有人用cat读二进制文件然后终端卡死的。 - 权限管理:
chmod、chown、umask。Pwn题经常给你一个flag文件,权限是400,只有特定用户能读。你提权不了,就看不到flag。 - 进程管理:
ps、top、kill、strace、ltrace。调试时strace能帮你看到程序调用了哪些系统调用,是不是在偷偷读flag。 - 网络工具:
nc、ncat、socat。远程打Pwn全靠它们。
权限模型速记:Linux权限分三组——所有者(u)、所属组(g)、其他人(o)。每组三个位:读(r=4)、写(w=2)、执行(x=1)。chmod 755 file就是所有者rwx,组和他人r-x。
我的小技巧:拿到一个陌生二进制,先跑file看架构,再跑checksec看保护。这是Pwn的“望闻问切”。
x86/x64汇编基础
汇编是Pwn的“母语”。你不一定要能手写汇编,但一定要能读懂。我刚开始学的时候,对着mov eax, 0和xor eax, eax纠结了半天——为什么清零要用两种写法?后来才知道,xor的机器码更短,是编译器优化的小把戏。
核心寄存器,你记住这几个就够了:
| 架构 | 通用寄存器 | 特殊寄存器 |
|---|---|---|
| x86 | eax, ebx, ecx, edx, esi, edi, ebp, esp | eip(指令指针) |
| x64 | rax, rbx, rcx, rdx, rsi, rdi, r8~r15 | rip(指令指针) |
为什么x64多了r8~r15?因为64位系统需要更多寄存器来传参,减少内存访问。嗯,这就引出了函数调用约定。
函数调用约定
调用约定,说白了就是“函数之间怎么传参数、怎么收尾”的一套规矩。不同平台、不同编译器,规矩不一样。Pwn里最常碰到的有三种:
cdecl(32位Linux默认)
- 参数通过栈传递,从右向左压栈。
- 调用者负责清理栈。
- 返回值放在
eax。
fastcall(32位Windows常用)
- 前两个参数用
ecx和edx传递,其余走栈。 - 被调用者清理栈。
System V AMD64(64位Linux默认)
- 前6个整数参数依次放在
rdi、rsi、rdx、rcx、r8、r9。 - 多余的参数走栈。
- 返回值放在
rax。
我曾经踩过的坑:有一次打一个64位Pwn题,我ROP链里传第7个参数,直接往栈上怼,结果程序崩了。后来才发现,64位下前6个参数走寄存器,第7个才开始走栈。我漏掉了这个细节,浪费了整整一个下午。
栈帧结构
栈帧是函数调用的“现场记录”。每次调用一个函数,系统都会在栈上分配一块区域,用来存局部变量、保存的寄存器、返回地址等等。你想想看,如果这块区域被破坏了——比如你输入了一个超长的字符串——那返回地址就可能被覆盖成你想要的任意地址。这就是最经典的栈溢出。
一个典型的栈帧长这样:
高地址
+------------------+
| 调用者的栈帧 |
+------------------+
| 返回地址 | ← 被溢出覆盖的目标
+------------------+
| 保存的 ebp/rbp | ← 旧基址指针
+------------------+
| 局部变量 |
| (缓冲区等) | ← 输入点
+------------------+
低地址
为什么返回地址在“上面”?因为栈是向下增长的。每次push,esp/rsp减小;每次pop,esp/rsp增大。所以局部变量在低地址,返回地址在高地址。你从低地址往高地址写数据,就能一路写到返回地址。
核心逻辑:栈溢出利用的本质,就是用输入数据覆盖返回地址,让程序跳转到我们指定的位置(比如shellcode或ROP链)。
下面这张图,把本章的知识体系串起来了:
你看,Linux命令行是操作环境,汇编是理解程序的语言,调用约定和栈帧是漏洞发生的舞台。这三者缺一不可。
我的建议:学完这一章,你可以自己写一个简单的C程序,然后用gcc -S生成汇编,对照着看栈帧结构。亲手做一遍,比看十遍书都管用。