CreateProcessA參數型Shellcode的編碼問題研究

發布時間 2021-12-22
近日,在對WebAccess/SCADA系統的漏洞研究中,啟明星辰ADLab的工控安全研究員發現了一個未被廣泛談論的漏洞利用技術問題,即經由CreateProcessA參數進行傳遞的shellcode的編碼問題。


簡單來講,該控制系統的漏洞由兩個程序構成:核心程序CoreProcess和輔助程序HelpProcess,核心程序CoreProcess通過系統函數CreateProcessA來啟動HelpProcess(同時傳遞了相關參數)。其中,CoreProcess的簡化代碼如下:


代碼.png


顯然,HelpProcess的WinMain函數存在一個經典的棧溢出漏洞。當lpCmdLine的數據長度超過400字節時,對buff的strcpy操作就會產生溢出;當長度超過404字節時,就會覆蓋到eipCallerNext,從而劫持HelpProcess的程序控制流。


回溯代碼可知,lpCmdLine的數據來源是CoreProcess的CreateProcessA調用,且是用戶可控的。因此,該漏洞的利用看起來是簡單的,只需要計算好eipCallerNext的偏移量并利用shellcode填充buff即可。該漏洞的利用鏈和堆棧布局如下所示:


回溯代碼.png

 

在利用過程中,采用測試填充字符進行溢出時,eipCallerNext的覆蓋總是正確的;但采用metasploit的shellcode來溢出時,eipCallerNext的覆蓋就變得不正確。對數據進行比較后發現,shellcode在CoreProcess和HelpProcess是不一樣的,即shellcode傳遞到HelpProcess后發生了改變。此外,通過嘗試metasploit的不同shellcode,發現這種改變沒有明顯的規律可循。


針對這個問題,ADLab的安全研究員進行了深入的分析,弄清了CreateProcessA參數傳遞的shellcode的編碼問題,并開發了自動化處理方法,從而兼容任意shellcode。


CreateProcessA的參數處理


Windows操作系統的內核是支持全球各種語言的,其提供統一的Unicode編碼型內核態API;針對具體的國家或地域,Windows通過區域編碼來實現本地語言支持,即Ansi字符串型的用戶態API。這些用戶態API在內部先把Ansi字符串轉換為Unicode字符串,然后再調用內核態API;這個轉換過程是透明的,用戶編寫的程序對此無感知。


在Window操作系統上,1個Unicode字符由2個字節組成,1個Ansi字符由1個字節或2個字節組成。當首字節的值是0到127時,它是1個ASCII字符,對應Unicode字符的2字節的內容就是該ASCII字符加1個填充字符0;例如,Ansi字符”A”,其對應的Unicode字符是”A\x00”。當首字節的值大于127時,則當前字節和下個字節組合起來是一個區域語言的字符,區域語言字符存在對應的Unicode字符映射表;例如,”\xce\xd2”的“\xce”不是1個合法的ASCII字符,它只能和“\xd2”聯合作為1個中文字符“我”,對應的Unicode字符是”\x11\x62”。


如下所示,CreateProcessA就是一個Ansi編碼型的用戶態API,字符串”AAAA”會被自動轉換為Unicode字符串并傳遞給HelpProcess,然后在調用WinMain之前又被自動還原為Ansi字符串。因此,對于Ansi字符串”AAAA”,CoreProcess和HelpProcess在程序開發上都無需做任何額外的處理。


代碼.png


通常情況下,CreateProcessA參數lpCmdline的來源是可靠的,比如編譯時預定義的字符串和API的返回值,此時lpCmdline都是正確的Ansi字符串。因此,CreateProcess幾乎總能在Unicode和Ansi之間自由地正確轉換。


實際上,對于任何一門區域語言,其Ansi字符和Unicode字符的映射都不是一一映射關系;即在2字節的全部取值空間中,Ansi字符表的有效項數總是小于Unicode字符表的有效項數。這意味著,針對無法確認是區域語言的2個字節,如果強制視作Ansi字符則轉換成Unicode字符后不一定能還原為初始的Ansi字符。例如:”\xeb\x2a”是一條常規的jmp offset指令,它不是1個合法的中文字符;如果視作Ansi字符強制轉換為Unicode字符則是”\x3f\x00”,再次轉換為Ansi字符即是”?”,丟失了jmp offset指令的語義。


