prettify code

2016年9月5日 星期一

[Write-up] Tokyo Westerns/MMA CTF 2nd 2016 - pwn300 diary

diary 是我在這場 CTF 當中唯一做到的 pwn 題,趁其他隊友還在睡覺時撿來做的XD

這場比賽題目品質很好,種類多而且都不猜謎,難度也很恰當,打完後覺得學到不少東西的比賽最棒了!


Info



附加檔案是 ELF64 的執行檔 diary

PIE 沒開,Partial RELRO





連線上去感受一下功能,選項很少的選單題:



總之三個功能:

  1. 寫日記
  2. 顯示某篇日記
  3. 刪掉某篇日記
寫日記的時候要輸入日期、日記長度、跟日記內容


Vulnerability

一打開 IDA 就看到 getnline 這個讀日記內容的函式有非常明顯的漏洞:



buffer a1 在傳進來之前是 malloc(len) 獲得的,read 時多讀了一個 byte(len + 1)。

因為 len(日記長度)完全沒有限制,所以可以構造長度使得 overflow 壓到下一個 chunk 的 size,就是個 heap exploit 常見題型了!


正當覺得這是個再正常不過的 heap exploit,想說先寫個 PoC 讓他 crash 的時候卻發現 ltrace 一直都不會顯示 mallocfree,這才注意到.. 他的 heap 居然是自己實作的 XDrz




因此花了點時間看它的 heap 實作方式。大致上其實跟 glibc 的 heap 幾乎一樣,但簡化了很多操作。

一樣的部分:

  • chunk 結構一樣是 previous_size,size,fd,bk
  • 同樣有 prev_in_use bit

他的 heap 實作簡述:

  • 只有一個 bin,而且他是 sorted bin,裡面的 free chunks 總是按照 size 由小到大排好序。
  • mallocsize 是 8 byte 對齊 (glibc#heap 是 16 byte 對齊)
  • bin 裏一開始就有一個大 chunk (4096 byte)
  • malloc 時:
    • 從 bin 裏找第一個 size 足夠的 chunk
    • 如果找到的 chunk 比需求 size 多超過 8 byte,切掉多餘的部分放回 bin
  • free 時:
    • 前後的 chunk 如果已經被 free,就先從 bin 裏 unlink 之後與當前 chunk 合併
    • 把當前 chunk 放進 bin
  • 所有 unlink 都沒有做 double-linked list 合法性的檢查
此外,自己實作的 heap 是另外 mmap 出來,並且權限為 rwxp,如果能控 rip 到上面就可以執行 shellcode。

因此需要做到兩件事:
  1. Information leak: 得知 heap base
  2. 控制 rip 使跳轉到 heap 上執行 shellcode

Leak heap base

如果是從 bin 裏被拿出來的 chunk,上面會殘留有 fdbk。輸入日記內容時因為是用 read 讀的,只要內容很短且沒有換行就可以讓 chunk 上的 fd 被留著,之後 show diary 就可以把上面的 fd 印出來,即可 leak heap base。詳細可見最後附的 script。

Control rip

最一開始發現的 one-byte overflow 漏洞這時候就要派上用場了。
只要 overflow 把 prev_in_use bit 改掉,就可以使在 free 時「誤會」前一個 chunk (prev_chunk) 已經被 free 了,此時就會對 prev_chunk 進行 unlink。而 prev_chunk 上的 data 是我們完全可控的,因此只要適當控好 prev_chunkfdbk,就可以改掉 GOT entry 的值。這也是 glibc 古老版本的 heap exploit 常用技巧。

unlink:

void unlink(Chunk *c) {
  c->bk->fd = c->fd;
  c->fd->bk = c->bk;
}

因此讓 prev_chunk 上的 data 為

  • fd: puts_got - 0x10 (0x602020 - 0x10)
  • bk: shellcode_addr
shellcode_addr 就根據自己哪篇日記的內容是 shellcode,用 heap base 算出來就可以了。

這樣進行 unlink 時就會

void unlink(Chunk *c) {
  c->bk->fd = c->fd; // *( shellcode_addr + 8 ) = fd = puts_got - 0x10
  c->fd->bk = c->bk; // *( (puts_got-0x10) + 0x10) = bk = shellcode_addr
                     // *puts_got = shellcode_addr
}
就成功把 puts_got 改成 shellcode 的位置,下次呼叫 puts 時就會跳到 shellcode 上了。
一點要注意的是因為剛才 unlink 操作裡會改掉 shellcode_addr 後面 8 個 byte,因此 shellcode 開頭要先 jmp 來跳過被改寫的地方。

這樣就做到任意 shellcode 執行了,至此為止的 exploit script







然後這題開始了。

binary 最一開始有利用 seccomp 做為 sandbox,簡單講就是通常用來阻擋 execve syscall 讓你能開 shell 做任意代碼執行。



首先要先知道他究竟阻擋了哪些 syscall。雖然之前有看過這類 sandbox ,花了點時間在研究 seccomp 的註冊方式QQ。

seccomp 的原理與解讀方式可以參考這篇,年初 32C3 那場的 writeup 也有出現 seccomp。

簡單來說註冊禁止 syscall 的要寫 BPF(Berkeley Packet Filter),肉眼不容易看懂。裝了 libseccomp之後, tools 資料夾裡面有個 scmp_bpf_disasm 可以直接讀 BPF 的 code 進行 disassembly。

因此先把 BPF 的內容 dump 出來再餵進去就可以了。



可以看到他阻擋了哪些 syscall number,對應一下可以知道是這些:

2 open
257 openat
56 clone
57 fork
58 vfork
59 execve
85 create
322 execveat

execve 被擋掉自然,但因為檔了 open ,因此也不能 open/read/write 來讀 flag 內容。不過黑名單編號總是會由遺漏,64bit 某些版本的 kernel 中會有些 syscall 在 5xx 號那附近,這裏可以看到列表,520 也是 execve。因此只要把 shellcode 中 syscall number 改成 520 就好了。

但會發現 execve("/bin/sh") 一樣會失敗!原因是 seccomp 的規則是會延續的,而 execve 的過程中就會用到 open,就被擋下來了QQ。
If execve(2) is permitted, then the seccomp mode is preserved across execve(2).


題目敘述當中其實有個 Hint 寫可以利用 "./bash"。因此 shellcode 改成 execve("./bash"),攻擊遠端時竟然就成功了!

原因是 ./bash 是 32bit 的,syscall number 會不一樣,所以原本禁止的 syscall 在 32bit mode 底下不太有影響。


開啟 bash 之後,會發現打什麼指令好像都沒反應,但是 echo 123 卻會成功。原因是 32bit 的 fork 剛好是 number 2,因此如果要執行指令就會因為使用 fork 而被 seccomp 擋下來。換句話說,我們就不能 lscat flag,只能想辦法透過 builtin command 來知道 flag 內容。

既然沒有 ls ,可以返璞歸真用 for 迴圈 + echo 當 ls 用,讀檔則可以利用 source 並把錯誤訊息(stderr)導回來就會看到 flag 了~



Flag: TWCTF{bl4ckl157_53cc0mp_54ndb0x_15_d4ng3r0u5}