大数跨境
0
0

PWN-格式化字符串漏洞一

PWN-格式化字符串漏洞一 豆豆咨询
2023-10-10
2
导读:printf函数进行格式化输出时,会根据格式化串中的格式化控制符在栈上取相应的参数,按照所需格式进行输出。即使函数调用没有给出输出数据列表,但系统仍按照格式化串中指明的方式输出栈中数据。

一、查看这段代码的执行结果,解释%.20d 和%hn 的含义。

main() {

  int num=0x41414141;

  printf("Before: num = %#x \n", num);

 printf("%.20d%hn\n", num, &num);

 printf("After: num = %#x \n", num); }

gcc pre.c -o pre ./pre

%.20d是精度修饰符,应用于整型(%d),d 表示以十进制形式输出带符号整数,%.number格式中,当应用于整型值时,它控制最少打印多少位字符。如果数字不够长,则在前面补 0。所以%.20d表示输出长度为 20 的整型量。

%hnh 表示按 2 字节短整型量输出,%n 表示将已输出的字符个数放入到变元指向的变量中。获取print函数va_list指针指向的值,视该值为一个内存地址,然后将数据(视为2字节短整型数)写入该地址。写入的数据为printf已经打印出的字符数。此代码为20,十进制为14


格式字符串

参数 含义 传递为
%d 十进制 (int) value
%u 无符号十进制 (unsigned int) value
%x 十六进制(unsigned int) value
%s 字符串((const) (unsigned) char *) reference
%n 到目前为止写入的字节数, (* int) reference

二、堆栈和格式字符串

  格式化函数的行为由格式字符串控制。该函数检索参数由堆栈中的格式字符串请求。
printf ("a has value %d, b has value %d, c is at address: %08x\n",a, b, &c);

2.1 查看在任何位置的内存

  • 我们必须提供内存的地址。但是,我们无法更改代码:我们只能提供格式字符串。

  • 如果我们在没有指定内存地址的情况下使用 printf(%s),那么 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 ("\x10\x01\x48\x08 %x %x %x %x %s");

  • \x10\x01\x48\x08 是目标地址的四个字节。在 C 语言中,字符串中的 \x10 告诉编译器将十六进制值 0x10 放在当前位置。该值将只占用一个字节。不使用\x,如果我们直接将“10”放在一个字符串中,就会存储字符“1”和“0”的ASCII值。它们的 ASCII 值分别为 49 和 48。

  • %x 使堆栈指针移向格式字符串。

  • 如果 user_input[] 包含以下格式字符串,攻击的工作方式如下:

"\x10\x01\x48\x08 %x %x %x %x %s"


  • 基本上,我们使用四个 %x 将 printf() 的指针移向我们存储在格式字符串中的地址。一旦我们到达目的地,我们将把 %s 交给 print(),让它打印出内存地址 0x10014808 中的内容。函数 printf() 会将内容视为字符串,并打印出字符串,直到到达字符串的末尾(即 0)。

  • user_input[] 和传递给 printf() 函数的地址之间的堆栈空间不适用于 printf()。但是,由于程序中的格式字符串漏洞,printf() 将它们视为与格式字符串中的 %x 匹配的参数。

  • 这种攻击的关键挑战是计算 user_input[] 和传递给 printf() 函数的地址之间的距离。这个距离决定了在给出 %s 之前需要在格式字符串中插入多少个 %x。

2.2 将整数写入内存

  • %n:到目前为止写入的字符数存储在相应参数指示的整数中。

  • int i;

  • printf ("12345%n", &i);


  • 它使用 printf() 将 5 写入变量 i。

  • 使用与在任何位置查看内存相同的方法,我们可以让 printf() 将整数写入任何位置。只需将上例中的 %s 替换为 %n,地址 0x10014808 处的内容就会被覆盖。

  • 使用此攻击,攻击者可以执行以下操作:

    • 覆盖控制访问权限的重要程序标志

    • 覆盖堆栈上的返回地址、函数指针等。

  • 但是,写入的值取决于在达到 %n 之前打印的字符数。真的可以写任意整数值吗?

    • 使用虚拟输出字符。要写入 1000 的值,只需填充 1000 个虚拟字符即可。

    • 为了避免长格式字符串,我们可以使用格式指示符的宽度规范。


