各类IO,以及 printf 源码分析

Prelogue

@ad2019-07-06
虽然做FMT_STRING漏洞的题目已经挺多了,但是对于printf实现过程也只是处于了解阶段,想要借着完成`printable@pwnable.tw`的机会仔细理解一下源码(虽然我还没做出来,但我感觉这题和fmtstr内部没什么多大关系),因为白天有事情时间有限可能完成得断断续续,分析的粒度主要看精力如果在接受完白天的摧残后晚上精力比较充沛可能分析的比较仔细,如果脑子快运作不起来了的话粒度可能比较粗。

Start

首先是最开始的程序

1
2
3
4
5
#include<stdio.h>
int main()
{
printf("n132\n");
}

看似很简单的printf其实内部的实现是非常复杂的.
其入口位于glibc/stdio-common/printf.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//glibc-2.23
int
__printf (const char *format, ...)
{
va_list arg;
int done;

va_start (arg, format);
done = vfprintf (stdout, format, arg);
va_end (arg);

return done;
}

#undef _IO_printf
ldbl_strong_alias (__printf, printf);

可以看出主要实现在vfprintf里,跟过去就会发现一个400多行的函数,直接啃难度太大的,我就先把vfprintf放到一边,复习一遍最基本的输入输出.

basic_io(sys_read&sys_write)

学过操作系统的应该都了解进程空间分为内核空间和用户空间,内核空间会提供一些系统调用来给运行在用户空间的程序用以完成一些基本的任务,像是读写打开什么的.这些系统调用可以完成最基本的工作,其他复杂的函数是通过许多的基本系统调用组合而成的。关于输入输出最基本的就是x64的0号调用sys_read和1号调用sys_write
下面是通过系统调用输入输出的一个demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int main()
{
asm(
"xor %rax,%rax\n"
"mov $0x9,%al\n"
"mov $0xcafe000,%rdi\n"
"mov $0x1000,%rsi\n"
"mov $0x3,%rdx\n"
"mov $0x22,%r10\n"
"mov $-1,%r8\n"
"mov $0,%r9\n"
"syscall\n"
"xor %rax,%rax\n"
"xor %rdi,%rdi\n"
"mov $0xcafe000,%rsi\n"
"mov $0x100,%rdx\n"
"syscall\n"
"xor %rax,%rax\n"
"inc %rax\n"
"inc %rdi\n"
"syscall\n"
);
}

简而言之

1
2
3
mmap(0xcafe000,3,22,-1,0);
read(0,0xcafe000,0x100);
write(1,0xcafe000,0x100);

0,1,2

其实在上述的程序中read(0,0xcafe000,0x100);其中的0表示stdin,1表示stdout,除此之外(Unix/Linux/BSD)默认还有stderr(2)
他们的定义在libio/libio.h中提及

1
2
3
extern struct _IO_FILE_plus _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;

其中的_IO_FILE_plus结构体由两部分组成:_IO_FILE结构体和跳表

1
2
3
4
5
6
//libio/libioP.h
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};

_IO_FILE结构体的部分用来描述一个文件的读写缓冲区以及其他信息

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
//libio/libio.h
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;

int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */

#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];

/* char* _save_gptr; char* _save_egptr; */

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

其中跳表的部分用来储存各个函数指针.

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
//libio/libioP.h
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
get_column;
set_column;
#endif
};

simple_io(PUTS of example)

相对printf来说puts的功能较为单一源码也较为简单可以先用来预热一下.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//libio/ioputs.c
int
_IO_puts (const char *str)
{
int result = EOF;
size_t len = strlen (str);//确定输出长度
_IO_acquire_lock (stdout);//上锁stdout
if ((_IO_vtable_offset (stdout) != 0//check 存在stdout
|| _IO_fwide (stdout, -1) == -1)
&& _IO_sputn (stdout, str, len) == len//调用stdout的_IO_sputn完成输出功能
&& _IO_putc_unlocked ('\n', stdout) != EOF)
result = MIN (INT_MAX, len + 1);
_IO_release_lock (stdout);//解锁stdout
return result;
}

