首页
社区
课程
招聘
[原创]格式化字符串打出没有回头路(下)——回头望月
发表于: 2024-5-28 08:52 49232

[原创]格式化字符串打出没有回头路(下)——回头望月

2024-5-28 08:52
49232

在我的前篇文章的最后提出了以下问题。在只有一次的格式字符串过程中,如果采用-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
#include
#include
 
#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

题目存在的困难如下

  1. 保护模式为Full RELRO,不能攻击fini_array
  2. 程序只能执行一次,因为执行的是_exit函数,所以也无法进行EOP的攻击。
  3. 开启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_table1-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 '' */                  \
      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_FILExsputn函数。代码在/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_FILExsputn函数。

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_TABLESTEP0_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 '' */                  \
      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_internalvfprintf

简单宏定义,即为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


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 7
支持
分享
最新回复 (1)
雪    币: 309
活跃值: (2417)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
不明觉厉
2024-6-1 17:29
0
游客
登录 | 注册 方可回帖
返回