#include <stdio.h>
int main()
{
    int id = 100, age = 25;
    char *name = "Bob Smith";
    printf("ID: %d, Name: %s, Age: %d\n", id, name, age);
}


三、利用漏洞

/* vul_prog.c */


#include<stdio.h>

#include<stdlib.h>

#define SECRET1 0x44

#define SECRET2 0x55

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

{

    char user_input[100];

    int *secret;

    int int_input;

    int a, b, c, d; /* other variables, not used here.*/

    /* The secret value is stored on the heap */

    secret = (int *) malloc(2*sizeof(int));

    /* getting the secret */

    secret[0] = SECRET1; secret[1] = SECRET2;

    printf("The variable secret's address is 0x%8x (on stack)\n", (unsigned int)&secret);

    printf("The variable secret's value is 0x%8x (on heap)\n", (unsigned int)secret);

    printf("secret[0]'s address is 0x%8x (on heap)\n", (unsigned int)&secret[0]);

    printf("secret[1]'s address is 0x%8x (on heap)\n", (unsigned int)&secret[1]);

    printf("Please enter a decimal integer\n");

    scanf("%d", &int_input);  /* getting an input from user */

    printf("Please enter a string\n");

    scanf("%s", user_input); /* getting a string from user */

    /* Vulnerable place */

    printf(user_input);

    printf("\n");

    /* Verify whether your attack is successful */

    printf("The original secrets: 0x%x -- 0x%x\n", SECRET1, SECRET2);

    printf("The new secrets:      0x%x -- 0x%x\n", secret[0], secret[1]);

    return 0;

}

上面的程序需要你提供输入,该输入将保存在调用的缓冲区user_input中。然后,程序使用printf打印出缓冲区。该程序是SET-UID程序(所有者是root),即它与root权限运行。不幸的是,有一种格式字符串漏洞以如何在用户输入上调用Printf的方式。我们希望利用此漏洞,看看我们能造成多大的伤害。

