Gargoyle——內存掃描逃逸技術
0x00 前言
Gargoyle是一種在非可執行內存中隱藏可執行代碼的技術。在一些程序員定義的間隔,gargoyle將蘇醒,且一些ROP標記它自身為可執行,且做一些事:
這個技術是針對32位的Windows闡述的。在本文中,我們將深入探討其實現細節。
0x01 實時內存分析
執行實時內存分析是一個相當大代價的操作,如果你使用Windows Defender,你可能在這個問題上就到頭了(谷歌的反惡意軟件服務)。因為程序必須在可執行的內存中,用于減少計算負擔的一種常用技術是只限制可執行代碼頁的分析。在許多進程中,這將數量級的減少要分析的內存數量。
Gargoyle表明這是個有風險的方式。通過使用Windows異步過程調用,讀寫內存能被作為可執行內存來執行一些任務。一旦它完成任務,它回到讀寫內存,直到定時器過期。然后重復循環。
當然,沒有Windows API InvokeNonExecutableMemoryOnTimerEx。得到循環需要做一些操作。
0x02 Windows異步過程調用(APC)
異步編程使一些任務延遲執行,在一個獨立的線程上下文中執行。每個線程有它自己的APC隊列,并且當一個線程進入alertable狀態,Windows將從APC隊列中分發任務到等待的線程。
有一些方法來插入APC:
ReadFileEx
SetWaitableTimer
SetWaitableTimerEx
WriteFileEx
進入alertable狀態的方法:
SleepEx
SignalObjectAndWait
MsgWaitForMultipleObjectsEx
WaitForMultipleObjectsEx
WaitForSingleObjectEx
我們要使用的組合是用CreateWaitableTimer創建一個定時器,然后使用SetWaitableTimer插入APC隊列:
默認的安全屬性是fine,我們不想手動重置,并且我們不想要一個命名的定時器。因此對于CreateWaitableTimer所有的參數是0或者nullptr。這個函數返回一個HANDLE,表示我們新的定時器。接下來,我們必須配置它:
第一個參數是我們從CreateWaitableTimer得到的句柄。參數pDueTime是一個指向LARGE_INTEGER的指針,指定第一個定時器到期的時間。例如,我們簡單的設為0(立即過期)。lPeriod定義了過期間隔(毫秒級)。這個決定了gargoyle調用的頻率。
下個參數pfnCompletionRoutine將是我們要努力的主題。這是來自等待線程的Windows調用的地址。聽起來很簡單,除非在可執行內存中分發的APC沒有一個gargoyle代碼。如果我們將pfnCompletionRoutine指向gargoyle,我們將觸發數據執行保護(DEP)。
取而代之,我們使用一些ROP gadget,將重定向執行線程的棧到lpArgToCompletionRoutine指向的地址。當ROP gadget執行,指定的棧在調用gargoyle第一條指令前調用VirtualProtectEx來標記gargoyle為可執行。
最后一個參數與在定時器到期后是否喚醒計算機有關。我們設置為false。
0x03 Windows數據執行保護和VirtualProtectEx
最后是VirtualProtectEx,用來修改各種內存保護屬性:
我們將在兩種上下文中調用VirtualProtectEx:在gargoyle完成執行后(在我們觸發線程alertable之前)和在gargoyle開始執行之前(在線程分發APC之后)。看資料了解詳情。
在這個PoC中,我們將gargoyle,跳板,ROP gadget和我們的讀寫內存都放進同一個進程中,因此第一個參數hProcess設置為GetCurrentProcess。下一個參數lpAddress與gargoyle的地址一致,dwSize與gargoyle的可執行內存大小一致。我們提供期望的保護屬性給flNewProtect。我們不關心老的保護屬性,但是不幸的是lpflOldProtect不是一個可選的參數。因此我們將將它設為一些空內存。
唯一根據上下文不同的參數是flNewProtect。當gargoyle進入睡眠,我們想修改它為PAGE_READWRITE或0x04。在gargoyle執行前,我們想標記它為PAGE_EXECUTE_READ或0x20。
0x04 棧跳板
注意:如果你不熟悉x86調用約定,本節將比較難以理解。對于新人,可以參考我的文章x86調用約定。
通常,ROP gadget被用來對抗DEP,通過構建調用VirtualProtectEx來標記棧為可執行,然后調用到棧上的一個地址。這在利用開發中經常很有用,當一個攻擊者能寫非可執行內存。可以將一定數量的ROP gadget放在一起做相當多的事。
不幸的是,我們不能控制我們的alerted線程的上下文。我們能通過pfnCompletionRoutine控制eip,并且線程棧中的指針位于esp+4,即調用函數的第一個參數(WINAPI/__stdcall調用約定)。
幸運的是,我們已經在APC入隊前就執行了,因此我們能在我們的alerted線程中小心的構建一個新的棧(棧跳板)。我們的策略是找到代替esp指向我們棧跳板的ROP gadget。下面的形式的就能工作:
有點詭異,因為函數通常不以pop esp/ret結束,但是由于可變長度的操作碼,Intel x86匯編過程會產生非常密集的可執行內存。不管怎樣,在32位的mshtml.dll的偏移7165405處有這么一個gadget:
注意:感謝Sascha Schirra的Ropper工具。
在我們調用SetWaitableTimer時,這個gadget將設置esp為我們放入lpArgToCompletionRoutine中的任何值。剩下的事就是將lpArgToCompletionRoutine指向一些構造的棧內存。棧跳板看起來如下:
我們設置lpArgToCompletionRoutine為void* VirtualProtectEx參數,以便ROP gadget能ret到執行VirtualProtectEx。當VirtualProtectEx得到這個調用,esp將指向void* return_address。我們可以設置這個為我們的gargoyle。
0x05 gargoyle
讓我們暫停片刻,看下在我們創建定時器和啟動循環之前創建的讀寫Workspace。這個Workspace包含3個主要內容:一些配置幫助gargoyle啟動自身,棧空間和StackTrampoline:
你已經看見了StackTrampoline,和stack是一個內存塊。SetupConfiguration:
在PoC的main.cpp中,SetupCOnfiguration這么設置:
非常簡單。簡單的指向多個Windows函數和一些有用的參數。
現在你有了Workspace的大概印象,讓我們回到gargoyle。一旦棧跳板被VirtualProtectEx調用,gargoyle將執行。這一刻,esp指向old_protections,因為VirtualProtect使用WINAPI/__stdcall約定。
注意我們放入了一個參數(void* setup_config)在StackTrampoline的末尾。這是方便的地方,因為如果它是以__cdecl/__stdcall約定調用gargoyle的第一個參數。
這將使得gargoyle能在內存中找到它的讀寫配置:
現在我們準備好了。Esp指向了Workspace.stack。我們在ebx中保存了Configuration對象。如果這是第一次調用gargoyle,我們將需要建立定時器。我們通過Configuration的initialized字段來檢查這個:
如果gargoyle已經初始化了,我們跳過定時器創建。
注意,在reset_trampoline中,我們重定位了跳板中VirtualProtectEx的地址。在ROP gadget ret后,執行VirtualProtectEx。當它完成后,它在正常的函數執行期間將破環棧上的地址。
你能執行任意代碼。對于PoC,我們彈出一個對話框:
一旦我們完成了執行,我們需要構建調用到VirtualProtectEx,然后WaitForSingleObjectEx。我們實際上構建了兩個調用到WaitForSingleObjectEx,因為APC將從第一個返回并繼續執行。這啟動了我們定義的循環APC:
0x06 測試
PoC的代碼在github上,且你能簡單的測試,但是你必須安裝:
Visual studio 2015 Community,但是其他版本也能用
Netwide Assembler v2.12.02 x64,但是其他版本也能用。確保nasm.exe在你的路徑中。
克隆gargoyle:
- git clone https://github.com/JLospinoso/gargoyle.git
打開Gargoyle.sln并構建。
你必須在和setup.pic相同的目錄運行gargoyle.exe。默認解決方案的輸出目錄是Debug或release。
每15秒,gargoyle將彈框。當你點擊確定,gargoyle將完成VirtualProtectEx/WaitForSingleObjectEx調用。
有趣的是,使用Systeminternal的VMMap能驗證gargoyle的PIC執行。如果消息框是激活的,gargoyle將被執行。反之則沒有只想能夠。PIC的地址在執行前使用stdout打印。