从源码看来最终是调用了_IO_sputn来实现puts的功能其中_IO_sputs的地址来源与stdout.vtablestdou是可写的所以可以劫持stdout来控制执行流
同时也有了了围绕_IO_FILE攻击的一系列攻击方法.不过这次重点不是_IO_FILE我们借由puts得知一般的输入输出函数是借用stdoutorstdinvtable完成输出输入任务,在下面的代码中可以看出_IO_sputn最终是通过IO_SYSWRITE实现写也就是系统调用,我们借由简单的puts把我们常用的输出函数和之前的系统调用0联系起来,在之后的printf中粒度就不会那么细我们只需要知道最终printf也是用的0号调用完成输出任务就可以了,把主要的目的放在理解其对格式化串的解析工作以及printf的工作流程.

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
//libio/fileops.c
ssize_t
new_do_write (FILE *fp, const char *data, size_t to_do)
{
size_t count;
if (fp->_flags & _IO_IS_APPENDING)
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do);
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0
&& (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}

size_t
_IO_new_file_xsputn (FILE *f, const void *data, size_t n)
{
const char *s = (const char *) data;
size_t to_do = n;
int must_flush = 0;
size_t count = 0;
if (n <= 0)
return 0;
/* This is an optimized implementation.
If the amount to be written straddles a block boundary
(or the filebuf is unbuffered), use sys_write directly. */
/* First figure out how much space is available in the buffer. */
if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
{
count = f->_IO_buf_end - f->_IO_write_ptr;
if (count >= n)
{
const char *p;
for (p = s + n; p > s; )
{
if (*--p == '\n')
{
count = p - s + 1;
must_flush = 1;
break;
}
}
}
}
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
/* Then fill the buffer. */
if (count > 0)
{
if (count > to_do)
count = to_do;
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
s += count;
to_do -= count;
}
if (to_do + must_flush > 0)
{
size_t block_size, do_write;
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)
/* If nothing else has to be written we must not signal the
caller that everything has been written. */
return to_do == 0 ? EOF : n - to_do;
/* Try to maintain alignment: write a whole number of blocks. */
block_size = f->_IO_buf_end - f->_IO_buf_base;
do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);
if (do_write)
{
count = new_do_write (f, s, do_write);
to_do -= count;
if (count < do_write)
return n - to_do;
}
/* Now write out the remainder. Normally, this will fit in the
buffer, but it's somewhat messier for line-buffered files,
so we let _IO_default_xsputn handle the general case. */
if (to_do)
to_do -= _IO_default_xsputn (f, s+do_write, to_do);
}
return n - to_do;
}
libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)

complex-io(printf for example)

可以看出printf简单处理后调用__vfprintf_internal也就是vfprintf

1
2
3
4
5
6
7
8
9
10
11
12
___printf_chk (int flag, const char *format, ...)
{
/* For flag > 0 (i.e. __USE_FORTIFY_LEVEL > 1) request that %n
can only come from read-only format strings. */
unsigned int mode = (flag > 0) ? PRINTF_FORTIFY : 0;
va_list ap;
int ret;
va_start (ap, format);
ret = __vfprintf_internal (stdout, format, ap, mode);
va_end (ap);
return ret;
}

这是一个400多行的函数,主要功能是更具fmtstr完成特定功能的输出,在自己分析前我参考了Mutepig关于printf的分析。
和百度百科的说明

因为printf就是处理fmtstr的一个函数,先来了解一下fmtstr的基本格式

1
2
通常意义上format的格式如下:
%[flags][width][.prec][mode]type

关于flags,width,.prec,mod,type的解释在百度百科的说明里面已经比较详细了.浏览一遍之后直接啃vfprintf了.

PART I: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
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

/* 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;

/* 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 *workstart = NULL;
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;

/* Orient the stream. */
#ifdef ORIENT
ORIENT;
#endif