因此,通過CreateProcessA的cmdline參數進行shellcode傳遞,必須要考慮區域語言的Ansi字符和Unicode字符相互轉換的問題。


在本文的漏洞利用案例中,本地區域的語言是中文簡體,對應Ansi編碼表是GBK。因此,必須要對metasploit的shellcode進行GBK編碼,確保其是正確的Ansi字符串。


GBK表的編碼在2字節取值空間的范圍是8140-FEFE,即第1字節的取值范圍是0x81到0xFE,第2字節的取值是0x40到0xFE,如下所示:


 字節.png


此外,第2字節的實際有效取值還有更多約束。比如,第2字節不能為0X7F。針對某些取值的字節,第2字節的取值比[0x40, 0xFE]的空間更小。如下圖所示,有的只能取該空間的后半部分,有的則只能取前半部分。


對于shellcode來講,其每個字節的取值在0到255之間都是完全合法的。因此,本文的漏洞利用要實現shellcode的隨意替換,必須要有一種方法來對shellcode中違背GBK編碼的字節進行處理,從而避免Ansi字符和Unicode字符間轉換導致的shellcode字符被改變的問題。一個基本的方法是按照如下的流程對shellcode進行處理,其關鍵是對GBK表進行查表并修正匯編指令。


 字節調整.png


以如下的shellcode為例,在掃描到字節0xEB時,發現是非ASCII字符且查表GBK結果是不存在,需要進行轉換;查詢GBK表后發現,在0xEB之前插入0x90可以使得90 EB是一個合法的GBK字符,同時90EB 38又不改變原來的匯編語義,轉換成功。同理,繼續掃描到下一個字節0XEB時,再做同樣的轉換就可以。但是,第2次的轉換插入了新的字節0x90,導致了原始lab1對應的偏移量發生了改變;原始lab的指令實際位于轉后的lab+1位置,使得第一個0XEB的語義非法了。因此,轉換過程還要求跟蹤指令區塊的長度變化。


轉換匯編.png


除了指令區塊的長度改變外,還有其它兼容性問題。比如,shellcode中特殊取值(典型有0)的字節處理問題,對shellcode的內嵌參數修改問題等。因此,盡管查表轉換是最根本的辦法,但全表查詢的空間大,限制了shellcode的靈活性。為了解決該問題,ADLab的安全研究員提出了一種基于計算的shellcode編碼方法。


Shellcode計算轉換


首先,我們把shellcode分為兩部分:頭部的固定decoder和尾部的多變payload。然后,采用查表方式進行手工編寫符合GBK編碼的匯編代碼。其中,decoder的長度很有限,決定了這個編寫的代價不大;同時,多變payload是沒有額外限制的,通過編寫對應的encoder來編碼payload使其不違反GBK編碼,又可以被decoder還原。通過這種方式,對原始shellcode的選擇和改變就完全不用關心GBK編碼問題,使得該漏洞的利用更加豐富。


為了減少decoder的體積,我們設計了一種計算方法來編碼和解碼,這樣就不需要存儲GBK字符表或者復雜的規則。原始shellcode編碼時的計算規則如下:


遇到字節是ASCII、0x80和0xff,直接保留。


遇到字節是\x00,轉換成加法運算符\x90和2個計算數符\x80和\x80。


遇到字節是\x90,轉換成加法運算符\x90和2個計算數符\x48和\x48。


遇到2個字節可以轉換為unicode字符,直接保留這2個字節。


遇到前面都不能處理的字節,直接轉換成加法運算符\x90和2個計算數符,第1個是\x80,第2個是差值。


采用上述的編碼方法后,任何shellcode都可以被轉換為合法GBK字符串,并且decoder對payload的解碼計算也十分簡單,只需要如下的1條規則:


遇到字符是\x90,直接對后2個字符進行加法計算,并用結果替換字符\x90。 


至此,CreateProcessA參數傳遞的shellcode的編碼問題就全部被約束在了只有一條規則的decoder代碼中,很顯然這是一個邊界十分明確的局部問題,因此很容易就解決了。采用這種方法,本文的漏洞利用可以隨意調用metasploit中的shellcode,無需再擔心它們的指令內部細節。


在多語言環境下,shellcode如果不是直接的內存傳遞,則可能會被系統API函數所轉換,從而導致其因在獲得執行權之前發生內容改變而無效。因此,在漏洞利用過程中,需要注意shellcode是否受到多語言版本的API影響。