CTF-PWN

0ctf2019-babyaegis

2019-05-22  本文已影响3人  Nevv

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)。具体的映射策略如下图所示。

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

程序分析

功能分析

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

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。

【参考链接】

上一篇 下一篇

猜你喜欢

热点阅读