前言:
从今年4月份开始,慢慢接触高版本的glibc,高版本glibc的堆题也使得国内ctf比赛进入到一个新的时期,众多比赛纷纷将pwn的堆题难度提升至了2.35.而众所周知,这一个版本的删除了好多函数的hook,其中最常用的
__malloc_hook
__realloc_hook
__free_hook
被删除,以至于我们很难进行后门利用。那么似乎在高版本,只可以去利用_IO_file
但是,某一天晚上凌晨,有师傅问我知不知道tls攻击手法?
于是乎,今天介绍一个在高版本中利用非常简单的手法。
因为相关文章比较少,所以本文纯手工查阅源码,以本地环境进行测试
如若发现错误之处,请您不吝斧正,非常感谢。^v^~
测试环境 Ubuntu22 glibc2.35
攻击要求,
-
能在任意地址写一个可控制的堆地址,
-
可以修露出tcache的key值或者可以修改其全局变量secret
-
程序可以正常从main 函数 的return返回或者可以触发exit()退出程序
原理介绍
main函数正常retrun会调用exit()函数,所以我们来看看exi()函数的执行流程
void exit (int status)
{
__run_exit_handlers (status, &__exit_funcs, true, true);
}
exit函数会先调用__run_exit_handlers函数,
void
attribute_hidden
__run_exit_handlers (int status, struct exit_function_list **listp,
bool run_list_atexit, bool run_dtors)
{
/* First, call the TLS destructors. */
#ifndef SHARED
if (&__call_tls_dtors != NULL)
#endif
if (run_dtors)
__call_tls_dtors ();
__libc_lock_lock (__exit_funcs_lock);
··· ···
}
我们可以看到这个函数里首先会判断"__call_tls_dtors"和"run_dtors"这两个变量的值是否为空,然后调用__call_tls_dtors().
这里会让人疑惑,__call_tls_dtors的初始值是什么?这里会不会直接执行呢?
我们在调试中发现这里会直接调用__call_tls_dtor()函数,然后我们看看这个函数
void
__call_tls_dtors (void)
{
while (tls_dtor_list)
{
struct dtor_list *cur = tls_dtor_list;
dtor_func func = cur->func;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (func);
#endif
tls_dtor_list = tls_dtor_list->next;
func (cur->obj);
atomic_fetch_add_release (&cur->map->l_tls_dtor_count, -1);
free (cur);
}
}
首先这里会检查这个链表是否为空,链表的每一个节点对应的是一个dtor_list类型的结构体,定义如下
struct dtor_list
{
dtor_func func;
void *obj;
struct link_map *map;
struct dtor_list *next;
};
如果程序是正常退出,那么这个链表就是空的
也就不会去执行 PTR_DEMANGLE (func);
下面是函数的汇编源码
=> 0x7ff6370e1d60 <__GI___call_tls_dtors>: endbr64
0x7ff6370e1d64 <__GI___call_tls_dtors+4>: push rbp
0x7ff6370e1d65 <__GI___call_tls_dtors+5>: push rbx
0x7ff6370e1d66 <__GI___call_tls_dtors+6>: sub rsp,0x8
0x7ff6370e1d6a <__GI___call_tls_dtors+10>: mov rbx,QWORD PTR [rip+0x1d301f] # 0x7ff6372b4d90
0x7ff6370e1d71 <__GI___call_tls_dtors+17>: mov rbp,QWORD PTR fs:[rbx]
0x7ff6370e1d75 <__GI___call_tls_dtors+21>: test rbp,rbp
0x7ff6370e1d78 <__GI___call_tls_dtors+24>: je 0x7ff6370e1dbd <__GI___call_tls_dtors+93># 检查链表是否为空
0x7ff6370e1d7a <__GI___call_tls_dtors+26>: nop WORD PTR [rax+rax*1+0x0]
0x7ff6370e1d80 <__GI___call_tls_dtors+32>: mov rdx,QWORD PTR [rbp+0x18]
0x7ff6370e1d84 <__GI___call_tls_dtors+36>: mov rax,QWORD PTR [rbp+0x0]
0x7ff6370e1d88 <__GI___call_tls_dtors+40>: ror rax,0x11
0x7ff6370e1d8c <__GI___call_tls_dtors+44>: xor rax,QWORD PTR fs:0x30
0x7ff6370e1d95 <__GI___call_tls_dtors+53>: mov QWORD PTR fs:[rbx],rdx
0x7ff6370e1d99 <__GI___call_tls_dtors+57>: mov rdi,QWORD PTR [rbp+0x8]
0x7ff6370e1d9d <__GI___call_tls_dtors+61>: call rax
0x7ff6370e1d9f <__GI___call_tls_dtors+63>: mov rax,QWORD PTR [rbp+0x10]
0x7ff6370e1da3 <__GI___call_tls_dtors+67>: lock sub QWORD PTR [rax+0x468],0x1
0x7ff6370e1dac <__GI___call_tls_dtors+76>: mov rdi,rbp
0x7ff6370e1daf <__GI___call_tls_dtors+79>: call 0x7ff6370c4370 <free@plt>
0x7ff6370e1db4 <__GI___call_tls_dtors+84>: mov rbp,QWORD PTR fs:[rbx]
0x7ff6370e1db8 <__GI___call_tls_dtors+88>: test rbp,rbp
0x7ff6370e1dbb <__GI___call_tls_dtors+91>: jne 0x7ff6370e1d80 <__GI___call_tls_dtors+32>
0x7ff6370e1dbd <__GI___call_tls_dtors+93>: add rsp,0x8
0x7ff6370e1dc1 <__GI___call_tls_dtors+97>: pop rbx
0x7ff6370e1dc2 <__GI___call_tls_dtors+98>: pop rbp
0x7ff6370e1dc3 <__GI___call_tls_dtors+99>: ret
我们来分析下,如果链表不是空的,后面怎么执行。
asm=
nop WORD PTR [rax+rax*1+0x0] #这条代码没有任何意义直接跳过
mov rdx,QWORD PTR [rbp+0x18] #rbp是第一个结构体的位置addr,
#那么将[addr+0x18]的值赋给rdx,这个值是结构体里的next指针。
mov rax,QWORD PTR [rbp+0x0] #将结构体的fuc指针交给rax
ror rax,0x11 #fuc 与0x11进行右循环异或,结果仍保存在rax
xor rax,QWORD PTR fs:0x30 #rax 与fs段里的某一个值进行异或,这个值就是tcache的key字段,
#或者说就是secret的变量的值。secret的位置在tls基址+0x30
mov QWORD PTR fs:[rbx],rdx #将next指针放入fs段
mov rdi,QWORD PTR [rbp+0x8] #将obj指针写入rdi
call rax #调用fuc
这里不会对tls_dtor_list的结构做是否合法的检查。而且这里还设置了rbp栈底指向结构体的地址,
所以,如果我们将rbp劫持到某一个地址,然后call rax的时候执行leave ret;就可以实现栈的迁移!
所以我们的思路就是把tls_dtor_list的头节点写为一个堆地址heap_address_ctr,然后在heap_address_ctr写入leave ret的gadget指针,这样,call rax 后,rip 指向了heap_address_ctr +8,我们就完成了栈的劫持,我们可以在这里布置rop。
思考:
-
代码中有一个异或操作以及循环异或,我们怎么解决?
-
当我们获取到一个leave_ret gadget的真实地址,以及知道tcache的key值(泄露也好,修改secret也行),我们就可以计算出我们要写入的值addr
-
python addr = ((leave_ret ^ tcache_key)<<0x11)&0xffffffffffff8000 addr += ((leave_ret ^ tcache_key)>>0x2f)&0x7fff
经过ror以及xor的操作后,可以得到真正的leave_ret的地址
-
-
tls_dtor_list 地址如何获取?
- 加载调试符号的libc文件,可以直接使用 gdb的p 指令 p &tls_dtor_list,附近可以找到secret