实验准备工作和踩坑总结(实验过程中有一些坑总结在前边:

准备工作:

  • 首先我们明确该实验缓冲区溢出的实现原理:
  • 漏洞就出在getbuf()函数中,代码如下:
  • int getbuf(){
        char buf[12];
        Gets(buf);
        return 1;
    }

    其中Gets()函数从输入设备读取字符串,用回车(/n)结束读取,但是没有上限!而在getbuf()函数中调用Gets()时,分配给Gets()的栈帧空间却是有限的,因此如果我们输入的字符串序列大于分配给Gets()的栈帧空间,就会发生缓冲区溢出,并覆盖掉getbuf()的返回地址、参数空间等等。

  • 本实验就以此展开。

1.首先,我将userid设置为:sxl,如图所示:

  1. c721172c4581409596c69b836cf1e8d9.png

因此可以得到专属于我的cookie:0x45f61b8d。

2.获取汇编代码txt文件:

因为在后续试验中我们要用到bufbomb的汇编代码,因此在终端依次输入命令:

(只是为了方便查看汇编代码,与实验进行无关~)

ssh username@10.92.13.8  连接到服务器

objdump -d bufbomb > 1.txt

scp username@10.92.8:1.txt 1.txt

就可以得到bufbomb汇编代码的txt文件(1.txt),方便观察。

3.工具使用:

gdb :准备调试程序,等同于先gdb,再file.

:为函数设置断点。

r -u cookie :GDB时以个人专属cookie运行bufbomb

s/n/si/c/kill:s即step in,进入下一行代码运行;n即step next。运行下一行代码但不进入。si即step instruction。运行下一条汇编/CPU指令;c即continue,继续运行直到下一个断点处。kill终止调试。

bt:bt是backtrace的缩写。打印当前所在函数的堆栈路径。

info frame :描写叙述选中的栈帧。

info args:打印选中栈帧的參数。

print :打印指定变量的值。

list:列出相应的源码

quit:退出gdb。

p/x $ebp: 以16进制输出ebp的值

4.执行命令,这里我是用了以下命令:

cat 0.txt | ./hex2raw | ./bufbomb -u sxl

这里0.txt中保存输入的字符序列,-u 之后则是个人专属cookie

踩坑:

1.首先就是level 0中注意:‘0a’是换行符‘/n’的ASCII,如果输入序列中有0a,记得及时变通(变通示例见下边level0)

2.搞清楚每个函数中,ebp的具体位置(详情见level1)

3.搞清楚leave指令和ret指令的具体操作:

leave:
       movl %ebp,%esp
       pop %ebp
ret:
       pop eip
  • 实验过程及分析(正式开始闯关)

Level 0Candle (10 pts)

  1. 这一关是在执行getbuf()函数后,并不继续进行原来的函数返回,而是转去执行smoke()函数,因此我们先查看一下smoke()函数的汇编代码

0ac34f8483054205be16f60d67d3d184.png

由上图可知,smoke()函数的入口地址为:0x08048e0a,因此在getbuf()函数ret时,跳转的地址就应该为0x8048e0a。

2.那我们再观察一下getbuf()函数的反汇编代码

1847022bd65249ac9e745815e094d624.png

由上图可知,getbuf()函数调用读取字符串函数gets()时,将getbuf()的指针地址(ebp-0x28)传给了gets(),并且由所学知识可知,当getbuf()函数返回时,return的地址所保存的地址为ebp+4,

如下图所示:

dc65bdd775d6464d884fc98713bcddc1.png

3.因此我们读入的字符串,先将0x28(40)+4(ebp大小)=44字节的地址用随意数据填满后,额外输入四位将返回地址覆盖掉,又因为小端法存储规则,因此我们将smoke()入口地址按照:0a 8e 04 08的顺序输入,就可以用smoke()入口地址0x08048e0a将getbuf()函数原先的返回地址覆盖掉,因此就可以实现函数在退出getbuf()函数时就会ret到smoke函数处执行smoke函数.

   

4.综上,我们首先读入下边字符序列(保存在0.txt中):

10 10 10 10 10 10 10 10 10 10 20 20 20 20 20 20 20 20 20 20 30 30 30 30 30 30 30 30 30 30 40 40 40 40 40 40 40 40 40 40 50 50 50 50 0a 8e 04 08

并使用命令:

cat 0.txt | ./hex2raw | ./bufbomb -u sxl

5.运行b93bcab47a2b4d8b98a67c595832bbdd.png

但是看图,发现不行,分析之后发现:因为0a=‘\n’,这是换行符('\n')的ASCII代码。当Gets()遇到这个字节时,它将假定您打算终止字符串。

因此重新考虑,从smoke()函数的入口地址的下一个地址进入

affa043f23a44787b8ba640367537cf3.png

也就是0x08048e0b,则更新的输入序列为:

10 10 10 10 10 10 10 10 10 10 20 20 20 20 20 20 20 20 20 20 30 30 30 30 30 30 30 30 30 30 40 40 40 40 40 40 40 40 40 40 50 50 50 50 0b 8e 04 08

将之保存在0.txt中并运行,如下图:

d2450ffec82549ed9638cafe1c4a53a9.png

c1207ff249e949668c795395418b264a.png

成功!

第Level1:Sparkler (10 pts)

  1. 与0关类似,此关任务是当执行getbuf()后,并不会返回到正常位置,而是会转去执行fizz代码,并且要将cookie值作为参数传递给fizz()函数。
  2. 因此我们先查看fizz()函数的汇编代码

可以看出其入口地址为:0x08048daf.

因此首先我们将返回地址覆盖

020141ee22d0451ca813cc0eea59b225.png

b07b001ee98143a785422f7ee0d37ffe.png

3.(难点)接下来探讨传递参数的情况,因为我们需要将cookie作为参数传递给fizz()函数,因此先观察fizz()函数中参数的传递情况

 31834774fbe44c30961aa655ade1a5cf.png

 从这一步可知,ebp+8的位置就是参数的传入位置,但是这里要注意(这里可能比较绕,多看会儿啊亲~)

此时(ebp+8)中的ebp是getbuf()函数执行leave操作后的ebp,又因为leave操作后(就是pop ebp后),又会进行ret操作(pop eip),且栈顶指针(esp)是会自动进行+4(esp+4)操作以还原栈帧。而在fizz()的汇编代码段中:

47c8620fb07c456bbf5b0e212f04f142.png

可以看出,push ebp后esp自己-4(esp+4-4),然后将esp的地址赋予ebp,因此可以得出:fizz()函数中,cmp指令中的ebp地址是getbuf()函数中ebp+4的地址。

综上:此时参数的正确地址就是getbuf()中的旧的ebp+4+8,我们只需要将参数放置到那里即可,下图分析:

8d14368f6f3d409bafd8146a46678595.png

 上边黑色注释总结了整个过程!

4.综上,首先我们用fizz()的入口地址将返回地址覆盖,然后用cookie值将fizz()的参数传入地址覆盖。

具体做法:先将44字节用除0a外的任意值填满,再将fizz()返回地址小端法传入(af 8d 04 08),再将cookie(8d 1b f6 45)传入参数区,也就是返回地址的上一个四字节处,因此传入为:

9b54b603d65041c1ad3bbb1d32e94dc0.png

5.运行:

51b877755b7040799cd73dfc84bab287.png

成功!

Level 2: Firecracker (15 pts)

  1. 目的:这一关是要执行栈中你指定的特殊指令,该指令的要求是通过编写汇编代码将一个全局变量赋值为cookie值,再去执行bang()函数.
  2. 首先查看一下bang()函数的入口地址

2522c586dd174eacab83972d92217eab.png

可以看到,bang()函数的入口地址为:0x08048d52。

3.接下来将global_value(全局变量)的值改为cookie(0x45f61b8d)

 (1)首先观察global_value的存储位置:

ef31a8fc631a43d8bb92ed9006db4305.png

因此可以得出:global_value存放地址为0x804d10c

 (2)写汇编代码进行修改global_value并且跳到bang()函数入口地址:

7695605c514244ac90fe0026f7ed044e.png

(注意:编译时将注释删掉)

保存为test.s汇编文件,编译之后用反汇编得到指令序列:

98c939724b3f442594d16e73c3e1d23e.png

因此可以得到修改global_value的指令序列为

 b8 8d 1b f6 45

b9 0c d1 04 08       

89 01

68 52 8d 04 08

c3

(3)很重要!因为在我自己的test.s汇编文件中,实现了对于global_value的修改和向bang()的跳转,又因为我们输入的字符串的起始地址为(ebp-0x28),因此getbuf()在执行完毕返回时,需要跳转到我们指令的起始地址(也就是此时的ebp-0x28),因此我们通过gdb调试获取(ebp-0x28)的绝对地址,如下图:

d05c170aacfb41149bff057cb45be229.png

因此可以看出:ebp存储为0x55683088

综上,分析情况见下图:

91c87a97243641d4bee2bd49317d069c.png

(4)2.txt文件为():

34b01e301c4f4c898f2dd80982cec6da.png

(5)运行:

0f8e1407b2a24c5e8c810e88f4ccfa3d.png

成功!

Level 3: Dynamite (20 pts)

1.目的:与之前几关有所不同,这一关是要修改getbuf()函数的返回值(正常状态为0x1)为你的cookie值,然后让函数正常返回到test(),注意恢复各个寄存器的状态,让test()察觉不到我们动了什么手脚。

2.首先明确函数调用和我们要进行操作的过程

任务一:test()在调用getbuf()之后正常返回到test(),也就是返回地址为call getbuf()的下一条指令地址(此处返回地址为0x08048e50)。

任务二:将getbuf()的返回值设置为自己的cookie(这里我的cookie为:0x45f61b8d),又因为返回值存储寄存器为eax,因此将cookie值赋值给寄存器eax即可。

综上:汇编代码编写就围绕上述两任务展开。

3.接下来来看一下test()代码得到任务一中所需的返回地址

9af754c85e314754a71f6fbf434636ff.png

从上图可以看得到:

汇编代码中压入栈的返回地址为:0x08048e50(call getbuf的下一条地址)

且给eax传入的cookie值为:0x45f61b8d(把你自己的cookie传入就行)

4.根据要求编写汇编代码:

如下:

558393faf8c34928bc321d194bd187a2.png

编译test3.s(gcc -c test3.s -o test3.o):

反汇编test3.o文件(objdump -d test3.o)得到指令序列:

ed9eaa3c35e14d9ea88cc3c6b7bd8582.png

 补充:编译.s文件和反汇编的指令为:

gcc -c test3.s -o test3.o

objdump -d test3.o

得到所需指令序列为:

    b8 8d 1b f6 45       

    68 50 8e 04 08       

    c3    

5.分析我们输入字符串的组成:              

 分析:

  1. 返回地址:因为在本关中,我们在进行缓冲区攻击时,首先是从(ebp-0x28)的地址开始读入字符串,读入时我们将需要执行的指令序列同样是以(ebp-0x28)为首地址进行输入,因此在读入字符串结束后,我们需要将(ebp-0x28)的地址作为返回地址,这样在读入字符串结束后,就会重新跳转到ebp-0x28的地址处,执行我们的汇编程序,而在汇编程序中,就会返回到test的正确地址(就是

47ff1b6873e6419a9442499a2efc35a9.png这一步啦~)

(2)(重点!)ebp的状态恢复问题(多看会儿这儿哦):

由(1)中的分析可以看出,在缓冲区攻击过程中,是将栈帧中保存的old_ebp覆盖掉了,而为了保证ebx的正确恢复,我们需要提前知道此时old_ebp的保存值并用一样的值进行覆盖(其实就是不想动它)。

综上,输入序列示意图为:

ebbe603d4c7e474e814f0aeec2bd8d3b.png

6.组成输入序列(3.txt):

(1)首先是汇编指令序列:b8 8d 1b f6 45 68 50 8e 04 08 c3   

(2)随意填充,足够40字节

b8 8d 1b f6 45 68 50 8e 04 08       

c3 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

(3)找到old_ebp的值并组成输入序列(难点就在这儿!

分析

GDB调试给getbuf()断点,并得到此时ebp的值:

b *0x08048e50  //给call getbuf的下一条地址断点
               //那为什么要在此处断点呢?
               //因为我们此时是要查看不进行缓冲区攻击时,ebp的正常值,因此断点在调用getbuf() 
                也就是call getbuf()下一条指令的地址处
r -u sxl       //执行

P/x $ebp  //16进制打印ebp的值

fe6e635050af4624a67f2be3d924e878.png

此时输入序列为:

b8 8d 1b f6 45 68 50 8e 04 08       

c3 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

e0 30 68 55

(4)将返回地址(ebp-0x28)写入输入序列

分析:

利用GDB调试找到(ebp-0x28)的绝对地址,如下:

40148f1e39804feb8e0554496bf150c1.png

此时输入序列为:

b8 8d 1b f6 45 68 50 8e 04 08       

c3 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

e0 30 68 55 88 30 68 55

7.测试运行:

73166b102e234ea4a22c7ca9a0c4365f.png

成功!            

Level 4: Nitroglycerin (10 pts)

1、问题分析:

    与level3及其类似,但是不同之处在于这里要用到新函数getbufn()和testn(),并且这一关的执行指令也有所变化,执行时要加上(-n)参数。

    而getbufn()和getbuf()的区别就是:getbuf()只调用一次,getbufn()则是被调用五次,并且我们要实现每一次getbufn()的返回值是cookie,且调用五次getbufn()之后,最终返回到的地址是testn()。

2、明确目标:

综上,我们的目标就有以下几步:

  1. 将每一次getbufn()的返回值改为个人专属cookie值。
  2. 将getbufn()调用五次后的返回地址设为testn()的入口地址
  3. 每一次调用后恢复ebp,以便下次成功调用。

因此,我们汇编代码的编写就以上述三个目标进行实现。

3、那就按部就班,首先查看一下getbufn()的汇编代码:

f8520cc987384f4ba4a1ea483338ba6e.png

分析:可以看出,该函数中给字符串输入的区域变得很大(缓冲区大小为0x208)。

4、再查看一下testn()函数,找到所需要的返回地址(也就是call getbuf()的下一条指令的地址):

2e051da1a4c744d0b14640244e026c97.png

可以看到,返回地址为:0x08048ce2。

因此汇编代码中,要入栈的地址为:0x08048ce2。

且汇编代码中给eax传入的cookie值为:0x45f61b8d(因为返回值是存储在eax中的)。

5、接下来讨论ebp的恢复问题(难点!这里多看看!

到此时,汇编代码中两项任务已经完成,还差一项也是区别于第三关的一项,那就是因为本关中调用getbuf()函数时栈的位置(ebp)会发生变化,因此每一次调用之后就要将之恢复。

其实这个操作我们已经比较熟悉了,就在第三关中我们也用到了这个操作,详情可以回顾一下第三关的解决办法(其实就是调试出call getbuf()之后的ebp的值,并在输入的字符序列中覆盖它让它维持稳定)。

但是这里显然不能这么做,因为我们要调用5次getbufn(),所以我们就找一个与getbufn()的ebp有关系的东西恢复它。显而易见,所谓有关系的东西就是它的长期合作伙伴esp啦,那我们就先看一下,在每一次的调用中,它两是啥关系,因此我们再来回顾一下getbuf()函数汇编代码:

106ace9abcfb4543a4c903635b0b7936.png

 可以看出ebp比esp大0x28,因此每一次调用getbufn()之后,就可以使用

esp+0x28=ebp来恢复ebp。

6、汇编代码实现

      综上,我们就可以实现我们需要的汇编代码:

 e67ca0da1b444d0295ad837f85a299c9.png

gcc -c test4.s -o test4.o //编译

objdump -d test4.o        //反汇编

得到指令序列:

1623b4f56cc64d4dbf6f76b178cea6c2.png

因此我们得到的指令序列为:

    8d 6c 24 28          

    b8 8d 1b f6 45       

    68 e2 8c 04 08       

    c3                   

         

7、从开始到这儿为止,我们分析实现了需要的汇编代码,并得到了对应的指令序列,那接下来让我们完成最后一步吧!(胜利就在眼前,奥利给冲冲冲!),组成我们的输入字符串序列:(一步步来喔,别急~)

首先确定指令序列为: 8d 6c 24 28 b8 8d 1b f6 45 68 e2 8c 04 08 c3  

那为了在输入字符串之后,成功执行我们的指令序列,我们就需要将getbuf()函数的返回地址覆盖为我们输入的指令序列的开头位置(就是为了正确执行我们的汇编代码)

所以GDB调试出五次调用中最大的输入首地址:

b getbufn()

r -n -u sxl     //注意加上-n

p/x ($ebp-0x208)

c               //continue

96aa66a8019445fa96c17b4eea1801af.png

见上图红色标注,五个地址中,最大的地址为:0x55682ed8。

所以这里就得到了最后所需的覆盖返回地址了。

注意:

这里要分清我们要覆盖的返回地址和在汇编代码中入栈的返回地址的区别:

  1. 要覆盖的返回地址:是为了执行我们输入的指令序列,因此使用五次调用getbuf()时最大的字符输入首地址将之覆盖。
  2. 入栈的返回地址:正常返回到testn()的真正返回地址。

所以说,执行顺序为:

读入字符串序列—>然后返回到我们读入的指令序列的首地址—>执行我们输入的指令序列(在这个指令中就实现了汇编代码中的操作)。

8、组成最终的输入字符串

     因为在-n环境下运行,缓冲区大小为0x208(520),因此前520+4字节用指令序列和nop(90)填充,最后四字节用返回地址覆盖,如下图:

9b8e485f1ddf4bea8802cc68f82ea501.png

因此可以得出,最终的输入字符串为:

39a406d68ce7411c8ba1b1897472aae6.png

运行:

cat 4.txt | ./hex2raw -n | ./bufbomb -n -u sxl

 注意:指令有变,记得加-n。8bc9e3a32c3c4ec6b6a68ca4132b5b0d.png

成功!芜湖!

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