深入理解计算机系统——LAB-4_Buf_Lab
简介
缓冲区溢出实验。
要求学生们通过利用一个缓冲区溢出漏洞,来修改一个二进制可执行文件的运行时行为。
这个实验教会学生们栈的原理,并让他们了解到写那种易于遭受缓冲区溢出攻击的代码的危险性。
实验环境和使用工具
主系统
Windows10
子系统
Windows Subsystem for Linux kali
gdb10.2.1
版本objdump 2.35.2
版本VS code
编辑器,下载了x86 and x86_64 Assembly
插件,提供汇编代码高亮功能
实验过程
准备过程,查看buflab-writeup.pdf
一共有三个二进制可执行文件:
bufbomb
:你将攻击的缓冲区炸弹程序通过
int getbuf()
函数从输入中读取字符串1
2
3
4
5
6
7
8/* Buffer size for getbuf */
int getbuf() {
char buf[NORMAL_BUFFER_SIZE];
Gets(buf);
return 1;
}- 容量为32字符的
buf
字符数组 - 其中调用
Gets(buf)
读取字符串(以\n
或文件结束符作为终止符),并将其连同终止符存储到一个目标地址(buf
数组中) - 如果字符串长度<=31个字符则正常返回1,反之则返回错误
- 容量为32字符的
bufbomb
有几个命令行参数:-u userid
为指定的userid操作炸弹-h
打印命令行参数列表-n
在”Nitro”模式下操作,在Level 4中使用。-s
将解决方案提交给评分服务器
makecookie
:根据userid(用户id)生成一个“cookie”用法示例:
1
2unix> ./makecookie bovik
0x1005b2b7cookie是由8个十六进制数字组成的字符串,不出意外与userid一一对应
在五次缓冲区攻击中的四次攻击中,我们的目标是将cookie放在他一般不该出现的地方。
hex2raw
:一个帮助在字符串格式之间进行转换的程序输入两个一组的16进制数字以空格或者换行符分隔,也就是16进制格式的exploit string,转换为ASCII码格式字符串。支持c语言风格的注释
你能通过设置一系列管道去通过
hex2raw
传递字符串:1
unix> cat exploit.txt | ./hex2raw | ./bufbomb -u bovik
你能存储原始字符串在一个文件,然后通过使用I/O重定向去提交给
bufbomb
:1
2unix> ./hex2raw < exploit.txt > exploit-raw.txt
unix> ./bufbomb -u bovik < exploit-raw.txt在GDB调试内部也能使用这种方法:
1
2unix> gdb bufbomb
(gdb) run -u bovik < exploit-raw.txt注意
0x0A
是换行符\n
,它会终止字符串读取,所以不能包含0x0A
。- 如果要创建字符串
0xDEADBEEF
,我们应该传递EF BE AD DE
接下来是五个Level的说明
Level 0: Candle(10 pts)
Level 0说明:
getbuf
函数被test
函数调用,test
函数C语言代码如下:
1 | void test() { |
函数调用顺序是test()->getbuf()->test()
,我们的任务是将其改变为test()->getbuf()->smoke()
,smoke()
函数代码如下:
1 | void smoke() { |
作者给出的一些建议:
- 为这个级别设计你的exploit string。你需要的所有信息都可以通过检查
BUFBOMB
的反汇编版本来确定。使用objdump -d
获取其反汇编版本。 - 注意字节顺序。
- 可以用GDB去逐步执行最后
getbuf
最后几条指令,确保程序正在做正确的事情。 buf
数组在getbuf
栈帧中的位置取决于被用来去编译bufbomb
程序的gcc的版本,因此我们必须去阅读一些汇编代码来确定它的真正位置
题解:
反汇编bufbomb
程序得到其反汇编版本bufbomb.asm
查看getbuf
函数
1 | 08049262 <getbuf>: |
我们可以看出Gets
函数的参数,也就是buf
数组的首地址是ebp-0x28
- 因为
0x28=40
,所以我们需要先存入40个字符到栈帧中去占用给临时变量预留的空间 - 然后再存入4个字符去占用本该是旧的esp存的空间,
- 最后构造4个字符去作为返回地址将返回地址修改为
smoke
函数的地址0x08048e0a
,按照小端法排列。
- 因为
0x0A
不能出现,所以我们修改成0x0B
,同样能满足要求。 - 最后得到的exploit string如图所示
- 验证,与预期结果符合,Level 0完成。
Level 1: Sparkler(10 pts)
Level 1说明:
bufbomb
文件有一个函数fizz
,代码如下:
1 | void fizz(int val) { |
与Level 0相似,我们的任务是让bufbomb
去执行fizz
函数而不是test
函数
不同的是,我们要给函数fizz
传入一个参数val
,这个参数val
要等于userid对应的cookie。
作者的建议:函数不会调用fizz
函数,它只会执行它的代码。这对于我们想在栈帧中放置cookie的位置有提示。
题解:
- 查看
bufbomb
的汇编代码,fizz
函数的地址是0x08048daf
- userid为07的cookie是
0x429c4151
- 将这两个写入Level 1的exploit string,中间的是一个无关4字节(
fizz
函数把它当做返回地址),结果如图所示: - 验证,符合预期结果,Level 1完成
Level 2: Firecracker(15 pts)
Level 2说明:
bufbomb
文件有一个函数bang
,代码如下
1 | int global_value = 0; |
与Level 0和Level 1类似,我们的任务是让bufbomb
执行bang
函数。
不同的是,我们要将全局变量global_value
设置成userid对应的cookie
作者给出的建议:
- 你可以通过GDB来获取信息来构造你的exploit string
- 手工确定指令序列的字节编码非常繁琐易错,我们通过编写包含想要放入栈中的指令和数据的汇编代码让工具去做所有的工作。
- exploit string取决于自己的机器、自己的编译器以及cookie
- 不要试图使用jmp或call指令去跳转到
bang
的地址,因为这些指令使用相对PC寻址,很难设置正确。因此需要将地址压入栈中。
题解:
查看
bufbomb
的汇编代码,bang
函数的地址是0x08048d52
查看
bang
的汇编代码,找到global_value
的地址0x804d10c
要将
global_value
的值设置成cookie然后再执行bang
函数,我们编写如下汇编代码去实现它因为不能直接传入汇编代码,我们用gcc得到对应的二进制文件,objdump反汇编得到它的机器码
为了执行这些代码,我们需要知道
buf
数组的首地址,也就是getbuf
函数里的ebp-0x28
,使用GDB得到这个地址为0x55682fb8
所以构造exploit string如图所示:
验证,符合预期结果,Level 2完成
Level 3: Dynamite(20 pts)
Level 3说明:
我们之前的攻击都导致程序跳转到其他函数的代码,然后导致程序退出。因此,使用破坏堆栈的exploit string来覆盖保存的值是可以接受的。
最复杂形式的缓冲区溢出攻击会导致程序执行一些漏洞利用代码,这些代码会改变程序的寄存器/内存状态,但会使程序返回到原始调用函数(本例中为test
)。调用函数对攻击视而不见。不过,这种攻击方式很棘手,因为您必须:1)将机器代码放到堆栈上,2)将返回指针设置到该代码的开头,3)撤销对堆栈状态的任何破坏。
您在这个级别的任务是提供一个exploit string,它将导致getbuf
返回您的cookie进行测试,而不是值1。您可以在测试代码中看到,完成的话将导致程序成功运行“Bomb!”。您的漏洞代码应该将您的cookie设置为getbuf
返回值,恢复任何损坏的状态,在堆栈上压入正确的返回位置,并执行ret
指令以真正返回到test
。
作者的建议:
- 您可以使用GDB获得构建漏洞字符串所需的信息。在
getbuf
中设置一个断点并运行到该断点。确定参数,如保存的返回地址。 - 手工确定指令的机器码既繁琐又容易出错。您可以通过编写包含要放入堆栈的指令和数据的汇编代码文件,让工具来完成所有的工作。用GCC汇编这个文件,用OBJDUMP拆解。您应该能够获得您将在提示符下键入的确切字节序列。
- 请记住,您的exploit string取决于您的机器、编译器,甚至您的userid的cookie。
一旦你完成了这一关,停下来反思一下你已经完成了什么。你让一个程序执行你自己设计的机器代码。你这样做的方式足够隐秘,以至于程序没有意识到有什么不对劲。
题解:
我们这一Level的思路是:因为
getbuf
的返回值保存在eax里,然后赋给val
,所以我们只需要执行构造的代码将eax里面的值修改为cookie,同时将赋值指令的地址压入栈中,然后用ret指令返回地址,正常去执行test
的剩余指令。查看
test
的汇编代码,将getbuf
返回值赋给val
的指令的地址是0x8048e50
编写汇编代码实现改写eax的值、压入地址、返回地址,如图所示:
gcc编译成可重定位目标文件,objdump查看机器码
为了还原栈,查看调用
getbuf
时的旧的ebp构造exploit string,如图所示:
验证,预期结果符合,Level 3完成
Level 4: Nitroglycerin(10 pts)
Level 4说明:
请注意:您需要使用“-n”命令行选项来运行这关。
从一次运行到另一次运行,尤其是由不同的用户运行,给定过程使用的确切堆栈位置会有所不同。这种变化的一个原因是,当程序开始执行时,所有环境变量的值都放在堆栈底部附近。环境变量存储为字符串,根据它们的值需要不同的存储量。因此,为给定用户分配的堆栈空间取决于他或她的环境变量的设置。在GDB下运行程序时,栈的位置也不同,因为GDB为自己的一些状态使用栈空间
在调用getbuf的代码中,我们加入了稳定堆栈的特性,因此getbuf的堆栈框架的位置在两次运行之间是一致的。这使得您可以在知道buf
的确切起始地址的情况下编写一个攻击字符串。如果你试图在一个正常的程序上使用这样的漏洞,你会发现它有时会起作用,但在其他时候会导致分段错误。因此得名“炸药”——阿尔弗雷德·诺贝尔开发的一种炸药,含有稳定元素,使其不太容易发生意外爆炸。
对于这个级别,我们走了相反的方向,使堆栈位置比平时更不稳定。因此得名“硝化甘油”——一种出了名的不稳定炸药
当你用命令行标志“-n”运行BUFFAMBOM
时,它将在“硝基”模式下运行。该程序不是调用函数getbuf
,而是调用一个略有不同的函数getbufn
:
1 | /* Buffer size for getbufn */ |
这个函数类似于getbuf
,只是它有一个512个字符的缓冲区。您将需要这个额外的空间来创建一个可靠的漏洞。调用getbufn
的代码首先在堆栈上分配一个随机的存储量,这样,如果您在getbufn
的两次连续执行期间对%ebp的值进行采样,您会发现它们相差多达240。
此外,当在硝基模式下运行时,BUFBOMB要求您提供您的字符串5次,它将执行getbufn
5次,每次都有不同的堆栈偏移量。您的利用字符串必须使它每次都返回您的cookie。
您的任务与Level 3级别的任务相同。同样,您在这个级别的工作是提供一个漏洞利用字符串,它将导致getbufn
返回您的cookie进行测试,而不是值1。您可以在测试代码中看到,这将导致程序运行“KABOOM!”您的漏洞代码应该将您的cookie设置为返回值,恢复任何损坏的状态,在堆栈上推送正确的返回位置,并执行ret指令以真正返回testn。
作者建议:
- 您可以使用HEX2RAW程序发送漏洞字符串的多个副本。如果文件exploit.txt中只有一个副本,则可以使用以下命令:
unix> cat exploit.txt | ./hex2raw -n | ./bufbomb -n -u bovik
- 诀窍是利用nop指令。它用一个字节(代码0x90)编码。在CS:APP2e教材的第262页上阅读关于“nop sleds”的内容可能会有所帮助
题解:
查看
getbufn
函数的汇编代码,缓冲区首地址为ebp-0x208
查看
testn
将eax
返回值赋给val
的指令地址为0x8048ce2
我们这一题的思路是:因为这一等级缓冲区首地址会变化,所以我们不能直接GDB调试去得到一个确切的起始地址,我们需要用到nop指令,将有效机器指令尽可能往较大的地址处放,前面填充nop指令,这样跳转到哪里我们都能成功执行我们的恶意代码。
因为要恢复ebp存储
testn
的帧指针,查看testn
的汇编代码1
2
3
4
5
6mov %esp,%ebp
# 此时esp和ebp相等
push %ebx
# 此时ebp=esp+0x4
sub $0x24,%esp
# 这个时候执行完后,ebp=esp+0x28,这就是esp和ebp每次的变化关系,通过esp来恢复我们的每次的ebp编写恶意代码实现恢复ebp、cookie作为返回值、下一条指令地址压入栈中、返回地址出栈。
gcc和objdump得到代码的字节序列。
gcc得到大概的几个首地址,这里取的是最大的
0x55682e48
构造exploit string
验证,成功,实验结束。
实验总结
这次实验时间很紧张,主要是我的原因,到现在终于圆满完成,还是值得庆祝的。
花费时间最长的部分其实是英文文档的阅读,读起来太过吃力。
这个实验让我对缓冲溢出漏洞有了直观的认识,重新复习了栈的原理,了解了易于遭受缓冲区攻击代码的危险性。
作业要早早完成,不然会掉头发的。