Pwn-fsb

1 minute read

Pwn 格式化字符串漏洞fsb总结

0x00 printf 函数中的漏洞

printf 函数,可以说是学习C语言接触到的第一个C语言语句,他的语法格式:

int printf ( const char * format, ... );

Writes the C string pointed by format to the standard output (stdout). If format includes format specifiers (subsequences beginning with %), the additional arguments following format are formatted and inserted in the resulting string replacing their respective specifiers.

A format specifier follows this prototype: %[flags][width][.precision][length]specifier

这里我只列举一些常用的,有兴趣的可以自行Google

specifier type meaning example
x/X unsigned int 无符号16进制整数,x对应的是abcdef,X对应的是ABCDEF(不输出前缀0x) printf("0x%x 0x%X",123,123);输出0x7b 0x7B
n int * 什么也不输出。%n对应的参数是一个指向signed int的指针,在此之前输出的字符数将存储到指针所指的位置 int num=0;
printf("lvlv%n",&num);
printf("num:%d",num);
输出:lvlvnum:4
%d - 十进制 - 输出十进制整数
%s - 字符串 - 从内存中读取字符串
%x - 十六进制 - 输出十六进制数
%c - 字符 - 输出字符
%p - 指针 - 指针地址
%n - 到目前为止所写的字符数

我们平时一般都会这么使用:

char s[20] = "Hello World\n";
printf("%s",s);

但是有人为了省事,写成了下面这样:

char s[20] = "Hello World\n";
printf(s);

这时,如果s的内容是可以被控制的,那么就会出现格式化字符串漏洞。

0x01 利用格式化字符串漏洞实现任意地址读取

先来看一段有漏洞的代码:

int main(){
    char buf[20];
    scanf("%s",buf);
    printf(buf);
    return 0;
}

这段代码很简单,就是把输入给输出了一遍。但是当我们输入一些格式化字符串时,printf就会把它当做格式化字符串进行解析并输出。 首先需要判断输入是栈中的第几个参数,然后就可以操纵输入,对相应位置的参数进行操作。 比如打印信息,或者写入数据。

这样我们就可以利用此漏洞实现任意地址读取:

特殊情况:有时候输入的长度会受到限制,这时可以使用格式化字符串的另一个特性:

格式化字符串可以使用一种特殊的表示形式来指定处理第n个参数,如输出第五个参数可以写为%4$s,第六个为%5$s,需要输出第n个参数就是%(n-1)$[格式化控制符]。

泄露栈上内容
32bit:   %n$x : 返回栈上第(n+1)个参数的值
64bit:   %n$p 或者 %n$llx (64bit) :返回栈上第(n-5)个参数的值
泄露内存地址的内容
32bit:   %n$s:把栈上第n+1个参数的值作为地址,返回该地址内存的值
64bit:   %n$s:把栈上第n-5个参数的值作为地址,返回该地址内存的值

0x02 使用格式化字符串漏洞任意写

为了getshell,需要对内存的某个地址进行写入操作,这时可以利用格式化字符串漏洞进行任意地址写入。这里主要利用的是%n这个格式化控制符,它的作用前面已经介绍过了,可以把已输出的字符数写入到对应参数的内存中。

例如,劫持printf的got表为system,然后执行system(“/bin/sh”)。payload如下:

printf_got = 0x08049778
system_plt = 0x08048320
payload = p32(printf_got)+%+str(system_plt-4)+c%5$n  //p32(printf_got)占了4字节,所以system_plt要减去4

在64位下有%lld,%llx等方式来表示四字(qword)长度的数据,而对称地,我们也可以使用%hd, %hhx这样的方式来表示字(word)和字节(byte)长度的数据,对应到%n上就是%hn,%hhn。因此使用%hhn时我们就必须一次修改四个字节。那么我们就得重新构造一下payload.

printf_got = 0x08049778
system_plt = 0x08048320
 
payload = p32(printf_got)
payload += p32(printf_got+1)
payload += p32(printf_got+2)
payload += p32(printf_got+3)
payload += %
payload += str(0x20-16)
payload += c%5$hhn
payload += %
payload += str(0x83-0x20)
payload += c%6$hhn
payload += %
payload += str(0x104-0x83)
payload += c%7$hhn
payload += %
payload += str(0x08-0x04)
payload += c%8$hhn

当然,对于格式化字符串payload,pwntools也提供了一个可以直接使用的类Fmtstr,具体文档见http://docs.pwntools.com/en/stable/fmtstr.html,我们较常使用的功能是:

