# pwn 学习笔记(5)–格式化字符串漏洞

前言:由于条件有限,因此对于该漏洞的学习不算很多,

# 格式化字符串漏洞基础:

# 格式化字符串介绍:

​ 格式化字符串函数可以接收可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数,格式化字符串的利用一般分为三个部分:

  • 格式化字符串函数
  • 格式化字符串
  • [后续参数]

# 格式化字符串函数:

​ 常见的格式化字符串有:

输入:

  • scanf()

输出:

函数 基本介绍
printf 输出到 stdout
fprintf 输出到指定 FILE 流
vprintf 根据参数列表格式化输出到 stdout
vfprintf 根据参数列表格式化输出到指定 FILE 流
sprintf 输出到字符串
snprintf 输出指定字节数到字符串
vsprintf 根据参数列表格式化输出到字符串
vsnprintf 根据参数列表格式化输出指定字节到字符串
setproctitle 设置 argv
syslog 输出日志

# 格式化字符串的格式:

1
%[parameter][flags][field width][.precision][length]type

​ 其中,需要注意的有 parameter 参数,以及 type 参数:

parameter:

  n$,获取格式化字符串中的指定参数

type:

  • d/i,有符号整数
  • u,无符号整数
  • 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 所在的地址。
  • n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
  • %, ' % ' 字面值,不接受任何 flags, width。

​ 下面使用几个案例来说明下几个需要注意的参数:

parameter:

  n$ 表示获取后面参数列表中的第几个参数。

​ 比如如下代码:

1
2
3
4
5
#include<stdio.h>
int main(){
printf("%2$d\n",1,2,3);
return 0;
}

​ 通过编译运行之后得到的值却是

1
2
root@g01den-virtual-machine:/home/g01den/Temp# ./a
2

​ 好了,我们稍微修改下刚刚的代码:

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

​ 然后编译为 32 位之后,查看下汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
0000119d <main>:
119d: 8d 4c 24 04 lea 0x4(%esp),%ecx
11a1: 83 e4 f0 and $0xfffffff0,%esp
11a4: ff 71 fc push -0x4(%ecx)
11a7: 55 push %ebp
11a8: 89 e5 mov %esp,%ebp
11aa: 53 push %ebx
11ab: 51 push %ecx
11ac: e8 2b 00 00 00 call 11dc <__x86.get_pc_thunk.ax>
11b1: 05 27 2e 00 00 add $0x2e27,%eax
11b6: 6a 03 push $0x3 <-------------------------------
11b8: 6a 02 push $0x2 <-------------------------------
11ba: 6a 01 push $0x1 <-------------------------------
11bc: 8d 90 30 e0 ff ff lea -0x1fd0(%eax),%edx
11c2: 52 push %edx
11c3: 89 c3 mov %eax,%ebx
11c5: e8 86 fe ff ff call 1050 <printf@plt>
11ca: 83 c4 10 add $0x10,%esp
11cd: b8 00 00 00 00 mov $0x0,%eax
11d2: 8d 65 f8 lea -0x8(%ebp),%esp
11d5: 59 pop %ecx
11d6: 5b pop %ebx
11d7: 5d pop %ebp
11d8: 8d 61 fc lea -0x4(%ecx),%esp
11db: c3 ret

​ 能够发现在我指的那三行,在调用 printf 函数之前是先将参数从右向左压入栈中,之后通过读取栈的参数与格式化字符串一起打印出来。

# 格式化字符串漏洞:

​ 有了前面的那些储备知识,这个时候就可以来看看格式化字符串的漏洞了。

# 初探:

​ 首先,写一个程序,如下内容:

1
2
3
4
5
#include<stdio.h>
int main(){
printf("%x %x %x %x \n",0x1);
return 0;
}

​ 那么,直接进行动态调试,观看 main 的栈帧中相关的信息:

1
2
3
4
5
6
7
00:0000│ esp 0xffffd500 —▸ 0x56557008 ◂— '%x  %x  %x  %x  \n'
01:0004│-014 0xffffd504 ◂— 0x1 <--------------------
02:0008│-010 0xffffd508 —▸ 0xf7fbeb20 —▸ 0xf7c1acc6 ◂— 'GLIBC_PRIVATE' <--------------------
03:000c│-00c 0xffffd50c —▸ 0x565561b1 (main+20) ◂— add eax, 0x2e27 <--------------------
04:0010│-008 0xffffd510 —▸ 0xffffd530 ◂— 0x1 <--------------------
05:0014│-004 0xffffd514 —▸ 0xf7e2a000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac
06:0018│ ebp 0xffffd518 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 —▸ 0x56555000 ◂— 0x464c457f

​ 然后,运行到 printf 函数之后,再看看输出的值是多少:

1
2
pwndbg> n
1 f7fbeb20 565561b1 ffffd530

​ 发现输出的值和箭头指出来的值一样。因此,黑客则可以利用此漏洞配合 **% n$d** 来进行内存的读取了。

# 一次简单的漏洞的实例:

​ 实际上,格式化字符串的漏洞很多时候都是由于程序员们偷懒,才会导致一些漏洞的产生,比如下面的程序,如果写成这个样子:

1
2
3
4
5
6
7
#include<stdio.h>
int main(){
char str[100];
scanf("%s\n",str);
printf("%s\n",str);
return 0;
}

​ 如果是这样的话,程序运行就不会出现太大的问题,但是,当程序员偷懒,使用下面的写法,就会出现问题:

1
2
3
4
5
6
7
#include<stdio.h>
int main(){
char str[100];
scanf("%s",str);
printf(str);
return 0;
}

​ 运行的话就会出现如下的结果:

