Writeup for Assignment BufBomb, CSAPP 2nd Edition
正如etone所说,你在专业上的技不如人,迟早有一天会找上来。
Writeup for Assignment BufBomb, CSAPP 2nd Edition
overview
All materials needed in this lab are
stored in my github repo.
- my gcc version:
gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
- glibc version:
ldd (Ubuntu GLIBC 2.35-0ubuntu3) 2.35
- kernel version:
Linux workshop 5.15.0-56-generic
这道题的最低利用条件应该是 No Canary
+No PIE
。
solution1
- elf file:
bufbomb
- compile command:
gcc -m32 bufbomb.c -o bufbomb -g -no-pie -fno-stack-protector -O0
- guard: no PIE, no Canary
审计题目源码后最容易发现的解法应该是跳过赋值语句,直接到printf
语句。
1 | 080492ae <test>: |
如以上代码所述,在0x80492d2
处执行完getbuf
,接下来是把返回值(即eax
)压进栈中,然后再把字符串(即getbuf returned 0x%x\n
)地址压入栈中。因为我们可以通过getxs
操作整个getbuf
函数的栈,又因为test
函数调用了getbuf
函数————也就是test
在getbuf
逻辑意义上的上面(或者物理意义的下面),我们也可以操纵整个test
的栈。这样第一种利用printf
语句的方法就很容易得出了:跳到0x80492e0
,然后控制栈顶使栈顶为0xdeadbeef。
1 | 08049288 <getbuf>: |
显然栈抬升了0x18个Bytes(注意栈从高向低生长)。因此我们的Payload需要加上0x18个Bytes的填充。
程序为32位程序;那么Payload还需要加上0x04个Bytes来填充edp。
最后我们需要将存储的eip指针覆盖为我们想要去的地址,也就是0x80492e0
,并且使得覆盖完后的栈顶[1]为0xdeadbeef。(注意Linux x64是小端序机器)
这是一种Payload:00000000 00000000 00000000 00000000 00000000 00000000 00000000 E0920408 EFBEADDE
solution2
- elf file:
bufbomb
- compile command:
gcc -m32 bufbomb.c -o bufbomb -g -no-pie -fno-stack-protector -O0
- guard: no PIE, no Canary
另外一个非常容易想到的思路和ret2libc
[2]非常像。
我们完全可以不使用0x080492e7
处的printf
————我们可以自己构造一个出来!
字符串的地址是0x0804A019
,第二个参数应为0xdeadbeef,所以根据i386架构下的ret2libc
原理,我们可以写出以下payload:
padding + ebp + (target address) + (return address) + arg1 + arg2 + arg3 ...
与solution1中一样,padding为0x18Bytes,ebp为0x04Bytes。目标函数为printf
在plt表中的位置。return function可以不填。arg1为0x0804A019
,arg2为0xdeadbeef。
1 | 00:0000│ eax esp 0xffffd3b0 ◂— 0x0 |
从上至下依次是28Bytes的padding zeros,目标函数地址,返回地址,参数1,参数2。
payload: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 50900408 00000000 19A00408 EFBEADDE
solution3
- elf file:
bufbomb-no-nx
- compile command:
gcc -m32 bufbomb.c -o bufbomb-no-nx -g -no-pie -fno-stack-protector -O0 -z execstack
- guard: no PIE, no Canary, no NX, no ASLR
接下来我们来讨论在关闭NX
保护和关闭ASLR
保护的利用情况。[3]
我们可以回顾一下程序的各个section基本情况:
1 | 0x8048000 0x8049000 r--p 1000 0 /root/repos/SCUCCS/C-Programming/Security Labs/lab2/bufbomb-no-nx |
.code
段有读、执行权限,但是没有写权限.data
、heap
、通常情况下的stack
段,都是只有读写权限.rodata
只有读权限
一般来说,写权限和执行权限应该尽量分开。这种保护方法就叫NX
保护————或者No eXecute
保护。
但是如果我们主动在gcc编译中关闭NX保护,那我们就可以得到一个RWX段,也就是同时有读、写、执行权限的段,栈。
这时我们可以考虑将shellcode写在栈上,然后劫持控制流到shellcode的开始处。这时Payload应具有下面的结构:
shellcode + padding + ebp + shellcode's start addr
第二个问题出现了。shellcode写在栈上,虽然我们可以通过关闭NX保护将shellcode从不可执行变成可执行,但是我们并不知道shellcode的地址。每次我们运行程序的时候,内核都会随机加载程序的地址空间。
Problem solved!那就让我们随便试两条汇编指令吧!
1 | 080492ae <test>: |
1 | mov eax, 0xdeadbeef |
1 | 0x80492a7 <getbuf+31> mov eax, 1 |
当跳转到0x80492d7后,这一切就像无事发生,只不过返回值,也就是eax
会被改成0xdeadbeef。[4]
1 | push 0xdeadbeef |
1 | 0x80492a7 <getbuf+31> mov eax, 1 |
跳转到0x80492e0也就意味着跳过了push第二个参数。因此,我们可以直接通过栈操作到给第二个参数赋值。
当然,既然我们可以执行任意汇编代码了,那我们有很多种方法来使得输出达到我们想要的结果。
solution4/彩蛋
- elf file:
bufbomb-no-nx
- compile command:
gcc -m32 bufbomb.c -o bufbomb-no-nx -g -no-pie -fno-stack-protector -O0 -z execstack
- guard: no PIE, no Canary, no NX, no ASLR
既然我们可以执行任意汇编代码,那我们为什么不试着拿Shell权限呢?首先我们需要找到一个放置Shellcode的地方。
回到我们前面给到的这个结构:shellcode + padding + ebp + shellcode's start addr
padding + ebp
一共是24Bytes,但是考虑到Shellcode执行阶段可能遇到的push
指令,更好的选择其实是放在shellcode's start addr
的后面。
这时我们的payload就变成了下面的结构:
padding + ebp + shellcode's start addr + shellcode
在网上找一个小一点的Shellcode[5],我们就得到了我们最终的Payload:
00000000 00000000 00000000 00000000 00000000 00000000 EBP START_ADDR 31C9F7E1 B00B5168 2F2F7368 682F6269 6E89E3CD 80
1 | # echo getbuf returned 0xdeadbeef |
Summary
首先声明一点,这个Assignment在CSAPP第三版中已经没有了。所以bufbomb.c
上面的参考价值不大。尤其是不要按照它上面的编译指令去编译:-Og
和-O2
会把程序结构搅乱到根本做不了,没有-fno-stack-protector
和-no-pie
就是字面意思上的做不了这道题。
然后谈谈我个人对这个Lab(Assignment)的理解:我并不觉得这个Lab(Assignment)很好。第一点就是CSAPP 2nd到CSAPP 3rd编辑的主旋律就是x86tox86-64,整个Lab(Assignment)在设计的时候带着IA32的思维,不难理解为什么放在现在颇有鸡肋之感。第二点是没有难度梯度,思维难度大且调试难度高的题目如果没有checkpoint很容易让人放弃。第三点就是与Buffer Lab冲突,而且Buffer Lab是它的上位替补,这个应该做过Buffer Lab的人都深有体会————深入浅出,让人醍醐灌顶。
References
- 1.基本的C语言函数调用栈知识可以看这篇文章。 ↩
- 2.在CTF-Wiki上简述了ret2libc的原理和利用方法。 ↩
- 3.How to turn off gcc compiler optimization to enable buffer overflow? - stackoverflow ↩
- 4.我们可以使用PWNTools中的ASM模块来将汇编代码编译成字节码。 ↩
- 5.利用
int 0x80
执行了/bin/sh
的一段Shellcode ↩