Google CTF 2018 - APT42

前幾天以 217 的名義參加了今年的 Google CTF
可惜因為大家都 變成 Google 員工不能參加 沒什麼時間,所以只打到 12 名

比賽中有一道名為 APT42 的題目吸引了我的注意
故事背景是 Google 在公司網路中發現可疑的流量,並發現許多系統中的 NTP 服務執行檔疑似被修改…


題目共分為兩個部分
Part1 為 Reverse ,希望我們可以幫忙逆向 malware
Imgur

Part2 則是 Pwnable ,希望我們可以打下 C2 Server 來搶回被竊取的資料
Imgur


Part.1 - Reverse the client

Analysis

題目附檔為一個 ELF 執行檔 ntpdate,就是個 NTP client,會與 time.google.com 進行對時
簡單的分析之後發現,.plt.got 中應該是 sleep 的位置被換成了一個可疑的函數:
Imgur

程式原本會在 daemon 模式下呼叫 sleep(60) 來定期與 NTP Server 對時
替換後的 sleep 會在等待一段不小的時間後進行惡意行為,直接 patch 掉後就可以使程式執行可疑的函數

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 就能繼續分析:
Imgur

字串部分也進行了加密,對於 library 函數的呼叫,則是透過自己實作類似 dl_resolve 的方式來呼叫
Imgur

這使得我們沒辦法直接透過靜態分析或是 ltrace 等工具快速的判斷程式做了什麼
但還是可以 Hook 他的 resolve function 來做一個簡易的 ltrace ,甚至修改參數:
Imgur

透過 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:
Imgur

client 每一次連線都會送出內容為 “hello” 的封包
C2 會依序送回下列指令,client 會將執行結果回傳,最後的 rm 會刪除執行檔本身
exec echo $USER
exec hostname
exec uname -a
exec ip a
rm

接下來嘗試送了很多不同的內容給 C2,但毫無反應…
於是決定先將程式中所有加密的字串透過 IDAPython 解出來:
Imgur

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/gctf_2018/APT42/sol.py


Part.2 - Pwn the C2

第二部分需要找出 C2 的漏洞,但題目並沒有提供 C2 Server 的 Binary…

Proxy Mode

經過一段時間的分析後,發現 client 在收到 upgrade 指令後,會成為一隻 Proxy
Fork 出 child 後, 呼叫 bind() / accept() 在 4242 Port 監聽其他 client 連線
並透過 clone() 建立 Thread 來負責轉送 C2 與連上來的 client 之間的封包:
Imgur

有趣的是在這個模式下,只會負責轉送 client 傳來的 hello / part1 flag 以及指令的執行結果
可以推測 mlwr-part1.ctfcompetition.com:4242 上所執行的並非真正的 C2 Server,而是被 “upgrade” 過的 client

Vulnerability

程式在檢查收到的 Packet 中的長度欄位時犯了個錯誤
圖中 54 行在檢查長度時,使用了 signed int
Imgur

由於 recv() 的長度參數類型為 size_t
這使得如果我們將長度設為負數或是 8 ,就能使 recv() 接收一個非常大的長度,進而觸發 Buffer Overflow

接下來需要繞過 Stack Canary,卻發現雖然是同一個 Process,但 Canary 每一次連線似乎都不同,無法透過 Brute Force 來繞過

Thread Implement

題目程式中的 Thread 是透過 clone() 來實作,設定了 CLONE_SETTLS Flag 使每個 Thread 擁有各自獨立的 TLS:

clone(Launch, stack_end, CLONE_UNTRACED|CLONE_SETTLS|CLONE_THREAD|CLONE_SIGHAND|CLONE_VM, func, 0LL, stack_end);

Launch 函數中會在執行 Thread Function 前產生隨機的 Stack Canary,因此每一次連線的 Stack Canary 都不相同:

rand = Rand();
tcb->stack_guard = rand ^ (Rand() << 32);

clone() 指定 CLONE_SETTLS 時,第 6 個參數為 TLS 的位置,這裡與 pthread 類似,都是把 TLS 放在 Stack 的下方
也就是說當我們的 Buffer Overflow 足夠長時,就能夠蓋掉 TLS 中的內容,其中就包含了 Stack Canary 的來源:

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

最後的漏洞利用很簡單,透過 Buffer Overflow 來執行 ROP 並同時覆蓋 TLS 中的 stack_guard 來繞過 Stack Canary

需要注意的是,由於程式在呼叫 recv() 時設定了 MSG_WAITALL flag,需要接收到完整的資料後才會 return
可以透過將 socket 的傳送端 shutdown 來使 recv() 結束,但這麼做也會使得沒辦法再送資料到 server

由於 Child 是 Fork 出來的,記憶體位置不會變動,可以先做一次 send 來 leak libc 及 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/gctf_2018/APT42/exp.py

這次運氣蠻好的搶到了 First Blood :D