/* Sanity check of arguments. */
ARGCHECK (s, format);

#ifdef ORIENT
/* Check for correct orientation. */
if (_IO_vtable_offset (s) == 0 &&
_IO_fwide (s, sizeof (CHAR_T) == 1 ? -1 : 1)
!= (sizeof (CHAR_T) == 1 ? -1 : 1))
/* The stream is already oriented otherwise. */
return EOF;
#endif

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);

/* Initialize local variables. */
done = 0;
grouping = (const char *) -1;
#ifdef __va_copy
/* This macro will be available soon in gcc's <stdarg.h>. We need it
since on some systems `va_list' is not an integral type. */
__va_copy (ap_save, ap);
#else
ap_save = ap;
#endif
nspecs_done = 0;

开始的这一部分是定义了一些变量,做了一些检查,看下来感觉比较有用的是记住 变量f表示 Current character in format string.

PART II: Find the first ‘%’

:::下面的源码分析会把一些重要的信息中文标注在源码里.:::

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
#ifdef COMPILE_WPRINTF
/* Find the first format specifier. */
f = lead_str_end = __find_specwc ((const UCHAR_T *) format);
#else
/* Find the first format specifier. */
f = lead_str_end = __find_specmb ((const UCHAR_T *) format);
#endif
//__find_specwc (const unsigned int *format)
//{
//return (const unsigned int *) __wcschrnul ((const wchar_t *) format, L'%');
//}
//__find_specmb (const unsigned char *format)
//{
//return (const unsigned char *) __strchrnul ((const char *) format, '%');
//}
//结合注释和上述两个函数的definition可以看出上面这部分是找到第一个%的位置
//
/* Lock stream. */
_IO_cleanup_region_start ((void (*) (void *)) &_IO_funlockfile, s);
_IO_flockfile (s);

//对stdout上锁


/* Write the literal text before the first format. */
outstring ((const UCHAR_T *) format,
lead_str_end - (const UCHAR_T *) format);
//输出第一个%前面内容
/* If we only have to print a simple string, return now. */
if (*f == L_('\0'))
goto all_done;
//简单串不含%的话直接返回.
/* 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;
//检查了几张表

第二部分也相对简单:

  • 寻找第一个%
  • 输出前面所有东西后简单串的话返回,否则继续。

PART III: Main

接下来是一个几乎贯穿函数的大do while循环对fmtstr进行解析.
循环内首先定义了必要的一些变量

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
 /* Process whole format string.  */
STEP0_3_TABLE;
STEP4_TABLE;

union printf_arg *args_value; /* This is not used here but ... */
int is_negative; /* Flag for negative number. */
union
{
unsigned long long int longlong;
unsigned long int word;
} number;
int base;
union printf_arg the_arg;
CHAR_T *string; /* Pointer to argument string. */
int alt = 0; /* Alternate format. */
int space = 0; /* Use space prefix if no sign is needed. */
int left = 0; /* Left-justify output. */
int showsign = 0; /* Always begin with plus or minus sign. */
int group = 0; /* Print numbers according grouping rules. */
int is_long_double = 0; /* Argument is long double/ long long int. */
int is_short = 0; /* Argument is short int. */
int is_long = 0; /* Argument is long int. */
int is_char = 0; /* Argument is promoted (unsigned) char. */
int width = 0; /* Width of output; 0 means none specified. */
int prec = -1; /* Precision of output; -1 means none specified. */
/* This flag is set by the 'I' modifier and selects the use of the
`outdigits' as determined by the current locale. */
int use_outdigits = 0;
UCHAR_T pad = L_(' ');/* Padding character. */
CHAR_T spec;

workstart = NULL;
workend = work_buffer + WORK_BUFFER_SIZE;

变量名字取得还是非常nice的..基本上看上去就能知道是干什么的.