fmtstr_payload(offset, {address:data}, numbwritten=0, write_size='byte')

第一个参数offset是第一个可控的栈偏移(不包含格式化字符串参数),代入我们的例子就是第六个参数,所以是5。第二个字典看名字就可以理解,numbwritten是指printf在格式化字符串之前输出的数据,比如printf(“Hello [var]”),此时在可控变量之前已经输出了”Hello”共计六个字符,应该设置参数值为6。第四个选择用 %hhn(byte), %hn(word)还是%n(dword)

在我们的例子里就可以写成:

fmtstr_payload(5, {printf_got:system_plt})
 %***c%n$n:  把栈上第n+1个参数的值作为地址,将该地址的高32bit值改为 hex(***)
 %***c%n$hn: 把栈上第n+1个参数的值作为地址,将该地址的高16bit值改为 hex(***)
 %***c%n$hhn:把栈上第n+1个参数的值作为地址,将该地址的高8bit值改为 hex(***)
[64bit下,(n+1)变为(n-5)即可 ]

0x03 64位下的格式化字符串漏洞利用

64位的传参顺序是rdi, rsi, rdx, rcx, r8, r9,接下来才是栈,所以这里的偏移应该是6.

我们还需要调整一下payload,使地址前面的数据恰好为地址长度的倍数。当然,地址所在offset也得调整。调整后的结果如下:

offset = 8
printf_got = 0x00601020
system_plt = 0x00400460
 
payload = a% + str(system_plt-1) + c%6$lln + p64(printf_got)

0x04 使用格式化字符串漏洞使程序无限循环

简单地说,在main函数前会调用.init段代码和.init_array段的函数数组中每一个函数指针。同样的,main函数结束后也会调用.fini段代码和.fini._arrary段的函数数组中的每一个函数指针。

而我们的目标就是修改.fini_array数组的第一个元素为start。需要注意的是,这个数组的内容在再次从start开始执行后又会被修改,且程序可读取的字节数有限,因此需要一次性修改两个地址并且合理调整payload。可用的脚本同样见于附件。

0x05 一些和格式化字符串漏洞相关的漏洞缓解机制

和Linux pwn中格式化字符串漏洞常用的利用手段相关的缓解机制RELRO和FORTIFY

首先我们介绍一下RELRO,RELRO是重定位表只读(Relocation Read Only)的缩写。重定位表即我们经常提到的ELF文件中的got表和plt表。关于这两个表的来源和作用,我们会在介绍ret2dl-resolve的文章中详细介绍。现在我们首先需要知道的是这两个表,正如其名,是为程序外部的函数和变量(不在程序里定义和实现的函数和变量,比如read。显然你在自己的代码里调用read函数的时候不用自己写一个read函数的实现)的重定位做准备的。由于重定位需要额外的性能开销,出于优化考虑,一般来说程序会使用延迟加载,即外部函数的内存地址是在第一次被调用时(例如read函数,第一次调用即为程序第一次执行call read)被找到并且填进got表里面的。因此,got表必须是可写的。但是got表可写也给格式化字符串漏洞带来了一个非常方便的利用方式,即修改got表。正如前面的文章所述,我们可以通过漏洞修改某个函数的got表项(比如puts)为system函数的地址,这样一来,我们执行call puts实际上调用的却是system,相应的,传入的参数也给了system,从而可以执行system(“/bin/sh”)。

接下来我们介绍另一个比较少见的保护措施FORTIFY,这是一个由GCC实现的源码级别的保护机制,其功能是在编译的时候检查源码以避免潜在的缓冲区溢出等错误。简单地说,加了这个保护之后(编译时加上参数-D_FORTIFY_SOURCE=2)一些敏感函数如read, fgets,memcpy, printf等等可能导致漏洞出现的函数都会被替换成__read_chk,__fgets_chk, __memcpy_chk, __printf_chk等。这些带了chk的函数会检查读取/复制的字节长度是否超过缓冲区长度,通过检查诸如%n之类的字符串位置是否位于可能被用户修改的可写地址,避免了格式化字符串跳过某些参数(如直接%7$x)等方式来避免漏洞出现。开启了FORTIFY保护的程序会被checksec检出,此外,在反汇编时直接查看got表也会发现chk函数的存在

参考链接:

https://bbs.pediy.com/thread-253638.htm

Tags:

Categories:

Updated:

Comments