资讯动态

Pwn入门必看:汇编基础与Linux内存模型全拆解

发布时间:2026/10/23 14:20:03 来源:尧图企业网站定制
CTF Pwn模块系列分享二汇编基础Linux内存模型拆解上一篇文章写了Pwn环境搭建和工具链配置当时就有朋友在评论区说环境是搭好了pwndbg也装上了但打开第一道入门题反汇编窗口一拉出来满屏的mov、push、pop看得懂单词看不懂程序。更别提什么栈溢出、返回地址、PIE、ASLR题解里每个字都认识连起来就不知道在说什么。这个状态我太熟了。Pwn题目表面上考的是漏洞利用实际上考的是两样东西你能不能读懂汇编你能不能在大脑里构建出程序运行时的内存布局。这两样不过关刷再多题都是被题解牵着走——看完这题会了换个壳又不会了。这篇文章就是系列第二篇专门把汇编基础和Linux内存模型这两块地基拆开揉碎讲清楚原理再带你用GDB亲手验证一遍。适合刚学完C语言、知道变量和函数是怎么回事但没接触过底层的朋友。看完你至少能自己看懂一段反汇编代码理解栈帧结构明白为什么覆盖返回地址能劫持程序流程。为什么Pwn的第一步是啃汇编和内存1.1 看不懂汇编的那一刻你已经落后于题解了很多人的入门路径是这样的看writeup下载题目附件checksec看保护然后直接拖进IDA。IDA确实能还原伪代码__readgsqword、sub_4006B6这些也能看个大概。但问题在于真正的漏洞点往往不在伪代码里而在内存布局的细节里。举个例子一道栈溢出题伪代码里就看到一个read(fd, buf, 0x100)buf的大小只有0x20。如果你不懂栈你就不知道多读的那0xE0个字节会覆盖到栈上的什么东西更不知道为什么要精确地覆盖到偏移0x28处才能劫持到返回地址。这些知识伪代码不会告诉你IDA的绿色高亮也不会告诉你只有当你理解函数调用时栈上到底发生了什么才能答得出来。说句不那么中听的话如果看不懂汇编和内存模型你连题解都看不懂——不是看不懂攻击思路而是看不懂攻击思路为什么能成立。1.2 Pwn的本质在代码的内存剧本里做文章我们不妨换一个视角来看待程序。一个程序运行起来本质上是CPU按照指令一条一条地执行这些指令操作的数据都存放在内存里。内存就是程序演出的舞台指令是剧本寄存器是演员手中的道具栈是舞台的一块特定区域堆是另一块。你写C代码的时候编译器已经帮你安排好了这一切。而Pwn攻陷的本质就是发现程序里某个环节出现了编剧没写到的操作——比如输入了超出预期的数据、访问了不该访问的内存、用错误的方式处理了数据——然后借助这个错误把你自己的数据写进关键位置从而篡改后续的剧本。想做到这一点你必须清楚这个舞台上各个区域的位置、大小、作用以及哪些位置是一碰就炸的关键枢纽比如返回地址。这就是为何我要把汇编和内存模型放在同一个章节里讲汇编描述的是数据的流动方式内存模型描述的是数据的存放位置。两者合在一起你才算真正认识一个正在运行的程序。1.3 这篇分享的阅读起点我需要先说明这篇内容假设你具备什么基础免得有人看到一半觉得太浅或者跟不上会写基本的C语言理解变量、数组、函数的概念知道指针是什么在Linux命令行下能编译一个C文件比如gcc -o test test.c对寄存器、栈、堆这些词有模糊概念但没系统地梳理过不要求任何Pwn基础也不要求汇编基础。如果你满足以上条件这篇内容就是为你准备的。我们用十几个最核心的指令、一张内存布局图、一次动手实验把Pwn最底层的地基打牢。汇编基础先把寄存器家族认个脸熟2.1 x86与x64同样的名字不同的位数在Linux下做Pwn你打交道的基本只有两种架构x8632位和x6464位。第一种寄存器是32位的名字以E开头比如EAX、EBX、ECX、EDX、ESI、EDI、EBP、ESP、EIP第二种寄存器是64位的名字以R开头比如RAX、RBX、RCX、RDX、RSI、RDI、RBP、RSP、RIP并且多了R8到R15这8个额外的通用寄存器。这个区别不只是名字的变化。x64下参数传递更宽松运算能力更强所以编译器生成的代码风格也会不同。为了便于理解后面的内容我会以x64为主因为在目前的CTF比赛中绝大多数Pwn题都是64位程序掌握x64比掌握x86更有现实价值而x86的概念在理解了x64之后再去看会非常简单。我把常见的寄存器列了个表建议先混个脸熟不要死记后面用的时候会不断重复。寄存器x86名称x64名称主要用途攻击者视角累加器EAXRAX函数返回值、运算结果函数调用后看看RAX里装着什么很多信息泄露从这里来基址指针EBPRBP当前栈帧的底部栈帧结构的关键标记很多题靠它定位局部变量栈指针ESPRSP当前栈顶永远盯住它它是栈上的游标指令指针EIPRIP下一条要执行的指令地址劫持它就能劫持程序流程这是Pwn的终极目标参数寄存器-RDI/RSI/RDX/RCX/R8/R9x64下传递前6个参数找gadget时经常遇到比如pop rdi; ret额外寄存器-R8-R15通用偶尔出现在gadget里有几个约定需要单独强调RBP是栈帧的底部标记它配合RSP可以确定当前函数的局部变量区域在哪RIP是CPU要执行的下一条指令地址正常情况下你无法直接修改它但如果你能控制栈上被ret弹出的数据就是间接控制了RIP。攻击者费尽心思做的很多事最终目的只有一个让RIP指向攻击者想要执行的代码。2.2 Pwn里最高频的七类指令够用但别贪多很多教材讲汇编会把几十条指令全列一遍看得人昏昏欲睡。其实Pwn入门阶段你用到的指令不超过二十条。真正高频的我总结为七类。第一类mov传送数据。mov rax, rbx ; 把rbx的值赋给rax mov rax, [rbp-0x10] ; 把rbp-0x10处内存里的值赋给rax mov [rbp-0x10], rax ; 把rax的值存入rbp-0x10处内存注意区分两种操作不带中括号是对寄存器/立即数直接操作带中括号表示访问内存。很多新手混淆寄存器里存的地址和这个地址指向的内存内容。在Pwn中你要知道一条mov是在搬数值还是在搬内存里的数据。第二类lea加载有效地址。lea rax, [rbp-0x20] ; 计算出rbp-0x20这个地址本身存入raxlea不访问内存只做地址运算。看到lea rax, [rbp-0x20]你可以直接理解为rax rbp - 0x20它经常用来把某个局部变量的地址装进寄存器为后续的函数调用准备参数。这一点在逆向时非常有用看到lea说明接下来大概要拿地址做文章了。第三类push和pop入栈/出栈。push rax ; 先让rsp - 8再把rax的值写到rsp指向的新位置 pop rax ; 先读取rsp指向位置的值再让rsp 8记住那句经典口诀push先减后写pop先读后加。栈从高地址向低地址生长所以入栈会让栈指针变小这一点后面讲内存模型时会详细展开。第四类call和ret函数调用与返回。call 0x401000 ; 把当前指令的下一条地址压栈然后跳转到0x401000 ret ; 从栈顶弹出一个地址跳转过去call做的事情可以拆成两步先把返回地址call指令的下一条指令地址压入栈再跳转到目标地址。ret则是反过来从栈顶弹出地址跳转过去。这个机制是整个栈溢出攻击的核心——如果你能修改栈上存储的返回地址ret之后程序就去了你指定的地方。第五类leave函数返回清理栈帧。leave ; 等价于 mov rsp, rbp; pop rbp函数退出前常用的指令。先将栈顶恢复为当前栈帧底部再弹出旧的RBP值这样调用者的栈帧就被恢复回来了。理解了leave你就能理解为什么很多函数的尾部是leave; ret连写。第六类add和sub加减运算。sub rsp, 0x20 ; 栈上分配0x20字节的局部空间rsp减少0x20 add rax, 1 ; rax加1在函数开头你经常能看到sub rsp, 0xXX这就是给局部变量腾地方。这里要留意栈对齐的问题x64下的System V调用约定要求在执行call之前rsp按16字节对齐所以你会看到有时候明明是0x18就够用的局部变量编译器偏偏给你分配0x20就是为了对齐。如果你自己写exp模拟函数调用时破坏了对齐某些用到SSE指令的函数如movaps会直接崩溃。第七类cmp和test比较与跳转。cmp rax, rbx ; 比较两者 jz 0x401020 ; 相等则跳转 test rax, rax ; 按位与用于判断是否为0cmp只改标志位真正的分支靠后面的条件跳转指令实现常见的包括jz/jnz相等/不等跳转、jg/jle有符号大于/小于等于跳转、ja/jbe无符号大于/小于等于跳转。在逆向时看到这类指令组合你就能还原出源码里的if或循环。说句实在话上面这七类指令已经能覆盖你解Pwn入门到进阶80%以上的汇编阅读需求。不要贪多遇到不认识的指令时gdb里用help或者上网查瞬时补上就行关键是把这几类指令和它们对内存的影响刻进脑子里。2.3 ATT语法和Intel语法的人格分裂问题新手最容易踩的坑之一为什么objdump里看到的汇编和IDA里看到的不一样在IDA里是mov rax, rbx在gdb里却变成了mov %rbx, %rax而且操作数顺序还反了。这其实是两种汇编语法的问题。IDA默认显示Intel语法操作数顺序是目的在左源在右而gdb默认配置的objdump指令输出常常是ATT语法操作数顺序是源在左目的在右寄存器前还要加%立即数前加$。我在入门时就被这个坑搞得很烦。后来我养成了一个习惯不管用哪个工具先把显示统一成Intel语法。gdb里执行set disassembly-flavor intel或者直接在~/.gdbinit里写上这一行之后disassemble出来的就永远是Intel风格了。pwndbg这类插件通常也会自带配置你可以在启动后确认一下。这里给新手一个明确建议以Intel语法为准去看汇编因为绝大多数CTF的writeup、IDA、pwndbg插件都默认显示Intel语法你学这一套就够用了。等以后读一些老文章遇到ATT语法再临时转换也不迟。函数调用现场栈帧与返回地址是怎么被安排的3.1 栈这个结构为什么天然适合函数调用栈stack是一种后进先出的数据结构。程序运行时的调用栈call stack利用的正是这个特性函数A调用了函数BB执行完返回后A可以无缝接着往下执行。为什么天然适合因为函数调用天然就是一个先进后出的过程——先进入的函数要等后进入的函数执行完才能结束所以用栈来保存每一层函数的现场再合适不过。栈在虚拟内存中从高地址向低地址生长。每次push栈顶RSP保存的地址减小每次pop栈顶增大。很多人第一次听栈向下生长会觉得反直觉其实你只需要把它想象成一个从高地址开始、往低地址方向倒扣的容器存放的数据越多容器的底部就越往低地址压。这个方向决定了你在计算偏移时的参照系一定要在心里建立起来。3.2 一次完整的call调用到底发生了什么我把一次函数调用的全过程拆成五步每一句都要看懂假设funcA正在执行调用funcB第一步caller准备参数。在x64下参数依次放入RDI、RSI、RDX、RCX、R8、R9参数超过6个才放到栈上。这一步由调用者来做。第二步call指令压下返回地址。call funcB时CPU会先把call指令的下一条指令的地址即funcA中调用点之后的返回位置压入栈中然后跳转到funcB的入口。此时栈顶就是返回地址。第三步callee执行序言prologue。funcB开头一般长这样push rbp ; 保存调用者的RBP压栈 mov rbp, rsp ; 把当前栈顶当作新栈帧底部rbp rsp sub rsp, 0x20 ; 为局部变量分配空间执行完后从RBP到RSP之间的区域就是funcB的栈帧局部变量、临时数据都放在这里。第四步函数体执行。代码在栈帧内读写局部变量比如mov [rbp-0x8], rax就是往局部变量槽里写数据。第五步函数返回。函数结尾通常长这样leave ; mov rsp, rbp; pop rbp ret ; pop 返回地址跳转回去leave先把栈顶恢复到RBP位置再弹出旧RBP这样RSP恰好指向返回地址。紧接着ret从栈顶弹出返回地址并跳转程序回到funcA继续执行。到这里一次调用完成。这个流程的理解太重要了。尤其是第二步和第五步返回地址在栈上ret依赖栈顶数据跳转。如果你在函数体内有缓冲区溢出又能写入栈帧内部的局部变量区域那么多写进去的数据就会顺着栈帧一路往上盖最终盖到返回地址上。等函数执行ret时CPU从栈顶弹出一个被攻击者控制的地址程序跳转过去——这就叫栈溢出控制RIP。3.3 调用约定x64的寄存器传参和x86的栈传参调用约定calling convention规定了函数参数怎么传、返回值放哪里、寄存器由谁保存。做Pwn不一定要求背全但两个最关键的必须清楚。x64下的System V调用约定Linux默认参数依次放入RDI、RSI、RDX、RCX、R8、R9多出的参数从右到左压栈返回值放在RAX中RBP、RBX、R12-R15由被调用者负责保存即被调函数如需使用必须先保存原值返回时恢复RDI、RSI、RDX、RCX、R8、R9、RAX、R10、R11为调用者保存寄存器意味着被调函数可以随意使用返回时无需恢复栈对齐在call指令执行前RSP需要按照16字节对齐。x86下的cdecl调用约定32位常见所有参数从右到左压栈传入返回值在EAX中函数返回后由调用者清理栈上的参数。区别一句话总结64位优先用寄存器传参32位几乎全靠栈传参。这也是为什么你在64位题目里经常需要找pop rdi; ret这样的gadget——因为攻击目标函数需要参数你先要把参数装进RDI再跳过去。3.4 从攻击者角度重新看这个流程把上面这个正常的流程用攻击者的眼睛再看一遍当funcB执行leave; ret的时候RSP正好指向返回地址。攻击者如果能通过某个漏洞在返回地址所在位置之前写入一串精心构造的数据让返回地址变成攻击者想要的地址那么ret的一瞬间RIP就落到了攻击者指定的位置。接下来会发生什么就看攻击者想干什么了可能是跳到一个后门函数ret2win可能是跳到一段ROP链的起点ret2libc/ret2syscall也可能是跳到shellcode。整个Pwn入门最核心的悟了瞬间就是你意识到函数调用栈上那个被保存的返回地址是程序信任你、没有任何防御机制验证的软肋。普通程序觉得它只会被正常调用路径覆盖攻击者却说不我来帮你覆盖。Linux进程的虚拟内存地图从.text到.stack4.1 进程眼里有一整块私有领土我们常说程序运行起来是一个进程。这个进程看到的地址空间是一个从0到0x7fffffffffff64位下用户态的默认最大值的连续虚拟地址空间。它以为自己独享了这块极大的内存但实际上物理内存是共享的CPU通过MMU内存管理单元把虚拟地址翻译成物理地址。这个机制带来的好处很多比如进程之间互不干扰、一个进程崩溃不需要牵连别人、可以用虚拟内存的方式管理文件的映射等。对Pwn选手来说虚拟内存的意义在于每个区域有固定的相对位置和默认属性攻击者利用漏洞时总要考虑这个目标地址位于哪个段那个段是否可执行这个段会不会被随机化。你需要先把这个大洲的地形摸清楚才能判断哪条路能走。4.2 从低地址到高地址每个段的角色64位Linux进程的用户态虚拟内存布局从低到高大致如下表区域大概位置存放内容攻击者关注点.text代码段低地址附近程序机器指令只读可执行是函数地址所在地.rodata低地址附近只读常量字符串经常存flag、Welcome提示等.data / .bss低地址附近已初始化全局变量 / 未初始化全局变量可读写有时存放全局结构体堆heap低地址向高地址生长动态分配的内存malloc/free堆利用的战场低地址往上长mmap映射区堆上方共享库、内存映射文件libc基址在这里ret2libc要leak它栈stack高地址附近局部变量、返回地址、函数调用现场向下生长栈溢出的主战场内核区最高地址内核代码与数据普通用户态程序不可接触有两个细节需要注意第一栈和堆的生长方向是反着的栈从高地址往低地址长堆从低地址往高地址长。两者一个往下一个往上中间隔着大片未映射的区域。如果某种漏洞让堆块溢出越过边界就可能污染到堆顶之上的其他数据栈溢出的内容则往高地址方向覆盖覆盖到储存返回地址的位置。第二.text段在内存里默认是可执行但不可写的。这意味着你不能直接修改代码段但可以跳过去执行它。如果你想让进程执行你自己写的代码要么把shellcode放到可执行的内存区域要么利用libc里现成的函数片段这就是后面会展开的ret2libc之类的技巧。4.3 地址随机化PIE、ASLR、RELRO这些名词一次理清很多新手在checksec阶段就被一堆缩写吓住这里我挑几个和内存模型强相关的讲明白。ASLR地址空间布局随机化是操作系统层面的机制。开启之后每次启动程序栈基址、堆基址、共享库基址都会随机变化。换句话说你在GDB里看到的栈地址、libc地址下次运行就可能变。Pwn中绕ASLR的基本思路是先泄漏后利用通过漏洞先读出某个随机地址再算出目标地址再实施攻击。PIE位置无关可执行文件是编译层面的机制。如果程序开启了PIE就连它自身的代码段、数据段基址都会被随机化每次运行程序的基址都不同关闭PIE通常是-no-pie编译时程序加载在固定地址比如0x400000附近代码里的函数地址就是固定的攻击者可以直接拿writeup里的地址用。因此Pwn题里checksec看到PIE未开启通常意味着思路能简单不少。RELRO重定位只读和内存模型也有关Full RELRO会把GOT全局偏移表设为只读防止攻击者改写GOT表项Partial RELRO则保留一定的可写空间。这部分在入门阶段先知道名字就行等做到格式化字符串、GOT劫持题时再深挖来得及。这串机制背后有一个核心逻辑防御者让内存里每个关键位置的地址变得不可预测攻击者要利用就必须先让信息泄露配合而泄露和利用这两步恰好构成Pwn题最常见的两个阶段。理解了这个博弈逻辑你看任何中长期题目都会更有方向感。动手验证用GDB把内存模型变成眼睛能看见的东西5.1 准备一个最简单的实验程序光看理论容易飘我们动手做个小实验。打开终端新建一个test.c#include stdio.h int add(int a, int b) { int sum a b; return sum; } int main() { int x 3; int y 4; int result add(x, y); printf(result: %d\n, result); return 0; }编译时注意关闭PIE和栈保护便于观察gcc -o test test.c -no-pie -fno-stack-protector然后启动gdbgdb ./test5.2 反汇编main函数观察寄存器变化在gdb中输入set disassembly-flavor intel disassemble main你会看到类似这样的输出地址可能略有差异Dump of assembler code for function main: 0x0000000000401136 0: push rbp 0x0000000000401137 1: mov rbp,rsp 0x000000000040113a 4: sub rsp,0x20 0x000000000040113e 8: mov DWORD PTR [rbp-0x14],0x3 0x0000000000401145 15: mov DWORD PTR [rbp-0x10],0x4 0x000000000040114c 22: mov edx,DWORD PTR [rbp-0x10] 0x000000000040114f 25: mov eax,DWORD PTR [rbp-0x14] 0x0000000000401152 28: mov esi,edx 0x0000000000401153 29: mov edi,eax 0x0000000000401155 31: call 0x401120 add 0x000000000040115a 36: mov DWORD PTR [rbp-0x8],eax 0x000000000040115e 40: mov eax,DWORD PTR [rbp-0x8] 0x0000000000401161 43: mov esi,eax 0x0000000000401163 45: lea rdi,[rip0x2e9a] 0x000000000040116a 52: mov eax,0x0 0x000000000040116b 53: call 0x401040 printfplt 0x0000000000401170 58: mov eax,0x0 0x0000000000401175 63: leave 0x0000000000401176 64: ret End of assembler dump.几处值得注意push rbp; mov rbp, rsp; sub rsp, 0x20就是函数的序言[rbp-0x14]和[rbp-0x10]是局部变量x和ymov esi, edx和mov edi, eax正是在为调用add准备参数——x64下第一个参数放RDI第二个放RSI。这样汇编和源码就对上了。5.3 单步执行亲眼看看栈的变化在main的call add那一行下一个断点break *0x0000000000401155 run info registers rsp rbp先看进入add之前的寄存器。然后单步一次si info registers rsp rbp你会发现RSP减小了8RBP还未变化。再执行几次进入add的序言后RBP变成了当前RSP之后sub rsp, 0x10分配局部变量区。这个时候用x/20gx $rsp观察栈内存你能看到从栈顶往下依次是局部变量区、旧RBP、返回地址、调用者局部变量。返回地址就是call之后那句mov DWORD PTR [rbp-0x8], eax的地址也就是0x000000000040115a。这个实验的本质在于把抽象的返回地址被压栈变成你在GDB里肉眼可见的具体内存内容。你甚至可以直接用set {unsigned long}($rsp) 目标地址来手工修改栈上的返回地址然后执行ret看看RIP会不会变成你指定的值——当然自己实验可以别拿真实程序开玩笑。5.4 查看进程的内存映射理解各段位置在gdb里执行info proc mappings或者装了pwndbg的话直接vmmap你会看到类似这样的输出Start End Permissions Name 0x0000000000400000 0x0000000000401000 r--p ./test 0x0000000000401000 0x0000000000402000 r-xp ./test 0x0000000000402000 0x0000000000403000 r--p ./test 0x0000000000403000 0x0000000000404000 rw-p ./test 0x00007ffff7dd1000 0x00007ffff7df6000 r--p /lib/x86_64-linux-gnu/libc.so.6 ...省略 0x00007ffffffde000 0x00007ffffffff000 rw-p [stack]注意最低地址处几个相邻的区域就是上文的.text、.rodata、.data、.bss这些段因为-no-pie所以起始地址固定在0x400000附近。上方大块的libc.so.6是动态链接进来的共享库它的起始位置就是之后ret2libc攻击时你想要的libc基址。最高处的[stack]就是栈区。你自己动手跑一遍之后这张内存地图就会刻进脑子里比背十遍表格都管用。新手常踩的坑和几个学习建议6.1 最容易混淆的三个知识点带过好几个刚开始学Pwn的朋友我发现有几个点反反复复地绊人拿出来单独说。第一栈顶地址的方向感混乱。不少人画栈帧图时把栈顶在最高地址画反了。记住口诀栈顶RSP是低地址栈底RBP是高地址。sub rsp, 0x20表示栈顶往下走即分配空间。每次看到指令先把RSP和RBP谁大谁小理清楚后面所有偏移量计算才不会乱。第二返回地址与RBP的混淆。上文的栈帧里从当前RBP往上高地址方向偏移8个字节保存的是旧的RBP再往上偏移8个字节才是返回地址。很多新手写exp时把覆盖偏移算到了RBP的位置结果只覆盖了RBP程序的流程没被劫持只在函数返回时报错。记住需要控制的是返回地址不是RBP。第三call和ret不是普通的跳转。call不仅要跳转还要压返回地址ret不仅要跳转还要弹出地址。如果只用jmp的思维去理解它们你永远想不明白为什么栈上多了8个字节、为什么ret能改变RIP的方向。6.2 不要一开始就扎进堆利用Pwn的方向很多栈溢出、格式化字符串、整数溢出、堆利用、内核利用往外还有各种奇怪的利用方式。我的建议是入门阶段先把精力集中在栈方向和格式化字符串上。原因很简单它们的利用模型相对清晰调试器里容易观察而且几乎覆盖了Pwn最核心的思维方法——理解内存布局、控制关键指针、绕过保护。堆利用牵扯到更复杂的内存管理机制、bin结构、堆风水如果你连栈帧都没看明白就冲进去大概率会被各种chunk结构直接劝退。等你把栈方向的ret2win、ret2shellcode、ret2libc、ROP基础链都刷过一遍再回头看堆利用会轻松得多。6.3 工具链与调试习惯的实战建议GDB插件强烈推荐pwndbg它在GDB基础上加了vmmap、heap、cyclic、checksec等命令省掉大量手动操作。安装时注意和GDB的版本匹配遇到启动报错优先看Python版本兼容问题。遇到地址计算尽量用Python的pwnlibpwntools来做。不要手算偏移用cyclic生成模式串、cyclic_find找偏移又快又不容易错。每次做题我先checksec在心里快速过一遍保护情况再看反汇编时心里会有一个哪些地址可以利用、哪些地址需要泄露的大致预期。养成这个习惯效率比见题就莽高得多。调试时多用x/系列命令打印内存、info registers看寄存器、contextpwndbg看上下几行汇编。看到内存布局之后不要急着跳过自己口头复述一遍这里是什么、为什么这个值被存在这比看十篇writeup都巩固。我在实际带新手的过程中发现几乎所有人卡住的点都不是不会写exp而是exp里每一个变量代表内存里哪一块内容没想清楚。所以这篇文章最后再分享一个大实话学Pwn慢就是快。花两个晚上亲手在GDB里把栈帧拆开、把虚拟内存地图看明白比刷十道题却每道都靠照抄题解要有用得多。等你真正能在脑子里运行程序的内存模型后面再学ROP、堆利用、格式化字符串都会有一种开窍的感觉。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