在分析下面之前先看几个宏

  • NOT_IN_JUMP_RANGE:判断是否可显示字符
    #define NOT_IN_JUMP_RANGE(Ch) ((Ch) < L_(' ') || (Ch) > L_('z'))
  • CHAR_CLASS:获取与0x20的偏移即table中的idx
    #define CHAR_CLASS(Ch) (jump_table[(INT_T) (Ch) - L_(' ')])
  • JUMP
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 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

JUMP简单来说就是跳到table[idx]

事实上要读懂printf要点就是读懂stepX_jumps其中X为0-4
虽然有点长,但是这4张表是printf的要点..还是放上来.

Table 当前状态
step0_jumps 处理flags
step1_jumps width处理结束
step2_jumps precision处理结束
step3a_jumps 已经接受一个h
step3b_jumps 已经接受一个l
step4_jumps mod已经处理接受

本质上用哪张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
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
#define STEP0_3_TABLE							      \
/* Step 0: at the beginning. */ \
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' */ \
}; \
/* Step 1: after processing width. */
... \
/* Step 2: after processing precision. */
... \
/* Step 3a: after processing first 'h' modifier. */ \
... \
/* Step 3b: after processing first 'l' modifier. */ \
... \
/* 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' */ \
}

其中的REF定义如下
# define REF(Name) &&do_##Name
还有LABLE
#define LABEL(Name) do_##Name
所以就可以把表和处理部分联系起来.
根据当前的值来选择需要去的标签.

这6张表可以看出相似度很高只是越后面的表form_unknown越来越多
form_unknown代表当前阶段不处理该字符。也就是说printf对串的处理是分阶段的第一阶段处理flags,第二阶段处理width,第三阶段….

如果处理完了flags进入了width这时候忽然又来了一个flag这样的话该串是的所以会跳到form_unknown标签.
知道了这一点之后我觉得后面的分析其实是比较简单的.如果感觉难了那就回来看看这里.

flags handler

处理flags主要还是要看step0_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
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
      /* Get current character in format string.  */
JUMP (*++f, step0_jumps);

/* ' ' flag. */
LABEL (flag_space):
space = 1;
JUMP (*++f, step0_jumps);

/* '+' flag. */
LABEL (flag_plus):
showsign = 1;
JUMP (*++f, step0_jumps);

/* The '-' flag. */
LABEL (flag_minus):
left = 1;
pad = L_(' ');
JUMP (*++f, step0_jumps);

/* The '#' flag. */
LABEL (flag_hash):
alt = 1;
JUMP (*++f, step0_jumps);

/* The '0' flag. */
LABEL (flag_zero):
if (!left)
pad = L_('0');
JUMP (*++f, step0_jumps);

/* The '\'' flag. */
LABEL (flag_quote):
group = 1;
if (grouping == (const char *) -1)
{
#ifdef COMPILE_WPRINTF
thousands_sep = _NL_CURRENT_WORD (LC_NUMERIC,
_NL_NUMERIC_THOUSANDS_SEP_WC);
#else
thousands_sep = _NL_CURRENT (LC_NUMERIC, THOUSANDS_SEP);
#endif

grouping = _NL_CURRENT (LC_NUMERIC, GROUPING);
if (*grouping == '\0' || *grouping == CHAR_MAX
#ifdef COMPILE_WPRINTF
|| thousands_sep == L'\0'
#else
|| *thousands_sep == '\0'
#endif
)
grouping = NULL;
}
JUMP (*++f, step0_jumps);

LABEL (flag_i18n):
use_outdigits = 1;
JUMP (*++f, step0_jumps);

通过观察标签内语句也可以看出flag主要是通过对之前声明的各种变量赋值来储存flag信息.

