首页
论坛
课程
招聘
[漏洞分析] [字符串格式化] [Andorid] [Windows] [Linux] [原创]及其详细的格式化字符串漏洞调试
2021-2-9 16:57 4892

[漏洞分析] [字符串格式化] [Andorid] [Windows] [Linux] [原创]及其详细的格式化字符串漏洞调试

2021-2-9 16:57
4892

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

 

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

 

正常程序:
20210130205232

 

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

 

漏洞程序:
20210130204009

 

在调试之前,用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时的栈情况:
20210130214810

 

参数从图中栈第二个开始,分别是格式化字符串、%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漏洞程序:
20210130205930

 

无保护程序:
20210130205945

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

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

 

从两个程序的栈来看,漏洞程序并不会如正常程序一样,将格式化参数压栈,只会压入漏洞字符串,也就是,程序在读取到printf函数的可变参数时,才会解析并压入栈,l而不是读取到格式化字符串中的格式字符就压栈。

 

这对我们接下来判断偏移量提供了一些信息。

32位验证阶段

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

 

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

 

我们试着运行漏洞程序:

 

无保护漏洞程序:
20210130231352
分别输出ff84c826与0,
第一个%x不固定是因为常量地址不固定,而不是PIE。

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

如观察20210130233620

 

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

 

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

 

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

 

成功!

 

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

20210131001808
20210131001846

 

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

 

这就是任意读。

64位栈与寄存器分析

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

 

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

 

发现了没有?

 

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

 

有了之前的经验,这次%1$s打印出来的应该是栈上第一个参数吧?
也就是图中rdi-6那个位置,对吗?
20210131005520
很明显不是,这个a哪来的?
网上一番搜罗与调试,偶然瞟到:
20210131005647

 

回想一番,按逻辑推导,函数读取第二个参数应该是从rsi读取,所以%1$x应该是读取第二个参数寄存器rsi!
类似的,
20210131010904
当参数少于7个时, 参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9。
如上两图所示,完美契合。

 

而从第六个偏移开始,相当于“第七个参数”,于是操作系统把目光放回了栈:
20210131011120
因为默认开了PIE的缘故,栈上除了我们输入的字符外其他大多都在变化,这也就是为什么要加个前缀“aaaa”了,对眼睛好一点。

 

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

 

除了栈上字符串变量“上面”的一些参数没了,其他都和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。

补充意外条件

如果格式化字符串下面恰好有其他的字符串,那函数就会误以为此为参数。
20210201025159
下面那个aaaa是程序中上一个printf的参数......没有被pop掉。
20210201024600
20210201024546

任意写概述

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

 

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

1
2
3
4
int a = 0;
printf("%.44d%n\n", a,&a);
printf("a: %d\n", a);
//a为44

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

 

%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调试才能直观,然鹅有砖要搬,等下回再开新篇。


[公告]春风十里不如你,看雪团队诚邀你的加入!

收藏
点赞4
打赏
分享
最新回复 (3)
雪    币: 15739
活跃值: 活跃值 (14106)
能力值: (RANK:75 )
在线值:
发帖
回帖
粉丝
Editor 活跃值 2021-2-9 17:05
2
0
感谢分享!
雪    币: 6714
活跃值: 活跃值 (4947)
能力值: ( LV12,RANK:210 )
在线值:
发帖
回帖
粉丝
pureGavin 活跃值 2 2021-2-9 19:56
3
0

优秀!补全了我没注意到的

最后于 2021-2-10 18:54 被pureGavin编辑 ,原因: 错字
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
smallseven 活跃值 2021-2-27 22:42
4
0
多谢分享。另可以指教下linux的自学方向吗,跪谢
游客
登录 | 注册 方可回帖
返回