前幾天以 217 的名義參加了今年的 Google CTF,可惜因為大家都 變成 Google 員工不能參加 沒什麼時間,所以最後只打到第 12 名
Challenge - APT42
比賽中有一題名為 APT42
的題目吸引了我的注意,故事背景是 Google 在公司網路中發現可疑的流量,並發現許多系統中的 NTP 服務執行檔疑似被修改...
題目共分為兩個部分,Part1 為 Reverse ,希望我們可以幫忙逆向 malware:
Part2 則是 Pwnable ,希望我們可以打下 C2 Server 來搶回被竊取的資料:
Part.1 - Reverse the client
Analysis
題目附檔為一個 ELF 執行檔 ntpdate
,就是個 NTP client,會與 time.google.com
進行對時
簡單的分析之後發現,.plt.got
中原本應該是 sleep
的位置被換成了一個可疑的函數:
程式原本會在 daemon 模式下呼叫 sleep(60)
來定期與 NTP Server 對時
替換後的 sleep
函數會在經過一段相對長的等待時間後執行惡意行為,直接 patch 掉後就可以使程式執行可疑的函數
int __cdecl main(int argc, const char **argv, const char **envp)
{
...
istty = isatty(1);
cout = std::operator<<<std::char_traits<char>>(&std::cout, "NTP client v0.1 (");
if ( istty )
mode = "single";
else
mode = "daemon";
...
while (1) {
...
if ( istty )
break;
sleep(60); // replaced sleep
}
...
}
Obfuscation
函數中使用了不少混淆手段妨礙靜態分析,直接 patch 成 nop 就能繼續分析
字串部分也進行了加密,對於 library 函數的呼叫,則是透過自己實作類似 dl_resolve
的方式來呼叫
這使得我們沒辦法直接透過靜態分析或是 ltrace 等工具快速的判斷程式做了什麼
但還是可以 Hook resolve function 來做一個簡易的 ltrace ,甚至修改參數
透過 trace ,我們得知程式會先檢查 /etc/krb5.conf
中是否存在 domain.google.com
字串來決定是否要進行惡意行為
(果然是 APT 啊 XD)
Protocol
接著會連線到 mlwr-part1.ctfcompetition.com:4242
與 C2 Server 進行溝通
protocol 沒有複雜的加密,大概就是 4 Bytes 長度 + 8 Bytes Random ID + 內容 + 1 Byte Checksum:
client 每一次連線都會送出內容為 "hello" 的封包
C2 會依序送回下列指令,client 會將執行結果回傳,收到最後的 rm
後會刪除執行檔本身:
exec echo $USER
exec hostname
exec uname -a
exec ip a
rm
接下來嘗試送了很多不同的內容給 C2,但毫無反應...
於是決定先將程式中所有加密的字串透過 IDAPython 解出來:
將 part1 flag
這個字串用上面提到的 protocol 送回 C2 就會得到 flag
Flag: CTF{I don't always encrypt my strings, but when I do, I inline them all}
Script: https://github.com/L4ys/CTF/blob/master/google-2018/APT42/sol.py
Part.2 - Pwn the C2
第二部分需要找出 C2 的漏洞,但題目並沒有提供 C2 Server 的 Binary...
Proxy Mode
經過一段時間的分析後,發現 Client 在收到 upgrade
指令後,會成為一隻 Proxy, 在 fork 出 child 後, 會在 4242 Port 監聽其他 Client 連線, 並透過 clone()
建立 Thread 來負責轉送 C2 與 Client 之間的封包:
有趣的是在這個模式下,只會負責轉送 Client 傳來的 hello
/ part1 flag
以及指令的執行結果, 可以推測 mlwr-part1.ctfcompetition.com:4242
上所執行的並非真正的 C2 Server,而是被 "upgrade" 成 Proxy 的 Client
Vulnerability
程式在檢查收到的 Packet 中的長度欄位時犯了個錯誤,圖中 54 行在檢查長度時,使用了 signed int
由於 recv()
的長度參數類型為 size_t
, 如果我們將長度設為負數或是 8 ,就能使 recv()
接收一個非常大的長度,觸發 Stack Overflow
接下來需要繞過 Stack Canary,嘗試後發現雖然是同一個 Process,但每一次連線的 Canary 似乎都不同,無法透過 Brute Force 來繞過
Thread Implement
題目程式中的 Thread 是透過 clone()
來實作,並設定了 CLONE_SETTLS
Flag 使每個 Thread 擁有各自獨立的 TLS:
clone(
RunThread, // fn
stack_end, // child_stack
CLONE_UNTRACED|CLONE_SETTLS|CLONE_THREAD|CLONE_SIGHAND|CLONE_VM, // flags
func, // arg
0, //
stack_end); // tls
在程式自行實做的 RunThread
函數中會在執行 Thread Function 前產生隨機的 Stack Canary,因此每一次連線的 Stack Canary 都不相同:
int RunThread(__int64 (__cdecl *func)(__int64))
{
...
tcb = __readfsqword(0);
rand = Rand();
tcb->stack_guard = rand ^ (Rand() << 32);
func(__readfsqword(0) + 0x1A4);
...
}
當 clone()
指定 CLONE_SETTLS
時,第 6 個參數為 TLS
的位置,這裡與 pthread
類似,都是把 TLS
放在 Stack 的下方
也就是說當我們的 Stack Overflow 足夠長時,就能夠蓋掉 TLS
中的內容,其中就包含了 stack_guard
:
typedef struct
{
void *tcb; /* Pointer to the TCB. Not necessarily the thread descriptor used by libpthread. */
dtv_t *dtv;
void *self; /* Pointer to the thread descriptor. */
int multiple_threads;
int gscope_flag;
uintptr_t sysinfo;
uintptr_t stack_guard;
uintptr_t pointer_guard;
......
} tcbhead_t;
Exploit
最後的漏洞利用很簡單,透過 Stack Overflow 來執行 ROP 並同時覆蓋 TLS
中的 stack_guard
來繞過 Stack Canary
需要注意的是,由於程式在呼叫 recv()
時設定了 MSG_WAITALL
flag,需要接收到完整的資料後才會 return, 可以透過將 socket 的傳送端 shutdown 來使 recv()
結束,但這麼做也會無法再傳送資料到 Server
由於 Child 是 fork 出來的,記憶體位置不會變動,可以透過 ROP 先呼叫 send()
來 leak libc base
第二次再執行 system("cat /home/*/flag* >&4")
來將結果透過 socket fd 傳回
Flag: CTF{~~~APT warz: teh 3mpire pwns b4ck~~~}
Script: https://github.com/L4ys/CTF/blob/master/google-2018/APT42/exp.py
這次運氣蠻好的搶到了 First Blood :D