使用 avcleaner 对项目进行源码级免杀
前言
SCRT安全团队开发的avcleaner工具可以对整个项目比如Meterpreter进行源码级别的免杀处理,通过分析抽象语法树的方式进行字符串混淆并重写系统调用来隐藏API函数的使用,使其绕过杀软的静态文件扫描和动态的API函数行为检测。
工具地址
https://github.com/scrt/avcleaner
简介
本文将主要介绍以下内容:
- avcleaner 原理
- avcleaner 使用方法
avcleaner 原理
avcleaner 使用Clang / LLVM工具链的libTooling对C / C ++源代码进行解析,通过分析可视化AST语法树找到字符串和函数调用。然后对字符串进行混淆,在适当的位置(包含函数或全局上下文中)插入变量定义/赋值,并且对函数进行隐藏然后混淆代码。
抽象语法树
编译器通常包括几个组件,最常见的是解析器和词法分析器。 当将源代码提供给编译器时,它首先从原始源代码(程序员编写的代码)中生成一个解析树,然后将语义信息添加到节点(编译器真正需要的)。 此步骤的结果称为抽象语法树。 维基百科展示了以下示例:
while b ≠ 0
if a > b
a := a − b
else
b := b − a
return a
这个程序的语法树如下图所示:
avcleaner 获取Clang的抽象语法树后,使用libTooling直接进行AST操作来处理源码。
字符串混淆
在以下代码情况,ESET Nod32
将标记字符串ntdll
为可疑
ntdll = LoadLibrary(TEXT("ntdll"))
可以用以下方式修改代码绕过检测:
wchar_t ntdll_str[] = {'n','t','d','l','l',0};
ntdll = LoadLibrary(ntdll_str)
第一段代码使字符串ntdll
存储在生成的二进制文件的.rdata
段内,反病毒程序很容易发现.rdata
段的字符串。 第二段代码使字符串在运行时存储在栈中,虽然IDA Pro或其他二进制分析程序也能够识别这种字符串,但是需要对二进制文件进行计算量更大的分析。
隐藏API导入
使用API函数会使链接器向PE文件导入表
(IAT) 增加导入的函数和dll文件名称。 最终函数名将在PE文件中以明文形式显示,杀软通常会检查导入表中是否有敏感函数。 针对这种检测方式最好的方法是隐藏所有的敏感函数。例如,在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);
详细的分析Clang的抽象语法树方法请参考SCRT团队的原文章 https://blog.scrt.ch/2020/06/19/engineering-antivirus-evasion/。
avcleaner 使用方法
推荐使用Docker的方式,使用官方的Dockerfile构建环境时失败了,然后按照issue的方法重新编写Dockerfile
FROM ubuntu:20.04
RUN apt update -y && apt upgrade -y
RUN apt install -y vim build-essential
RUN apt install -y libstdc++5
RUN apt install -y clang
RUN apt install -y llvm
RUN apt install -y libclang-dev
RUN apt install -y zlib1g-dev
ENV TZ=Europe/Paris
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN apt install -y cmake
RUN mkdir /avcleaner
COPY . /avcleaner
RUN cd /avcleaner && mkdir CMakeBuild
WORKDIR /avcleaner/CMakeBuild
RUN cmake ..
RUN make -j 2
build完成后运行以下命令即可,可以看到avcleaner.bin已经生成好了:
sudo docker build . -t avcleaner
sudo docker run --rm -v "${PWD}":/root -it avcleaner bash
然后需要把构建项目所需的包含路径都重新设置下,将windows SDK的库文件放进去,这里直接把VS的MSVC和windows SDK 10.0.18362.0的库文件全部放入,并在run_example.sh的基础上修改下路径:
echo "Don't forget to update the path to your local winsdk"
./avcleaner.bin "$1" --strings=true --api=true -- -D "_WIN64" -D "_UNICODE" -D "UNICODE" -D "_WINSOCK_DEPRECATED_NO_WARNINGS"\
"-I" "/usr/lib/clang/10.0.0/include" \
"-I" "/usr/lib/clang/10.0.0/" \
"-I" "include"\
"-I" "10.0.18362.0/ucrt" \
"-I" "10.0.18362.0/shared" \
"-I" "10.0.18362.0/um" \
"-I" "10.0.18362.0/winrt" \
"-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" "-fdiagno>
"-fsyntax-only" "-disable-free" "-disable-llvm-verifier" "-discard-value-names"\
"-dwarf-column-info" "-debugger-tuning=gdb" "-momit-leaf-frame-pointer" "-v"
测试一个简单的小程序:
#include <windows.h>
int main(int argc, char** argv) {
MessageBoxA(NULL, "Test", "Something", MB_OK);
MessageBoxA(NULL, "Another test", "Another something", MB_OK);
return 0;
}
直接执行
run_example.sh messagebox_simple.c
运行过程会爆很多error,但这不会影响程序获得抽象语法树,混淆后的文件会生成到messagebox_simple.c的同目录中
修改后的代码为:
#include <windows.h>
int main(int argc, char** argv) {
const char hid_Someth_P9AbMBR6oMjy[] = {'\x53','\x6f','\x6d','\x65','\x74','\x68','\x69','\x6e','\x67',0};
const char hid_Anothe_HawkSTN0F3Hb[] = {'\x41','\x6e','\x6f','\x74','\x68','\x65','\x72','\x20','\x74','\x65','\x73','\x74',0};
const char hid_Anothe_PZRsVtjkoo0b[] = {'\x41','\x6e','\x6f','\x74','\x68','\x65','\x72','\x20','\x73','\x6f','\x6d','\x65','\x74','\x68','\x69','\x6e','\x67',0};
typedef int (*_MessageBoxA)(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType);
TCHAR hid_User___p0vMrQ1P6zgn[] = {'\x55','\x73','\x65','\x72','\x33','\x32','\x2e','\x64','\x6c','\x6c',0};
HANDLE hid_hHandl_Qup09IX4JdUQ = LoadLibrary(hid_User___p0vMrQ1P6zgn);
TCHAR hid_Messag_fFBJtLlox4C0[] = {'\x4d','\x65','\x73','\x73','\x61','\x67','\x65','\x42','\x6f','\x78','\x41',0};
_MessageBoxA hid_Messag_zYQZXHW5DYRb = (_MessageBoxA) GetProcAddress(hid_hHandl_Qup09IX4JdUQ, hid_Messag_fFBJtLlox4C0);
typedef int (*_MessageBoxA)(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType);
TCHAR hid_User___aFEQM58boRNU[] = {'\x55','\x73','\x65','\x72','\x33','\x32','\x2e','\x64','\x6c','\x6c',0};
HANDLE hid_hHandl_6cvq292MKRjj = LoadLibrary(hid_User___aFEQM58boRNU);
TCHAR hid_Messag_GSgUzV76oycQ[] = {'\x4d','\x65','\x73','\x73','\x61','\x67','\x65','\x42','\x6f','\x78','\x41',0};
_MessageBoxA hid_Messag_isG1DyzPa6xX = (_MessageBoxA) GetProcAddress(hid_hHandl_6cvq292MKRjj, hid_Messag_GSgUzV76oycQ);
hid_Messag_zYQZXHW5DYRb(NULL, "Test", hid_Someth_P9AbMBR6oMjy, MB_OK);
hid_Messag_isG1DyzPa6xX(NULL, hid_Anothe_HawkSTN0F3Hb, hid_Anothe_PZRsVtjkoo0b, MB_OK);
return 0;
}
结论
SCRT安全团队开发的avcleaner工具可以对整个Meterpreter进行源码级别的免杀处理,但是在测试中发现对于比较复杂的多个函数的源代码文件进行混淆时会失败,功能仍需要进行完善。对于avcleaner更详细的讲解请看:
- https://blog.scrt.ch/2020/06/19/engineering-antivirus-evasion/
- https://blog.scrt.ch/2020/07/15/engineering-antivirus-evasion-part-ii/