格式化字符串漏洞原理详解

菜鸡刚学总结下,方便复习。

理解这个漏洞的原理,你需要有汇编层面的函数调用和函数的参数传递知识。如果你不清楚函数的参数是如何传递的,可以看《加密与解密》的逆向分析技术篇,也可以参考我博客里的(https://blog.csdn.net/qq_43394612/article/details/84332149)

再说格式化字符串漏洞之前,先了解一下printf函数和利用该漏洞的重要 格式化字符串%n,利用他可以做到任意内存写入。

函数原型

int printf (“格式化字符串”,参量… )
函数的返回值是正确输出的字符的个数,如果输出失败,返回负值。
参量表中参数的个数是不定的(如何实现参数的个数不定,可以参考《程序员的自我修养》这本书),可以是一个,可以是两个,三个…,也可以没有参数
printf函数的格式化字符串常见的有 %d,%f,%c,%s,%x(输出16进制数,前面没有0x),%p(输出16进制数,前面带有0x)等等。
但是有个不常见的格式化字符串 %n ,它的功能是将%n之前打印出来的字符个数,赋值给一个变量。

除了%n,还有%hn,%hhn,%lln,分别为写入目标空间2字节,1字节,8字节。 注意是对应参数(这个参数是指针)的对应的地址开始起几个字节。不要觉得%lln,取的是8个字节的指针,%n取的就是4个字节的指针,取的是多少字节的指针只跟 程序的位数有关,如果是32位的程序,%n取的就是4字节指针,64位取的就是8字节指针,这是因为不同位数的程序,每个参数对应的字节数是不同的。

具体事例:
在这里插入图片描述
%n之前打印了5个a,所以n的值变成了5。

了解了这些后就可以说下格式化字符串漏洞了。

漏洞成因和基本原理

正确使用printf是这样的:

#include <bits/stdc++.h>
using namespace std;
int main()
{
  int n=5;
  printf("%d",n);
  return 0;
}

但也有人会懒省事,写成这样:

#include <bits/stdc++.h>
using namespace std;
int main()
{
  char a[]="neuqcsa";
  printf(a);
  return 0;
}

实参与函数形参的结合顺序是从左往右依次进行的,所以上面的代码也能输出:
在这里插入图片描述
上面的代码不会有什么问题,但是如果将字符串的输入权交给用户就会有问题了。看下面的代码:

#include <bits/stdc++.h>
using namespace std;
int main()
{
  char a[100];
  scanf("%s",a);
  printf(a);
  return 0;
}

如果用户输入的字符串是"%x%x%x",则会输出以下结果
在这里插入图片描述
输出的结果是 内存中的数据。

看一下调用printf函数后的堆栈图:(cdecl调用方式,参数从右往左依次入栈)
在这里插入图片描述
在OD中可以清晰的看到:
在这里插入图片描述

这是因为printf函数并不知道参数个数,它的内部有个指针,用来索检格式化字符串。对于特定类型%,就去取相应参数的值,直到索检到格式化字符串结束。

所以尽管没有参数,上面的代码也会将format string 后面的内存当做参数以16进制输出。这样就会造成内存泄露。

任意内存的读取及任意内存写入:

任意的内存的读取需要用到格式化字符串 %s,其对应的参量是一个指向字符串首地址的指针,作用是输出这个字符串。

在说任意内存的读取之前要知道 局部变量是存储在栈中,这点很关键。所以一定可以找到我们所输入的格式化字符串。
例:

#include<stdio.h>
#include<stdlib.h>
int main()
{
  char a[100];
  scanf("%s",a);
  printf(a);
  system("pause");
  return 0;
}

可以得到以下结果
在这里插入图片描述
看下堆栈图:
这是调用scanf函数前的堆栈图。
在这里插入图片描述
输入字符串后的堆栈图:
在这里插入图片描述
调用printf函数的过程:

mov eax,数组首地址
push eax  
call printf

该过程只是将数组的首地址入栈,此时堆栈图如下。
在这里插入图片描述
所以在格式化字符串里用很多的%x 就一定可以找到这个AAAA的位置。我们将这个位置记下来,实例中就是第七个%x的位置,即第7个参数。

这里说下可以直接读取第七个参数的方法。(在linux下有用,win下没用)
%< number>$x 是直接读取第number个位置的参数,同样可以用在%n,%d等等。
但是需要注意64位程序,前6个参数是存在寄存器中的,从第7个参数开始才会出现在栈中,所以栈中从格式化串开始的第一个,应该是%7 $n.

下面用ubuntu的环境演示下。
在这里插入图片描述
图中是第六个参数是41414141。
在这里插入图片描述
同样可以得到41414141。这样就方便的多了。

读取内存

有了上面内容的铺垫就可以学任意读取了:
看下面的代码:
在这里插入图片描述
从命令行输入字符串后,将该字符串复制到a内,再直接打印a;
输入的字符串的前4个字节如果是一个有效的字符串的首地址,就可以用%s将其打印出来,做到任意内存读取。如果不是有效的字符串,会出现段错误。
在这里插入图片描述
如何写入地址,需要用到linux自带的printf命令,将shellcode编码转义为字符。(注意用反引号将printf命令括住,反引号在Tab键的上面,反引号内的内容会被当做命令执行。)
如果是用scanf输入字符串,则无法使用printf命令,只能对照ascii码表,scanf和命令行输入的shellcode编码不能直接被转义。(所以为了方便演示,后面都使用了命令行输入参数)
写入地址实例:
在这里插入图片描述
0x41414141这个地址已经成功写入内存,下面只需用%s读取对应位置,就能读取以0x41414141为首地址的字符串。
如果用%n就能将0x41414141这个地址指向的值修改,就能造成任意内存的修改,可以将栈中返回地址修改为想要执行的shellcode的首地址等等。

修改内存

下面写个修改静态变量的例子
例:
在这里插入图片描述
测试前,请先关闭内存地址随机化(PIE),否者b在内存中的地址是不确定的。
先运行下,得到b的地址
在这里插入图片描述
接着确定偏移量
在这里插入图片描述
这里是第九个参数。
接着用shellcode编码将b的地址写入,并查看能否写入成功。
在这里插入图片描述
用%n修改其值。
在这里插入图片描述
因为%n之前打印了75个字符,所以这里将b的值从0修改为75

你也可以通过%< number >$n 来直接修改第九个参数来修改b的值。注意在命令行输入字符串参数时,要用 " \ "将 $ 转义,例如:
在这里插入图片描述
在%n之前打印了4个字符,所以b的值直接被修改为4了。
你可以通过控制打印的字符个数来修改b的值,达到几乎任意修改。
例如%0xxxxxd,通过打印数字前面补0,进行简化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章