安全矩阵

 找回密码
 立即注册
搜索
查看: 3325|回复: 0

极其详细的格式化字符串漏洞调试

[复制链接]

991

主题

1063

帖子

4315

积分

论坛元老

Rank: 8Rank: 8

积分
4315
发表于 2021-2-27 21:27:40 | 显示全部楼层 |阅读模式
本帖最后由 gclome 于 2021-2-27 21:30 编辑

原文链接:极其详细的格式化字符串漏洞调试


网上一搜的文章都很浅,越看疑惑越多,于是自己写了个和网上一样的程序,一步一步调试。

给绝对萌新的说明:黑漆漆的图片里的数据代表了栈结构。

正常程序:

gcc -m32 -o normal_stringpwn normal_stringpwn.c -no-pie

漏洞程序:



在调试之前,用checksec看了一下,发现程序默认开启了PIE保护,于是就顺其自然,分别调试有PIE保护与无保护的程序。

编译两个版本:
gcc -m32 -o stringpwn stringpwn.c
gcc -m32 -o stringpwn1 stringpwn.c -no-pie

进入gdb,在printf处下断点,运行。
任意读
32位正常程序栈底层分析


萌新解惑:加入“aaaa”是为了方便定位我们的数据在栈上的位置,不然很难从一大堆二进制识别出字符串的位置。

正常程序输入aaaa%x%x,进入printf时的栈情况:


参数从图中栈第二个开始,分别是格式化字符串、%s、%d(0x400==1024)、

%f(100 0000 0100 0100 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000==0x4044 0000 0000 0000)

用之前写过的《浮点数底层验证》,可以计算得出此二进制串解析为double为1.25*(100 0000 0100)-2^(11-1)-1=1.25*32=40。

正好就是我们输入的小数40,其中,double形参占用两个栈单位,其值分别为0x00000000与0x40440000。

而接下来又一个%s,那是因为调用printf前和调用中会调用很多字符串处理函数,于是此格式化字符串会在栈上重复出现。

32位漏洞程序栈底层分析


PIE漏洞程序:

无保护程序:


本文无关:

顺便打印了此时程序的esp下50个数据,感觉onegadget的条件其实也不会太难符合,栈上NULL很多。

对比正常程序的栈可以得知参数从图中第二个栈单位开始。

从两个程序的栈来看,漏洞程序并不会如正常程序一样,将格式化参数压栈,只会压入漏洞字符串,也就是,程序在读取到printf函数的可变参数时,才会解析并压入栈,l而不是读取到格式化字符串中的格式字符就压栈。这对我们接下来判断偏移量提供了一些信息。

32位验证阶段


从上面的图可得,一个程序开启和不开启PIE,运行某一个程序位置时栈上对应的数据用途都是相同的,如存储参数的栈位置,开启PIE后同位置还是存储参数。

所以,接下来验证前面的判断时两个漏洞程序的结果是等效的。

我们试着运行漏洞程序:

无保护漏洞程序:

分别输出ff84c826与0,第一个%x不固定是因为常量地址不固定,而不是PIE。

对比

可以发现格式化字符从第一个压栈参数开始读,第二个%x输出0,而参数部分栈的第二个地址确实也是0。

再运行PIE漏洞程序加强验证:

虽开启了PIE但输出的两个值确实和前面的图中栈的数据格式相对应。

所以,理论上们可以读取格式化字符串前每一个地址的数据。

但是,如果要读取第100个地址的内容,那就相当麻烦了,不说我们要输入100个%x,若字符串变量长度不足,也会崩毁栈致使程序崩溃。

(鼠标滚到开头你会发现字符串数组只有10个字节)

鉴于此,另辟蹊径,我们可以利用%{n}$x来快捷打印离离第一个参数开始第n-1个地址的数据。

如观察


我们发现字符串后面四个字符在0xffffd2e8开始,而%1$x就是压入栈的格式化字符串参数且位于0xffffd2d0,以此为基础类推,%6$x就是“aa%x%x”的地址,来试试看:


(注意,我们上面的分析都是基于输入“aaaa%x%x”,而接下来我们会改变输入,比如“aaaa%6$x”,虽然参数不一样了,但是经过调试,除了栈上字符串变化之外其他数据都一致。)


%1$s打印栈上以第一个参数(也就是格式化字符串参数)第1-1个(也就是参数本身)地址对应的字符串:




成功!