该程序有两个存储在其内存中的秘密值,并且您对这些秘密值感兴趣。但是,秘密值对您来说是未知的,您也无法识别它们读取二进制代码(为了简单起见,我们使用常量0x44和0x55来标记秘密。但在实践中,虽然您不知道秘密,但找出他们的内存地址(范围或确切值)并不难,因为他们是连续地址。因为对于许多操作系统,地址是与运行该程序的任何时候完全相同。

在这个实验中,我们只是假设你已经知道了确切的地址。为实现这一目标,程序“故意”打印出您的地址。用这样的知识,您的目标是实现以下内容(不一定在同时):

  • 崩溃程序

  • 打印出secret[1]值

  • 修改secret[1]值

  • 将secret[1]值修改为预先确定的值

请注意,程序的二进制代码(Set-UID)只能由您读取/执行,并且无法修改代码。也就是说,您需要在不修改易受攻击的代码的情况下实现上述目标。但是,您确实有源代码的副本,这可以帮助您设计攻击。

3.1 崩溃程序

程序崩溃,原因是用%s输出地址中的值时,保存在栈中值并不都是合法地址,可能是0,指向受保护内存的地址或者没有映射到物理地址的虚拟地址。当程序试图从一个非法地址获取数据时,程序将崩溃。

3.2  打印出secret[1]值

  • 假设堆栈上的变量包含一个秘密(常量),我们需要将其打印出来。

  • 使用用户输入:%x%x%x%x%x%x%x

  • printf() 打印出va_list指针所指向的整数值,并将其前进 4 个字节。

  • %x 的数目由va_list指针的起点与变量之间的距离决定。它可以通过反复试验来实现。


先用试错法,随机输入一个数。从结果看,secret[1]的地址在第 9个%x。

%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.


以我们只要把秘密信息的地址转化为十进制整数输入,再将第9个%x换为%s即可将该位置的秘密信息输出。

0x84D200C(十六进制) = 139272204(十进制)

%x.%x.%x.%x.%x.%x.%x.%x.%s

U 的 ascii 码就是 55,说明我们已经将 secret[1]的值成功输出。

3.3 修改secret[1]值

%n:将到目前为止打印出来的字符数写入内存

printf("hello%n",&i) ⇒ 当 printf() 到达 %n 时,它已经打印了 5 个字符,因此它将 5 个字符存储到提供的内存地址。

%n 将va_list指针所指向的值视为内存地址并写入该位置。

因此,如果我们想将值写入内存位置,我们需要在堆栈上具有它的地址。

因为 secret[1]的地址已经在栈中,所以可以直接修改它的值。

%n表示该符号前输入的字符数量会被存储到对应的参数中去。在访问任意地址内存的时候,我们可以将一个数字写入指定的内存中。只要将上一小节的 %s 替换成 %n 就能够覆盖原地址的内容。此外将%x更改为%10x,规定不足八位左补空格。

%.8x%.8x%.8x%.8x%.8x%.8x%.8x%.8x%n

前面 8 个%.8x 已经打印了 8*8=64 个字符,而 0x40 就是十进制的 64, 所以 secret[1]的值修改成功。

3.4 修改 secret[1]为指定值

修改 secret[1]为0x6666(十六进制) = 26214 (十进制),
26214-56 = 26158

%.8x%.8x%.8x%.8x%.8x%.8x%.8x%.26158x%n

%
8x%8x%8x%8x%8x%8c%8x%26158x%n

四、利用漏洞(内存随机化)

如果第一个 scanf 语句(scanf("%d",int_input))不存在,即程序不要求您输入整数,则上述攻击对于那些已实现地址随机化的操作系统来说变得更加困难。注意秘密[0](或秘密[1])的地址。当您再次运行该程序时,您会得到相同的地址吗?
引入地址随机化以使许多攻击变得困难,例如缓冲区溢出,格式字符串等。为了理解地址随机化的想法,我们将在此任务中关闭地址随机化,并查看对先前易受攻击的程序(没有第一个scanf语句)的格式字符串攻击是否仍然很困难。您可以使用以下命令关闭地址随机化(请注意,您需要以 root 用户身份运行它):

sysctl -w kernel.randomize_va_space=0

关闭地址随机化后,您的任务是重复任务 1 中描述的相同任务,但您必须从易受攻击的程序中删除第一个 scanf 语句(scanf("%d",int input))。

如何让scanf接受任意数字?通常,scanf 将暂停以键入输入。有时,您希望程序采用数字0x05(而不是字符"5")。然而,当您在输入端键入"5"时,scanf实际上会接收ASCII值"5",这是0x35,而不是0x05。难点在于,在 ASCII 中,0x05不是典型字符,因此我们无法键入此值。解决此问题的一种方法是使用文件。我们可以很容易地编写一个C程序,将0x05(再次强调,不是"5")存储到一个文件中(让我们称之为mystring),然后我们可以运行易受攻击的程序(让我们称之为a.out),其输入被重定向到mystring;也就是说,我们运行"a.out < mystring"。这样,scanf 将从文件 mystring 而不是从键盘获取其输入。



五、补充说明

1.1 一个例子

#include <stdio.h>

int main(void){

int a=10,b=20,key=0;

printf(“a=%d,b=%d”,a,b);//使用格式化串进行输出

return 0;

}

如果对上述例子中的代码语句进行如下修改:

printf(“a=%d,b=%d”,a,b)改为printf(“a=%d,b=%d”)

那么当程序再次编译后,运行时发现输出结果不再是"a=10,b=20"了,这是为什么呢?

printf函数进行格式化输出时,会根据格式化串中的格式化控制符在栈上取相应的参数,按照所需格式进行输出。即使函数调用没有给出输出数据列表,但系统仍按照格式化串中指明的方式输出栈中数据。

在例子中,修改前,参数a,b正常人栈,所以输出正常;修改后,printf的参数不包括a,b,未能在函数调用时将其入栈,所以当 printf在栈上取与格式化控制符%d相对应的变量时,就不能找到a、b,而是错误地把栈上其他数据当做a、b的值进行了输出。

int a=0; printf("1234567890%n",&a);

对于上面代码,格式化串中指定了%n,此前输出了1~0这10个字符,因此这里将会修改a的值,即向其中写入字符数10。

格式符除了常见的d、f、u、o、x之外,还有一些指针型的格式符:

s————参数对应的是指向字符串的指针;

n——这个参数对应的是一个整数型指针,将这个参数之前输出的字符的数量写入该格式符对应参数指向的地址中。


类似地,恰当利用%p、%s、%n等格式符,一个精心构造的格式化串即可实现对程序内存数据的任意读、任意写,从而造成信息泄露、数据篡改和程序流程的非法控制这类威胁。


除了printf函数之外,其他该系列函数也有可能产生格式化串漏洞:printf,fprintf,sprintf,snprintf,vprintf,vfprintf,vsprintf,wprintf等。格式化串漏洞的利用可以通过如下方法实现:


①通过改变格式化串中输出字符数的多少实现修改要在指定地址写人的值:可以修改填充字符串长度实现;也可以通过改变输出的宽度实现,如%8d。


②通过改变格式化串中格式符的个数,调整格式符对应参数在栈中位置,从而实现对栈中特定位置数据的修改。如果恰当地修改栈中函数返回地址,那么就有可能实现程序执行流程的控制。也可以修改其他函数指针,改变执行流程。


相对于修改返回地址,改写指向异常处理程序的指针,然后引起异常,这种方法猜测地址的难度比较小,成功率较高。


格式化串漏洞是一类真实存在、并且是危害较大的漏洞,但是相对于栈溢出等漏洞而言,实际案例并不多。并且格式化串漏洞的形成原因较为简单,只要通过静态扫描等方法,就可以发现这类漏洞。此外,在VS2005以上版本中的编译级别对参数进行了检查,且默认情况下关闭了对%n控制符的使用。


格式化字符串的特殊用法

1. m$:表示参数列表中的第m个参数,其中m为int类型常数

eg: printf("The heximal value of %d is 0x%1$02x\n", 15);

打印结果为:

    The heximal vaule of 15 is 0x0f

上例中的%1$表明当前转换格式(converion specification)转换的是参数列表中的第一个参数,即15,这样就可以实现在一个printf中重复引用相同的参数,第二个转换格式'%1$02x'中的02x表示当前的转换格式域宽(参看用法2)为2,即至少占用两个字符的宽度,不够的宽度用0填充,02x中的'0'为标志字符(flag character),表示使用0进行填充,默认是用空格(blanks)填充的。


2. %m(conversion specifier):指示转换域宽,其中(conversion specifier)为转换指示字符,如常见的d(表示int),s(表示string),m表示转换后的字符串最小占用的长度,即此次转换后域的宽度,有以下两种情况:


1) m为int类型常数

