2019年5月15日 星期三

DEF CON CTF Qualifier 2019: KNOW_YOUR_MEM

English version can be found below. Click me to get there.
(English version may contain less details than Chinese one)


中文版


題目

在記憶體位置 [0x0000100000000000, 0x0000200000000000) 共 \(2^{32}\) 個 page 中,隨機挑選了 31 個 page 並 mmap 起來(剩下的 page 都未被 mapped),並且在這 31 個 page 的其中一塊藏了 flag。
找出這 31 個 page 後,只要看第一個 byte 就能確定這塊是否藏有 flag,如果是的話,從開頭開始連續輸出一小段就一定會包含 flag。

現在這個程式在做好上述的設置之後,會讀入你給予的 shell code 並執行它,並且在最多 10 秒後這個程式會被強制結束。

你的 shellcode 只能執行這幾個 syscall:exitexit_groupreadwritemmap、munmapmprotectbrkset_thread_area
(用 seccomp 列白名單限定的)

順帶一提,可以寫單純的 C 程式來解題,題目有提供良好的 template 讓你測試並把寫完的程式碼轉成 shellcode。

解題過程

我們整隊在初期有人大概看過各題的樣子之後,一直沒有人想要認真接手這題。大概是因為題目附上的檔案感覺很多很複雜吧。總之,專心的把題目附上的東西讀懂,就可以釐清出題目的內容,其實只有我上面描述的那麼單純而已。

畢竟是 CTF 題,我在解題過程中也花了一些心力檢查有沒有別的漏洞,例如我能不能用 open 開啟 flag 檔案、原本程式開 flag 檔案的 file descriptor 有沒有關、random 取 page 的部份是否有瑕疵……,不過都沒有成功,最終還是嘗試照規矩來解。

按照規矩解題的第一個想法,當然就是把所有 page 都檢查過去囉!
不過直接讀取一個記憶體位置,很可能會導致 SIGSEGV 而關掉整個程式。研究一下他提供的 syscall 之後,發現 mprotect 會在該記憶體範圍裡有 unmapped page 時回傳錯誤,因此可以用它來好好的檢查。

這個程式就很簡單的寫個 for 迴圈掃過去而已,但是寫完測試發現它(不意外的)非常慢。
在 10 秒內,它大約只能檢查完全部範圍的 1/256 而已!



在這邊卡了非常久,仔細的研究程式有沒有其他漏洞以及提供的那些 syscall 有沒有特殊性質。

突然發現:mmap 會儘量在你指定的記憶體位置附近找夠大塊的空白記憶體區間,如果不行,才會找其他位置來給你。
這樣的話,我們就可以一口氣詢問很大一塊(非常多個 page)是否都是 unmapped。

身為剛退役的 ACM 選手,看到這點當然就馬上想到分塊作法囉:
假設分塊的大小為 \(K\),那麼我們就詢問接下來的 \(K\) 個 page 是否都是空的:如果是,就整塊跳過;否則,進去一個一個 page 檢查。取 \(K\) 為大約 \(\sqrt{N}\) 時會有最佳時間複雜度!
(別忘了,mmap 檢查完取得的那塊空間一定要 munmap 掉)

寫完之後,測試下去卻沒有成功。
深入檢查之後,發現 mmap 下去得到了 ENOMEM

應該是一塊太大了(好好計算發現一塊是 256 MB),把分塊大小調小一些(縮小成 1/16)就通過簡單版測試了。

接著需要調整一下程式碼讓他可以直接被編譯成 shellcode。

題目提供了很長的資料,但實際需要做的只有兩件事

1. 把 syscall 通通加上前綴 sys_
2. 不要使用非 syscall 的函數



改完之後,本地完整版測試就通過了。
照著題目說明,把產出的檔案丟到 nc 即可拿到 flag!

我的解答程式:




English version


Task

There are \(2^{32}\) pages between Memory address [0x0000100000000000, 0x0000200000000000).
31 of them (picked by random) are mapped by mmap and the remainings are left unmapped.
The flag is hidden in one of the 31 pages and you can identify it by checking the first byte in the page. The flag is hidden at the beginning of that page, so you can read it easily (after finding the correct page).

You can send a short segment of shellcode. It should to find the flag in 10 seconds.
Only the following syscalls are allowed in your shellcode: exit, exit_group, read, write, mmap, munmap, mprotect, brk, set_thread_area.
(Filtered by seccomp in whitelist.)

By the way, one can write the solution in C language. Template and scripts to transform C program into shellcode is provided.


My Solution

Of course, I spend some time to find other vulnerability in the task.
For example, "do open() actually banned?", "is the file descriptor that read the flag closed?" or "is the 'random' really unpredictable?".
None of them works. Maybe I should follow the rule: search on memory.

To search the flag on memory, the easiest way is to check every possible pages.
Note that one cannot directly read a page to verify, or the program will be killed by SIGSEGV. Use mprotect to check a page beforehand.

Turns out this solution is, as expected, extremely slow. It can only check 1/256 of all possible pages in 10 seconds.



Spending some time to check the manuals of provided syscalls... I found this in mmap:
If addr is not NULL, then the kernel takes it as a hint about where to place the mapping; on Linux, the kernel will pick a nearby page boundary and attempt to create the mapping there. If another mapping already exists there, the kernel picks a new address that may or may not depend on the hint.


Hence, we can ask mmap to map a large piece of memory to check if all the pages in that range is still unmapped!

With this property, we can find all mapped pages with partitioning:
Pick a number \(K\) as the size of a partition. For the next partition, use mmap to check if it's all empty: if so, skip this partition; otherwise, check the pages one by one.
The best complexity happens when \(K\) is about \(\sqrt{N}\).
(Remember to munmap the acquired memory)


After implementing this algorithm, I got ENOMEM upon mmap.
One partition is 256MB in my configure. Way too large!
I passed the simplified test after changing the size of a partition to 16MB.


All remained is to fix my code so that it can be compiled into raw shellcode.
According to the reference, there are only two things to do:

1. Add prefix sys_ to every syscall I used.
2. Don't use any non-syscall function.



Pass all local tests and capture the flag!
My code:

沒有留言:

張貼留言