小结一下flags的处理主要依靠step0_jumps,处理代码主要是对变量赋值将信息储存,之后输出时再查看变量来实现格式化输出,因为每个标签结束还是JUMP (*++f, step0_jumps);所以可以允许多个flag同时起作用.(实验了一下发现的确可以%+#100lx).

width handler

在上一个步骤中的step0_jumps中可以看到:

1
2
REF (width_asterics),	/* for '*' */				      \
REF (width), /* for '1'...'9' */ \

也就是如果遇到[0-9]*就会进入处理width的环节
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
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
    /* Get width from argument.  */
LABEL (width_asterics):
{
const UCHAR_T *tmp; /* Temporary value. */

tmp = ++f;
if (ISDIGIT (*tmp))
{
int pos = read_int (&tmp);

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);

/* Negative width means left justified. */
if (width < 0)
{
width = -width;
pad = L_(' ');
left = 1;
}
//负数转正
if (__glibc_unlikely (width >= INT_MAX / sizeof (CHAR_T) - 32))
{
__set_errno (EOVERFLOW);
done = -1;
goto all_done;
}
//实验了一下 x64:INT_MAX==0x7fffffff
if (width >= WORK_BUFFER_SIZE - 32)
{
/* We have to use a special buffer. The "32" is just a safe
bet for all the output which is not counted in the width. */
size_t needed = ((size_t) width + 32) * sizeof (CHAR_T);
if (__libc_use_alloca (needed))
workend = (CHAR_T *) alloca (needed) + width + 32;
else
{
workstart = (CHAR_T *) malloc (needed);
if (workstart == NULL)
{
done = -1;
goto all_done;
}
workend = workstart + width + 32;
}
}
}
//上面这一块中超出`WORK_BUFFER_SIZE-32`的话会调用`malloc`或者 `alloca`
//其中enum { WORK_BUFFER_SIZE = 1000 };一开始的时候printf会申请一块0x400的heap来当作WORK_BUFFER
//所以利用`printf`改写`__malloc_hook`直接利用大width调用malloc还是可以的,实现条件是width大于当前的`space`那么保险起见可以设置为INT_MAX-33.
JUMP (*f, step1_jumps);
//如果存在*那么width部分就结束了直接进入下一部分.
/* Given width in format string. */
LABEL (width):
width = read_int (&f);

if (__glibc_unlikely (width == -1
|| width >= INT_MAX / sizeof (CHAR_T) - 32))
{
__set_errno (EOVERFLOW);
done = -1;
goto all_done;
}

if (width >= WORK_BUFFER_SIZE - 32)
{
/* We have to use a special buffer. The "32" is just a safe
bet for all the output which is not counted in the width. */
size_t needed = ((size_t) width + 32) * sizeof (CHAR_T);
if (__libc_use_alloca (needed))
workend = (CHAR_T *) alloca (needed) + width + 32;
else
{
workstart = (CHAR_T *) malloc (needed);
if (workstart == NULL)
{
done = -1;
goto all_done;
}
workend = workstart + width + 32;
}
}
//同为*情况类似 读取int 判断是否越界 判断当前buffer是否足够 不足够则补充
if (*f == L_('$'))
//如果数字后面接的是$说名当前数字表示的不是width而是`positional parameter`那么跳到定位模块.
//为啥放在这里不放在前面..
/* Oh, oh. The argument comes from a positional parameter. */
goto do_positional;
//否则width 处理完毕 进入下一阶段
JUMP (*f, step1_jumps);

源码中放了我自己读源码时候留下的注释.总结一下width处理部分主要做的工作:

防止图片丢失,文字版如下
防止图片丢失,文字版如下

1
2
3
4
5
6
7
8
9
10
11
12
13
Width
width_asterics
确定width
检查是否合法
调整work_buffer
完成width分析
width
确定width
检查是否合法
调整work_buffer
是否positional parameter
是,转跳do_positional
否,完成width分析

这么看来width的确定还是挺简单的…没什么复杂的东西…

其中的malloc还是挺nice的,虽然我之前做过一题0ctf的就是利用的这个malloc,之前只是看wp没有去关注源码…

.precision handler

精度部分处理的处理因为和之前的宽度类似我简单带过.粗粗看了一遍发现基本和width一样得到的结果放在prec

REF (precision), /* for '.' */

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
   LABEL (precision):
++f;
if (*f == L_('*'))
{
const UCHAR_T *tmp; /* Temporary value. */

tmp = ++f;
if (ISDIGIT (*tmp))
{
int pos = read_int (&tmp);

if (pos == -1)
{
__set_errno (EOVERFLOW);
done = -1;
goto all_done;
}

if (pos && *tmp == L_('$'))
/* The precision comes from a positional parameter. */
goto do_positional;
}
prec = va_arg (ap, int);

/* If the precision is negative the precision is omitted. */
if (prec < 0)
prec = -1;
}
else if (ISDIGIT (*f))
{
prec = read_int (&f);

/* The precision was specified in this case as an extremely
large positive value. */
if (prec == -1)
{
__set_errno (EOVERFLOW);
done = -1;
goto all_done;
}
}
else
prec = 0;
if (prec > width && prec > WORK_BUFFER_SIZE - 32)
{
if (__glibc_unlikely (prec >= INT_MAX / sizeof (CHAR_T) - 32))
{
__set_errno (EOVERFLOW);
done = -1;
goto all_done;
}
size_t needed = ((size_t) prec + 32) * sizeof (CHAR_T);

if (__libc_use_alloca (needed))
workend = (CHAR_T *) alloca (needed) + prec + 32;
else
{
workstart = (CHAR_T *) malloc (needed);//精度过高也要...
if (workstart == NULL)
{
done = -1;
goto all_done;
}
workend = workstart + prec + 32;
}
}
JUMP (*f, step2_jumps);
//处理prec完毕 进入下一阶段

可以看出在处理width和prec时都会坚持是否当前数字表示的为位置参数(x$),从而决定是否跳do_positional处理.

mod handler

此时已经是使用的是step2_jumps发现这部分也非常易于理解先处理h再处理l和其他的这里简单起见我只写上常用的.其他的像是
L,q,z,Z,t,j用到再查.

fmtstr mod
h is_short
hh is_char
l is_long
ll is_long_double,is_long
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

/* Process 'h' modifier. There might another 'h' following. */
LABEL (mod_half):
is_short = 1;
JUMP (*++f, step3a_jumps);

/* Process 'hh' modifier. */
LABEL (mod_halfhalf):
is_short = 0;
is_char = 1;
JUMP (*++f, step4_jumps);

/* Process 'l' modifier. There might another 'l' following. */
LABEL (mod_long):
is_long = 1;
JUMP (*++f, step3b_jumps);

/* Process 'L', 'q', or 'll' modifier. No other modifier is
allowed to follow. */
LABEL (mod_longlong):
is_long_double = 1;
is_long = 1;
JUMP (*++f, step4_jumps);

LABEL (mod_size_t):
is_long_double = sizeof (size_t) > sizeof (unsigned long int);
is_long = sizeof (size_t) > sizeof (unsigned int);
JUMP (*++f, step4_jumps);

LABEL (mod_ptrdiff_t):
is_long_double = sizeof (ptrdiff_t) > sizeof (unsigned long int);
is_long = sizeof (ptrdiff_t) > sizeof (unsigned int);
JUMP (*++f, step4_jumps);

LABEL (mod_intmax_t):
is_long_double = sizeof (intmax_t) > sizeof (unsigned long int);
is_long = sizeof (intmax_t) > sizeof (unsigned int);
JUMP (*++f, step4_jumps);

发现好像每一阶段都有些类似,源码都很简单,这里就不多赘述.

format hander

终于到了对类型进行处理.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
     /* Process current format.  */
while (1)
{
process_arg (((struct printf_spec *) NULL));//null
process_string_arg (((struct printf_spec *) NULL));

//下面是遇到form_unknown后的状况
LABEL (form_unknown):
if (spec == L_('\0'))//检查后面是否还有需要分析的没有的话done=-1退出
{
/* The format string ended before the specifier is complete. */
__set_errno (EINVAL);
done = -1;
goto all_done;
}
//有东西跳 do_positional试试.
/* If we are in the fast loop force entering the complicated
one. */
goto do_positional;
}

其中
do_positional:该标签下又一个400多行的函数,暂未分析,猜测和确定参数位置有关(前面出现$的时候都转跳到这里)
process_arg:400多行的宏,用来对不同的type进行处理.
process_string_arg:120多行的宏,通过之前分析flags,width,.prec,mod得到的各种参数来确定输出字符串格式.

type有多种分析起来差不多,我挑%n&%a来分析这两个对我来说比较神奇.

  • %n 标签如下

    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
       LABEL (form_number):						      \
    if (s->_flags2 & _IO_FLAGS2_FORTIFY) \
    { \
    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"); \
    }\
    //此处和FORTIFY保护有关系主要检测fp->_flags2来判断是否开启,之后在FORTIFY相关题目中补充
    /* 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; \
    //通过mod确定改写的长度.写入完成.
  • %a 标签如下
    可以看出它比其他的type能多输出内容的原因

    1
    2
    n132>>> p sizeof(long double)
    $4 = 0x10

结构也比较清晰这里就不多加注释(原因其实是我快睡着了.)

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
LABEL (form_floathex):						      \
{ \
/* Floating point number printed as hexadecimal number. */ \
const void *ptr; \
int function_done; \
\
if (fspec == NULL) \
{ \
if (__ldbl_is_dbl) \
is_long_double = 0; \
\
struct printf_info info = { .prec = prec, \
.width = width, \
.spec = spec, \
.is_long_double = is_long_double, \
.is_short = is_short, \
.is_long = is_long, \
.alt = alt, \
.space = space, \
.left = left, \
.showsign = showsign, \
.group = group, \
.pad = pad, \
.extra = 0, \
.wide = sizeof (CHAR_T) != 1 }; \
\
if (is_long_double) \
the_arg.pa_long_double = va_arg (ap, long double); \
else \
the_arg.pa_double = va_arg (ap, double); \
ptr = (const void *) &the_arg; \
\
function_done = __printf_fphex (s, &info, &ptr); \
} \
else \
{ \
ptr = (const void *) &args_value[fspec->data_arg]; \
if (__ldbl_is_dbl) \
fspec->info.is_long_double = 0; \
\
function_done = __printf_fphex (s, &fspec->info, &ptr); \
} \
\
if (function_done < 0) \
{ \
/* Error in print handler; up to handler to set errno. */ \
done = -1; \
goto all_done; \
} \
\
done_add (function_done); \
} \
break; \

.

输出到下一个%前的其他内容完成此次循环,进而分析下一个格式化串.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
      /* The format is correctly handled.  */
++nspecs_done;

if (__glibc_unlikely (workstart != NULL))
free (workstart);
workstart = NULL;

/* Look for next format specifier. */
#ifdef COMPILE_WPRINTF
f = __find_specwc ((end_of_spec = ++f));
#else
f = __find_specmb ((end_of_spec = ++f));
#endif

/* Write the following constant string. */
outstring (end_of_spec, f - end_of_spec);

至此已经对vfprintf完成了分析虽然粒度没有特别细但是足以了解vfprintf解析格式化串的过程,有些递归预测分析的味道但是实际上也是由表来驱动的,咋感觉起来两个都不像(写printf的人可能也没考虑…)….我怕不是啥都没学到…感觉要点就是他的用哪一个表表示在哪一个状态这个理解之后就整个函数看起来和蔼可亲了…

epilogue

#最近学的单词居然用上了orz…
以io开始,以基本的io,较为复杂的io热身引入,主要分析了复杂的io(printf)。

发现事实上本来看起来很长的代码在知道结构之后感觉其实很简单.事实告诉我们编译原理要好好学.
暂时结束.可能日后在后面加上相关拓展知识的题目和一些深入的探索.

REFERENCE

@mut3p1g的分析 省下了了很多时间,更快地弄清楚整个函数结构