美文网首页CTF-PWN
0ctf2019-babyaegis

0ctf2019-babyaegis

作者: Nevv | 来源:发表于2019-05-22 15:17 被阅读3次

    Asan 简述

    ​ AddressSanitizer 后文均简称为ASan 是 Google 开源的一个用于进行内存检测的工具,包括但可能不限于 Heap buffer overflow, Stack buffer overflow, Global buffer overflow 等等。其支持检测的漏洞有:

    1. Heap-use-after-free
    2. Heap-buffer-overflow
    3. Stack-buffer-overflow
    4. Global-buffer-overflow

    1.插桩和动态运行库

    ​ ASan 由两个主要部分构成,插桩和动态运行库( Run-time library ),插桩主要是针对在llvm编译器级别对访问内存的操作(store,load,alloca等),将它们进行处理。动态运行库主要提供一些运行时的复杂的功能(比如poison/unpoison shadow memory)以及将malloc,free等系统调用函数hook住。其实该算法的思路很简单,如果想防住Buffer Overflow漏洞,只需要在每块内存区域右端(或两端,能防overflow和underflow)加一块区域(RedZone),使RedZone的区域的影子内存(Shadow Memory)设置为不可写即可。

    2.内存映射

    ​ AddressSanitizer保护的主要原理是对程序中的虚拟内存提供粗粒度的影子内存(每8个字节的内存对应一个字节的影子内存),为了减少overhead,就采用了直接内存映射策略,所采用的具体策略如下:Shadow=(Mem >> 3) + offset。每8个字节的内存对应一个字节的影子内存,影子内存中每个字节存取一个数字k,如果k=0,则表示该影子内存对应的8个字节的内存都能访问。
    ​ 如果k在0到7之间,表示前k个字节可以访问,如果k为负数,不同的数字表示不同的错误(e.g. Stack buffer overflow, Heap buffer overflow)。具体的映射策略如下图所示。

    • 64位

      Shadow = (Mem >> 3) + 0x7fff8000;
      [0x10007fff8000, 0x7fffffffffff]  HighMem
      [0x02008fff7000, 0x10007fff7fff]  HighShadow
      [0x00008fff7000, 0x02008fff6fff]  ShadowGap
      [0x00007fff8000, 0x00008fff6fff]  LowShadow
      [0x000000000000, 0x00007fff7fff]  LowMem
      
    • 32位

      
      Shadow = (Mem >> 3) + 0x20000000;
      [0x40000000, 0xffffffff]  HighMem
      [0x28000000, 0x3fffffff]  HighShadow
      [0x24000000, 0x27ffffff]  ShadowGap
      [0x20000000, 0x23ffffff]  LowShadow
      [0x00000000, 0x1fffffff]  LowMem
      

    ​ 显而易见的是,ASan 的检查很大一部分是基于影子内存中的flag值。假设如果全段影子内存的 flag 全为0,我们就可以完全无视掉ASan,而0ctf 的 babyaegis,正是给了一个写0的机会,给了我们一次对一个指针再次读写的机会。

    • 对于会有内存操作的库函数,如strlen等,都会进行hook掉,动态的检测内存
    • hook掉的malloc分配的策略大概可以描述如下,不同size分配的内存区域不同,但是地址会固定,如0x10字节大小的,一开始都会分配到0x602000000010这个地址
    • 分配的每块内存的前面0x10个字节都会带有一些描述这块内存的信息,如size,使用状态
    • free掉之后的内存正常情况是不会再次被分配的

    程序分析

    功能分析

    程序的功能很简单,增删改查且保护全开:

    1. Add note
    2. Show note
    3. Update note
    4. Delete note
    5. Exit
    

    add_note

    - size需要满足
    
      v14 = read_int("Size: ");
      if ( v14 < 16 || v14 > 1024 )
        error("Size: ");
    
    - 之后读入content
    - 之后读入8字节的ID
    

    我这里尝试添加一个note,其真实存储content的地址在0x602000000000起始的内存空间处:

    pwndbg> x /60gx 0x602000000000
    0x602000000000: 0x02ffffff00000002  0x0900000120000010
    0x602000000010: 0x3131313131313131  0x000000002b673131
    0x602000000020: 0x02ffffff00000000  0x2080000120000010
    0x602000000030: 0x0000602000000010  0x0000555555668ab0
    0x602000000040: 0x0000000000000000  0x0000000000000000
    

    通过调试分析可以发现内存的布局大概是这样子的:

     RDI  0x555556504cc0 (notes) —▸ 0x602000000030 —▸ 0x602000000010 ◂— '11111111'
    0x555556504cc0 // note 数组
    
    pwndbg> x /10gx 0x602000000030
    0x602000000030: 0x0000602000000010  0x0000555555668ab0  // cfi_check
    0x602000000040: 0x0000000000000000  0x0000000000000000
    0x602000000050: 0x0000000000000000  0x0000000000000000
    0x602000000060: 0x0000000000000000  0x0000000000000000
    0x602000000070: 0x0000000000000000  0x0000000000000000
        
    pwndbg> x /4gx 0x0000555555668ab0
    0x555555668ab0 <cfi_check>: 0xccccccfffff25be9  0x0000000000841f0f
    
    // 最终数组中第一元素的内存地址
    pwndbg> x /60gx 0x602000000000
    0x602000000000: 0x02ffffff00000002  0x0900000120000010
    0x602000000010: 0x3131313131313131  0x000000002b673131
    0x602000000020: 0x02ffffff00000000  0x2080000120000010
    

    show_note

    ​ 这个函数逻辑很简单,也是根据输入的下标去数组中寻找对应的地址,打印其content,再从content后边取出8个字节作为打印出来。

    update_note

    ​ 这个函数就是对note内容的更新:

    unsigned __int64 update_note()
    {
      unsigned __int64 v0; // rdi
      unsigned __int64 v1; // rdi
      __int64 v2; // rbx
      unsigned __int64 v3; // rsi
      __int64 v4; // rax
      unsigned __int64 v5; // rdi
      __int64 (__fastcall **v6)(); // rdi
      __int64 (__fastcall *v7)(); // rbx
      unsigned __int64 v9; // [rsp+8h] [rbp-28h]
      int v10; // [rsp+18h] [rbp-18h]
      signed int v11; // [rsp+1Ch] [rbp-14h]
    
      v0 = (unsigned __int64)"Index: ";
      printf((unsigned __int64)"Index: ");
      v11 = read_int("Index: ");
      if ( v11 < 0 || v11 >= 10 )
        goto LABEL_29;
      v0 = (unsigned __int64)&notes + 8 * v11;
      if ( *(_BYTE *)((v0 >> 3) + 0x7FFF8000) )
        _asan_report_load8(v0);
      if ( !*(_QWORD *)v0 )
    LABEL_29:
        error(v0);
      v1 = (unsigned __int64)&notes + 8 * v11;
      if ( *(_BYTE *)((v1 >> 3) + 0x7FFF8000) )
        _asan_report_load8(v1);
      v9 = *(_QWORD *)v1;
      printf((unsigned __int64)"New Content: ");
      if ( *(_BYTE *)((v9 >> 3) + 0x7FFF8000) )
        _asan_report_load8(v9);
      v2 = *(_QWORD *)v9;
      if ( *(_BYTE *)((v9 >> 3) + 0x7FFF8000) )
        _asan_report_load8(v9);
      v3 = strlen(*(_QWORD *)v9) + 1;
      v10 = read_until_nl_or_max(v2, v3);
      printf((unsigned __int64)"New ID: ");
      v4 = read_ul("New ID: ");
      if ( *(_BYTE *)((v9 >> 3) + 0x7FFF8000) )
        v4 = _asan_report_load8(v9);
      v5 = v10 + *(_QWORD *)v9;
      if ( *(_BYTE *)((v5 >> 3) + 0x7FFF8000) )
        v4 = _asan_report_store8(v5);
      *(_QWORD *)v5 = v4;
      v6 = (__int64 (__fastcall **)())(v9 + 8);
      if ( *(_BYTE *)(((v9 + 8) >> 3) + 0x7FFF8000) )
        _asan_report_load8((unsigned __int64)v6);
      v7 = *v6;
      if ( *v6 != cfi_check )
      {
        _asan_handle_no_return(v6);
        _ubsan_handle_cfi_check_fail_abort(&unk_34B100, v7);
      }
      ((void (__fastcall *)(_QWORD, unsigned __int64))v7)((unsigned int)v11, v3);
      puts("Update success!");
      if ( *(_BYTE *)((v9 >> 3) + 0x7FFF8000) )
        _asan_report_load8(v9);
      if ( *(_QWORD *)v9 >> 44 != 6LL )
        error(v9);
      return __readfsqword(0x28u);
    }
    

    delete_note

    ​ 这里一开始是选择index,然后拿出来,free掉。这里又是另外一个漏洞,free掉之后没有置0,不过因为用了Address Sanitizier,任何use after free都会退出,而且Address Sanitizier也会把那个地址修改为一个不可读写的地址

      free(*(__sanitizer **)v3);
    

    secret

    这里首先是读取一个地址,然后会判断一下地址右移44位之后是否大于0,假如大于的话,会或运算上0x700000000000,之后会对这个地址写0/因为程序开了PIE,地址大于 0x500000000000,而堆地址是大于0x600000000000,两个都小于0x700000000000,因此是不能对程序中的变量和堆上的变量进行写0操作.唯一有可能的就是Shadow Memory,这里就是之前提到的,修改影子内存为0,bypass掉asan的检查。

    
    unsigned __int64 secret()
    {
      _BYTE *v0; // rax
      unsigned __int64 v2; // [rsp+0h] [rbp-10h]
    
      if ( secret_enable )
      {
        printf((unsigned __int64)"Lucky Number: ");
        v2 = read_ul("Lucky Number: ");
        if ( v2 >> 44 )
          v0 = (_BYTE *)(v2 | 0x700000000000LL);
        else
          v0 = (_BYTE *)v2;
        *v0 = 0;
        secret_enable = 0;
      }
      else
      {
        puts("No secret!");
      }
      return __readfsqword(0x28u);
    }
    

    漏洞利用

    调试

    ​ 首先查看下实际存储数据的位置以及其对应的影子内存。为0的时候代表8个字节都可以写

    pwndbg> x /40gx 0x602000000000
    0x602000000000: 0x02ffffff00000002  0x0900000120000010 // 前0x10字节是header
    0x602000000010: 0x3131313131313131  0x0000000000000100
    0x602000000020: 0x02ffffff00000000  0x2080000120000010
    0x602000000030: 0x0000602000000010  0x0000555555668ab0
    0x602000000040: 0x02ffffff00000002  0x0900000120000010
    0x602000000050: 0x3131313131313131  0xffffffffffffff00
    0x602000000060: 0x02ffffff000000ff  0x2080000120000010
    0x602000000070: 0x0000602000000050  0x0000555555668ab0
    0x602000000080: 0x02ffffff00000002  0x0900000120000010
    0x602000000090: 0x0000000000000000  0xbe00000000000000
    0x6020000000a0: 0x02ffffff00000002  0x2080000120000010
    0x6020000000b0: 0x0000602000000090  0x0000555555668ab0
    0x6020000000c0: 0x0000000000000000  0x0000000000000000
    
    // 影子内存
    pwndbg> x /40gx 0xc047fff8000
    0xc047fff8000:  0x0000fafa0000fafa  0x0000fafa0000fafa
    0xc047fff8010:  0x0000fafa0000fafa  0xfafafafafafafafa
        
    // 释放note1后
    pwndbg> x /40gx 0xc047fff8000
    0xc047fff8000:  0xfdfdfafafdfdfafa  0xfafafafafafafafa
    0xc047fff8010:  0xfafafafafafafafa  0xfafafafafafafafa
    
    
    

    其中asan 的 header的结构如下

    image

    ​ 使用secret,我们可以吧影子内存中对应控制chunk的size字段的字节修改为0,这样的话就可以溢出到下一个chunk,并修改其size字段,这时候size字段也不能修改的太大,太大话会出发asan的内存回收机制,收回所有的内存空间。

    ​ 修改size字段后再free,会发现修改size的所有影子内存都被修改,此时再创建note,其地址空间就会与第一个内存空间重合,也就有了任意读,可以leak出程序基址,libc基址。

    利用

    ​ 使用bss段的一个callback,_ZN11__sanitizerL20InternalDieCallbacksE,在update的时候,检测函数指针错误的时候会调用Die,Die又会调用这个callback,跳转到gets函数,这个时候就有一个ROP,之后ROP一下就能get shell。具体可以参考下面两位师傅的exp。

    【参考链接】

    相关文章

      网友评论

        本文标题:0ctf2019-babyaegis

        本文链接:https://www.haomeiwen.com/subject/kxluzqtx.html