1
2
3
root@g01den-virtual-machine:/home/g01den/Temp# ./a
aaa.%x
aaa.fff53a28

​ 根据 gdb 调试,就会发现,它输出了比 esp 地址高 4 字节的那一地址的数据。

# 泄露内存:

​ 还是刚才那个程序,其实可以在这个时候多输入几个 % x 来进行父函数栈帧的 esp 高 4 字节的地址开始的多个字节的内存,或者通过 n$ 来进行任意内存的读取:

1
2
3
4
5
6
7
aaa.fff53a28root@g01den-virtual-machine:/home/g01den/Temp# ./a
%x.%x.%x.%x.%x.%x.%x
ff8631d8.0.565a31d4.0.0.252e7825.78252e78

root@g01den-virtual-machine:/home/g01den/Temp# ./a
%3$x
565cf1d4

# 泄露任意地址的内存:

​ 上一个方法只是泄露了栈上的数据,其实,格式化字符串可以对任意内存地址的数据进行泄露。、

​ 攻击者可以使用类似于 "% s" 的格式规范做到泄露栈中存放的值对应的地址的字符串的内容,这里引用下 dalao 的对于 % s 的讲解:

程序会将 % s 指向的地址作为一个 ASCII 字符串处理,直到遇到一个空字符。所以,如果攻击者能够操纵这个参数的值,那就可以泄露任意地址的内容。

或者说

% s 是把地址指向的内存内容给打印出来,可以把 函数的地址给打印出来。

​ 也就是说,想要做到任意地址的内存泄露,就需要想办法对栈上的某个地址的值进行修改为想要获得的那个字符串的地址。

# 覆盖栈内存:

​ % n 不能够输出字符,但是,它能把已经成功输出的字符个数写入对应的整形指针参数所致的变量,只要变量对应的地址可写,就可以利用格式化字符串来改变其对应的值。

一般来说,这种方法利用的步骤为:

  • 确定覆盖地址
  • 确定相对位移
  • 进行覆盖

注:以下为我个人的见解,如有问题,请指正。

​ 首先,之前说过了 % n 这个格式化字符串的作用是啥:

% n, 不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。

​ 之后,用一个程序作为示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
int a = 123, b = 456;
int main() {
int c = 789;
char s[100];
printf("%p\n", &c);
scanf("%s", s);
printf(s);
if (c == 16) {
puts("modified c.");
} else if (a == 2) {
puts("modified a for a small number.");
} else if (b == 0x12345678) {
puts("modified b for a big number!");
}
return 0;
}

​ 这个程序的格式化字符串漏洞的函数很容易就能找到,那就是 printf (s),因此,假设对变量 c 进行修改,就需要用到 % n 对 C 进行修改,所以,格式如下:

c 的地址 + 12 个任意的字符 + 对应格式化字符串的参数的偏移

​ 好了,实际试试看吧:

1
2
3
4
AAAA,$p,$p,$p,$p,$p,$p,$p,$p,$proot@g01den-virtual-machine:/home/g01den/Temp# ./a
0xff8d3ad4
AAA,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p
AAA,0xff8d3ad8,(nil),0x565d51e4,(nil),0x315,0x2c414141,0x252c7025,0x70252c70,0x2c70252c,0x252c7025,0x70252c70

​ 由此可知,传入的格式化字符串 AAA,所对应的参数是第六位,因此,对应格式化字符串对应的偏移为 6,也就是说,值为 %6$n,好的,之后就是 c 的地址,我在学到这里的时候出现了理解不能的问题,最后大致是整明白了,就是不知道对不对。

​ 对于 C 的地址,我认为是,正是因为构造的格式化字符串的前四个字节是 c 的地址,因此,% n 这个格式化字符串在写的时候对应的就是第六个参数,而第六个参数的前四个字节就是 c 的地址,因此就成功修改了 c 这个变量的值;** 那么问题就来了,为啥不能直接指定第五个参数进行修改呢?** 我个人的理解是这样的:因为第五个参数存放的并不是 c 这个变量的地址,是 789,因此,% n 就试图把 16 写入 789 这个地址里去,但是,这个地址要么不存在,要么就是无法访问,则会出现 segment fault,导致程序出现了错误。

# 覆盖小数字:

​ 那么,问题又出现了,因为如果让变量的地址作为开始的字符,就会导致 % n 写入的最小的值也是 4,那么,如果要让某个变量被覆盖为 2,又该怎么做呢?

注:以下对于内容的解释部分均为我自己的理解,如有错误,还请指正。

​ 照理来说,想要某个变量被覆盖为 2 的话,就需要诸如

aa% k$naa + 地址

​ 这样的字符串,所以是为啥呢?

​ 这里借用 julao 们的说法是

aa%knxx,如果用这样的方式,前面aanxx,如果用这样的方式,前面 aa%k 是第六个参数,nxx 是第七个参数,后面在跟一个 我们想要修改的地址,那么这个地址就是第八个参数,只需要把 k 改成 8 就可以把这第八个参数改成 2,aa%8$nxx

​ 我个人的理解为,在栈中,每个字符的长度为 1 字节,另外,对于格式化字符串的参数传递而言,每个参数都占用了四个字节(32 位),所以,这里需要前和中都构成四个字节的字符串,因此,第六位的字符串就是 aa% k,第七位就是 $naa,第八位就可以传入变量的地址了。

后续的内容需要花费一些时间,短时间内先放放,暂时放下不管,之后有时间再来补充。

更新于

请我喝[茶]~( ̄▽ ̄)~*

g01den 微信支付

微信支付

g01den 支付宝

支付宝

g01den 贝宝

贝宝