%6$x以十六进制打印以第一个参数(也就是格式化字符串参数)第6-1个地址存放的数值:

结果:






所以0x25和0x36确实是‘%’和‘6’的ascii编码。



这就是任意读。



64位栈与寄存器分析


而64位程序分析也是类似,仍然是同一份代码,但是这次不指定-m32,而是直接编译成64位。


需要注意的点是64位中函数前6个参数不是压栈而是存入寄存器(格式化字符串作为第一个参数传入rdi):

栈:

对比:

发现了没有?


可知,函数参数并没有入栈,栈上的字符串是因为字符串变量是局部变量,本身就在栈上,32位也是如此,输入的字符串存放于栈上,而参数则是字符串在栈的地址,很合理。


有了之前的经验,这次%1$s打印出来的应该是栈上第一个参数吧?也就是图中rdi-6那个位置,对吗?

很明显不是,这个a哪来的?网上一番搜罗与调试,偶然瞟到:

回想一番,按逻辑推导,函数读取第二个参数应该是从rsi读取,所以%1$x应该是读取第二个参数寄存器rsi!


类似的,

当参数少于7个时, 参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9。
如上两图所示,完美契合。

而从第六个偏移开始,相当于“第七个参数”,于是操作系统把目光放回了栈:

因为默认开了PIE的缘故,栈上除了我们输入的字符外其他大多都在变化,这也就是为什么要加个前缀“aaaa”了,对眼睛好一点。


此时,printf栈帧前很干净,除了我们输入的局部变量s的数据外,什么都没有(我知道上面还有一个main+49,那是printf栈帧的......),于是栈上对应第七个参数就是printf栈帧前的数据,也就是我们的唯一的局部变量s:

除了栈上字符串变量“上面”的一些参数没了,其他都和32位差别不算很大(64位printf一样会调用其他函数且需要存放一样数量的参数,但是64位下,printf调用的每个底层函数的参数个数都没有超过6个,所以栈上除了那唯一的局部变量数据外什么参数都没有,没有压进来)。


总结一下就是


32位:
从栈帧第一个参数(格式化字符串)往高地址,%x以4字节为单位



64位:
%1-5$lx:



rsi,rdx,rcx,r8,r9


%6-...$lx: 从printf族函数栈帧前,也就是主调函数的局部变量们开始,%lx以8字节为单位(一般情况是这样,毕竟系统函数参数很少超过6个,我太菜了还没见过)。


可能会有人问那是怎样定位“第七个参数”的呢?答案就是ebp。


补充意外条件



如果格式化字符串下面恰好有其他的字符串,那函数就会误以为此为参数。

下面那个aaaa是程序中上一个printf的参数......没有被pop掉。



任意写概述


任意写威力很大,比如覆盖got表,但是出现的情况很少。


还是printf,它有一个格式字符%n,可以把%n之前打印成功的字符个数赋值给某个变量:

  1. <font size="3">int a = 0;
  2. printf("%.44d%n\n", a,&a);
  3. printf("a: %d\n", a);
  4. //a为44</font>
复制代码



基础知识就只是如此,任意写的基本思路就是,先任意读取到我们输入的格式化字符串的位置(不是格式化字符串参数所在位置!是所存放在局部变量的位置)。


%n格式符会取变量地址并且存入数据,就如同上面任意写时%7$x可以读到我们的局部变量所在位置一样(相当于直接“接触字符串”)并且打印出对应字符的ascii码一样:


我们可以在字符串中写入地址,并且用任意读的原理调试出%{偏移量}$n,而%n会将当前字符个数存进字符串中的指定地址。


但是若需往该地址写入10000怎么办?这种情况下会打印出10000个字符,可能有人会说,也许%.44d会将44个0压栈,这个倒是不会的。


搜罗搜罗一些exp,发现都是将数据一字节一字节写入,而%n也有限制符:
%$hn写入2字节,%$hhn写入1字节,%$lln写入8字节,在32位和64位环境下一样。


直接写4字节会导致程序崩溃或等候时间过长,比如我开着wsl2调试任意读的时候,作死objdump了一个libc库,在川流不息的汇编流下,我用gdb调试完成并且打算截图的一片良好光景,崩了。


当然若缓冲区长度不够就不能如此浪费了,有一点是一点,但是有时候可以结合栈溢出循环调用。


需要结合ida+pwntools调试才能直观,然鹅有砖要搬,等下回再开新篇。


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-28 22:54 , Processed in 0.014489 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表