自动重构Meterpreter绕过杀软

阅读量    100009 |   稿费 160

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

前言

此文章记录了我们对杀毒软件某些方面的研究,以及我们如何设法自动重构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在编译时混淆代码库。

显然,后者将是首选方法,这是许多流行的研究得出相同的结论。 主要原因是,转换遍历可以只编写一次,而可以独立于软件的编程语言或目标体系结构重复使用。

LLVMCompiler

但是,这样做需要有用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.dllSamEnumerateUsersInDomain出现在已编译的二进制文件中。

要解决此问题,可以在运行时使用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);
}

替换字符串

由于CallExprInitListExpr都可以用类似的方式处理,因此我们定义了一个公共的可用函数。

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来查询函数体的起始位置。 这是插入变量声明的理想位置,因为该变量将在整个函数中可见,并且我们插入的标记不会覆盖内容。

在任何情况下,这两种情况都可以通过访问每一个父节点来解决,直到找到一个FunctionDeclVarDecl类型的节点为止:

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

如您所见,这很好用。 现在,此示例非常简单,可以使用正则表达式解决,并减少代码行数。

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多