eg1: printf("The num is %4d.\n", 1);

Result:

    The num is    1.

eg2: printf("The num is %4d.\n", 10000);

Result:

    The num is 10000.

这个例子表明如果域宽m小于转换后字符串实际占用的宽度,转换后的字符串不会被截断,会占用实际的宽度。


2) m为*或*(num)$,*表示下一个参数的值(即下一个待转换的参数),*(num)$表示第num个参数的值,当然这些参数必须是int类型

eg1: printf("The num is %*d.\n", 4, 1);

Result:

    The num is    1.

eg2: printf("The num is %1$*2$d.\n", 1, 4);#*2$表示4宽度

Result:

    The num is    1.

注意:如果用这种格式指定域宽,最好同时指定当前转换参数的索引,即此例中的%1$,参考用法1,否则会有编译Warning。



参考文献

[1] https://blog.csdn.net/cyynid/article/details/129216799?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-1-129216799-blog-120577896.235%5Ev38%5Epc_relevant_sort_base3&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-1-129216799-blog-120577896.235%5Ev38%5Epc_relevant_sort_base3&utm_relevant_index=1

[2] https://www.cnblogs.com/skprimin/p/16032957.html

【声明】内容源于网络
0
0
豆豆咨询
提供前沿的信息技术咨询,实用的编程、项目管理方法,优质的专家服务,如PHP、Java、C#、C++、ASP.NET、ThinkPHP、Git、Matlab、图像处理、数据库、云计算、科技论文撰写等。
内容 44
粉丝 0
豆豆咨询 提供前沿的信息技术咨询,实用的编程、项目管理方法,优质的专家服务,如PHP、Java、C#、C++、ASP.NET、ThinkPHP、Git、Matlab、图像处理、数据库、云计算、科技论文撰写等。
总阅读33
粉丝0
内容44