Lazy Project
Published on

Google CTF 2018 - APT42

前幾天以 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 掉後就可以使程式執行可疑的函數

ntpdate+0x4093F2
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:

ntpdate+0x40AB14
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 都不相同:

ntpdate+0x40A740
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:

libc/source/sysdeps/x86_64/nptl/tls.h
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