深入理解计算机系统——LAB-4_BufLab

深入理解计算机系统——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 */
      #define NORMAL_BUFFER_SIZE 32

      int getbuf() {
      char buf[NORMAL_BUFFER_SIZE];
      Gets(buf);
      return 1;
      }
      • 容量为32字符的buf字符数组
      • 其中调用Gets(buf)读取字符串(以\n或文件结束符作为终止符),并将其连同终止符存储到一个目标地址(buf数组中)
      • 如果字符串长度<=31个字符则正常返回1,反之则返回错误
    • bufbomb有几个命令行参数:

      • -u userid为指定的userid操作炸弹
      • -h打印命令行参数列表
      • -n在”Nitro”模式下操作,在Level 4中使用。
      • -s将解决方案提交给评分服务器
  • makecookie:根据userid(用户id)生成一个“cookie”

    • 用法示例:

      1
      2
      unix> ./makecookie bovik
      0x1005b2b7
    • cookie是由8个十六进制数字组成的字符串,不出意外与userid一一对应

    • 在五次缓冲区攻击中的四次攻击中,我们的目标是将cookie放在他一般不该出现的地方。

  • hex2raw:一个帮助在字符串格式之间进行转换的程序

    • 输入两个一组的16进制数字以空格或者换行符分隔,也就是16进制格式的exploit string,转换为ASCII码格式字符串。支持c语言风格的注释

    • 你能通过设置一系列管道去通过hex2raw传递字符串:

      1
      unix> cat exploit.txt | ./hex2raw | ./bufbomb -u bovik
    • 你能存储原始字符串在一个文件,然后通过使用I/O重定向去提交给bufbomb

      1
      2
      unix> ./hex2raw < exploit.txt > exploit-raw.txt
      unix> ./bufbomb -u bovik < exploit-raw.txt

      在GDB调试内部也能使用这种方法:

      1
      2
      unix> 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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()->getbuf()->test(),我们的任务是将其改变为test()->getbuf()->smoke()smoke()函数代码如下:

1
2
3
4
5
void smoke() {
printf("Smoke!: You called smoke()\n");
validate(0);
exit(0);
}

作者给出的一些建议:

  • 为这个级别设计你的exploit string。你需要的所有信息都可以通过检查BUFBOMB的反汇编版本来确定。使用objdump -d获取其反汇编版本。
  • 注意字节顺序。
  • 可以用GDB去逐步执行最后getbuf最后几条指令,确保程序正在做正确的事情。
  • buf数组在getbuf栈帧中的位置取决于被用来去编译bufbomb程序的gcc的版本,因此我们必须去阅读一些汇编代码来确定它的真正位置

题解:

反汇编bufbomb程序得到其反汇编版本bufbomb.asm

image-20210603102948513

查看getbuf函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
08049262 <getbuf>:
8049262: 55 push %ebp
8049263: 89 e5 mov %esp,%ebp
8049265: 83 ec 38 sub $0x38,%esp
8049268: 8d 45 d8 lea -0x28(%ebp),%eax
804926b: 89 04 24 mov %eax,(%esp)
804926e: e8 bf f9 ff ff call 8048c32 <Gets>
8049273: b8 01 00 00 00 mov $0x1,%eax
8049278: c9 leave
8049279: c3 ret
804927a: 90 nop
804927b: 90 nop
804927c: 90 nop
804927d: 90 nop
804927e: 90 nop
804927f: 90 nop

我们可以看出Gets函数的参数,也就是buf数组的首地址是ebp-0x28

  • 因为0x28=40,所以我们需要先存入40个字符到栈帧中去占用给临时变量预留的空间
  • 然后再存入4个字符去占用本该是旧的esp存的空间,
  • 最后构造4个字符去作为返回地址将返回地址修改为smoke函数的地址0x08048e0a,按照小端法排列。

image-20210603121223800

  • 因为0x0A不能出现,所以我们修改成0x0B,同样能满足要求。
  • 最后得到的exploit string如图所示
    image-20210603174115683
  • 验证,与预期结果符合,Level 0完成。
    image-20210603192226913

Level 1: Sparkler(10 pts)

Level 1说明:

bufbomb文件有一个函数fizz,代码如下:

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

与Level 0相似,我们的任务是让bufbomb去执行fizz函数而不是test函数

不同的是,我们要给函数fizz传入一个参数val,这个参数val要等于userid对应的cookie。

作者的建议:函数不会调用fizz函数,它只会执行它的代码。这对于我们想在栈帧中放置cookie的位置有提示。

题解:

  • 查看bufbomb的汇编代码,fizz函数的地址是0x08048daf
    image-20210603203218420
  • userid为07的cookie是0x429c4151
    image-20210603204916203
  • 将这两个写入Level 1的exploit string,中间的是一个无关4字节(fizz函数把它当做返回地址),结果如图所示:
    image-20210603213910804
  • 验证,符合预期结果,Level 1完成
    image-20210603213409034

Level 2: Firecracker(15 pts)

Level 2说明:

bufbomb文件有一个函数bang,代码如下

1
2
3
4
5
6
7
8
9
10
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 0和Level 1类似,我们的任务是让bufbomb执行bang函数。

不同的是,我们要将全局变量global_value设置成userid对应的cookie

