glibc2.35后门执行研究:tls_dtor_list攻击劫持exit执行流程

admin 2022年9月29日10:37:03评论37 views字数 4191阅读13分58秒阅读模式

前言:

​ 从今年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的初始值是什么?这里会不会直接执行呢?
glibc2.35后门执行研究:tls_dtor_list攻击劫持exit执行流程
我们在调试中发现这里会直接调用__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;
};

如果程序是正常退出,那么这个链表就是空的

glibc2.35后门执行研究:tls_dtor_list攻击劫持exit执行流程
也就不会去执行 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

总结:

  1. 我们要能够知道tcache的key值,

  2. 获取tls_dtor_list的地址,改变他的值,写为一个已知的地址A

  3. 在A写入leave_ret经过加密后的addr,addr后面紧接着写上rop

  4. exit()退出程序

FROM:tttang . com

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年9月29日10:37:03
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   glibc2.35后门执行研究:tls_dtor_list攻击劫持exit执行流程http://cn-sec.com/archives/1323543.html

发表评论

匿名网友 填写信息