作者给出的建议:

  • 你可以通过GDB来获取信息来构造你的exploit string
  • 手工确定指令序列的字节编码非常繁琐易错,我们通过编写包含想要放入栈中的指令和数据的汇编代码让工具去做所有的工作。
  • exploit string取决于自己的机器、自己的编译器以及cookie
  • 不要试图使用jmp或call指令去跳转到bang的地址,因为这些指令使用相对PC寻址,很难设置正确。因此需要将地址压入栈中。

题解:

  • 查看bufbomb的汇编代码,bang函数的地址是0x08048d52
    image-20210604100834183

  • 查看bang的汇编代码,找到global_value的地址0x804d10c
    image-20210604141946443

  • 要将global_value的值设置成cookie然后再执行bang函数,我们编写如下汇编代码去实现它
    image-20210604142631247

  • 因为不能直接传入汇编代码,我们用gcc得到对应的二进制文件,objdump反汇编得到它的机器码
    image-20210604142605461

  • 为了执行这些代码,我们需要知道buf数组的首地址,也就是getbuf函数里的ebp-0x28,使用GDB得到这个地址为0x55682fb8

    image-20210604151933110

  • 所以构造exploit string如图所示:
    image-20210604152555659

  • 验证,符合预期结果,Level 2完成
    image-20210604152527883

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
    image-20210604171307444

  • 编写汇编代码实现改写eax的值、压入地址、返回地址,如图所示:

    image-20210604172403658

  • gcc编译成可重定位目标文件,objdump查看机器码
    image-20210604174037448

  • 为了还原栈,查看调用getbuf时的旧的ebp
    image-20210604181743588

  • 构造exploit string,如图所示:
    image-20210604184022787

  • 验证,预期结果符合,Level 3完成
    image-20210604184002866

Level 4: Nitroglycerin(10 pts)

Level 4说明:

请注意:您需要使用“-n”命令行选项来运行这关。

从一次运行到另一次运行,尤其是由不同的用户运行,给定过程使用的确切堆栈位置会有所不同。这种变化的一个原因是,当程序开始执行时,所有环境变量的值都放在堆栈底部附近。环境变量存储为字符串,根据它们的值需要不同的存储量。因此,为给定用户分配的堆栈空间取决于他或她的环境变量的设置。在GDB下运行程序时,栈的位置也不同,因为GDB为自己的一些状态使用栈空间

在调用getbuf的代码中,我们加入了稳定堆栈的特性,因此getbuf的堆栈框架的位置在两次运行之间是一致的。这使得您可以在知道buf的确切起始地址的情况下编写一个攻击字符串。如果你试图在一个正常的程序上使用这样的漏洞,你会发现它有时会起作用,但在其他时候会导致分段错误。因此得名“炸药”——阿尔弗雷德·诺贝尔开发的一种炸药,含有稳定元素,使其不太容易发生意外爆炸。

对于这个级别,我们走了相反的方向,使堆栈位置比平时更不稳定。因此得名“硝化甘油”——一种出了名的不稳定炸药

当你用命令行标志“-n”运行BUFFAMBOM时,它将在“硝基”模式下运行。该程序不是调用函数getbuf,而是调用一个略有不同的函数getbufn

1
2
/* Buffer size for getbufn */
#define KABOOM_BUFFER_SIZE 512

这个函数类似于getbuf,只是它有一个512个字符的缓冲区。您将需要这个额外的空间来创建一个可靠的漏洞。调用getbufn的代码首先在堆栈上分配一个随机的存储量,这样,如果您在getbufn的两次连续执行期间对%ebp的值进行采样,您会发现它们相差多达240。

此外,当在硝基模式下运行时,BUFBOMB要求您提供您的字符串5次,它将执行getbufn5次,每次都有不同的堆栈偏移量。您的利用字符串必须使它每次都返回您的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
    image-20210604200855918

  • 查看testneax返回值赋给val的指令地址为0x8048ce2
    image-20210604200930760

  • 我们这一题的思路是:因为这一等级缓冲区首地址会变化,所以我们不能直接GDB调试去得到一个确切的起始地址,我们需要用到nop指令,将有效机器指令尽可能往较大的地址处放,前面填充nop指令,这样跳转到哪里我们都能成功执行我们的恶意代码。

  • 因为要恢复ebp存储testn的帧指针,查看testn的汇编代码

    1
    2
    3
    4
    5
    6
    mov %esp,%ebp
    # 此时esp和ebp相等
    push %ebx
    # 此时ebp=esp+0x4
    sub $0x24,%esp
    # 这个时候执行完后,ebp=esp+0x28,这就是esp和ebp每次的变化关系,通过esp来恢复我们的每次的ebp

    image-20210604235350148

  • 编写恶意代码实现恢复ebp、cookie作为返回值、下一条指令地址压入栈中、返回地址出栈。
    image-20210604212313389

  • gcc和objdump得到代码的字节序列。
    image-20210604212418838

  • gcc得到大概的几个首地址,这里取的是最大的0x55682e48
    image-20210604212514423

  • 构造exploit string
    image-20210604212346929

  • 验证,成功,实验结束。
    image-20210604212437957

实验总结

这次实验时间很紧张,主要是我的原因,到现在终于圆满完成,还是值得庆祝的。

花费时间最长的部分其实是英文文档的阅读,读起来太过吃力。

这个实验让我对缓冲溢出漏洞有了直观的认识,重新复习了栈的原理,了解了易于遭受缓冲区攻击代码的危险性。

作业要早早完成,不然会掉头发的。

  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2021 Sung
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信