启发式扫描是一项杀毒软件所采用的技术,其通过检测文件的特征和行为 AI机器学习来发现潜在的恶意软件。这种方法并不依赖于已知的病毒特征库,而是基于恶意软件的常见行为模式和特征来判断文件是否可能具有恶意。所以基于这样的原理往往会产生一些虚警(误报)
一个很好的例子就是日常生活种常见的QVM系列:
偶然间网上看见有人造谣,本人非常气愤😡
那么有哪些手段可以变绿呢?(不是戴帽子😁)
这里简单说一些可以快速变绿的基础办法:
number 1
编译器的优化标志可以影响生成的二进制文件的大小、行为和检测难度。通过优化标志,可以使生成的二进制文件更难以被杀毒软件和EDR系统检测,提高绕过防御的成功率。
cl.exe Build
cl.exe /TP /O1 /GS- /GL /EHsc /c xxx.cpp link.exe xxx.obj /SUBSYSTEM:CONSOLE /NODEFAULTLIB:msvcrt.lib /MT:libcmt.lib /OUT:xxx.exe /OPT:REF /OPT:ICF 编译器标志的解释 /TP:指定源文件是C++文件。 /O1:指定最小代码大小的优化。 /GS-:禁用缓冲区安全检查。 /GL:启用整个程序优化。 /EHsc:指定要使用的异常处理模型的类型。 /c:编译源文件并创建目标文件。 链接器标志的解释 /SUBSYSTEM:CONSOLE:指定可执行文件是控制台应用程序。 /NODEFAULTLIB:msvcrt.lib:防止链接默认的 C 运行时库。 /MT:libcmt.lib:指定要使用的C运行时库。 /OUT:GregsBestFriend.exe:指定输出文件名。 /OPT:REF:消除未使用的功能和数据代码。 /OPT:ICF:识别并消除冗余代码。
g++ Build
g++.exe -Os -s xxx.cpp -static -static-libgcc -static-libstdc++ -Wl,--subsystem,windows -o xxx.exe -Os:优化可执行文件的大小。该标志告诉 g++ 优先考虑减少生成代码的大小而不是执行速度。 -s:剥离可执行文件的所有符号表和重定位信息,减少其大小。 -static:静态链接所有库,以便将它们包含在可执行文件中。 -static-libgcc:静态链接GCC库。 -static-libstdc++:静态链接C++标准库。 -Wl,--subsystem,windows:将子系统设置为Windows,以便可执行文件作为GUI程序而不是控制台应用程序运行。 生成的GregsBestFriend.exe可执行文件应位于该g++文件夹中。
clang++ Build
clang++.exe -O2 -Ob2 -Os -fno-stack-protector -g -Xlinker -subsystem:console -o xxx.exe xxx.c -luser32 -lkernel32 -fno-unroll-loops -fno-exceptions -fno-rtti 可能降低AV/EDR软件的检测率的其他命令行选项包括: -s:剥离可执行文件的所有符号表和重定位信息,减少其大小。 -Os(GCC 和 LLVM):优化大小。 -Oz(仅限 LLVM):优化大小甚至超过-Os. -fno-unroll-loops:不要展开循环。 -fno-exceptions和-fno-rtti:禁用 C++ 异常和 RTTI(运行时类型信息)。 -finline-limit=n(GCC) 和-mllvm -inline-threshold=n(LLVM):调整内联阈值。降低该值有助于防止堆栈溢出并改善混淆。 -flto和-flto=thin(仅限 LLVM):执行链接时优化。-flto=thin可以减少编译时间,但可能会生成更大的二进制文件。 -fmerge-functions(仅限 LLVM):将相同的函数合并为一个函数。
clang++ LLVM Build
clang++.exe -O2 -Ob2 -Os -fno-stack-protector -Xlinker -pdb:none -Xlinker - subsystem:console -o xxx.exe xxx.cpp -luser32 -lkernel32 - fno-unroll-loops -fno-exceptions -fno-rtti 可能导致较低检测率的其他命令行选项 AV/EDR 软件的检测率较低,包括: -Os(GCC 和 LLVM):优化大小。 -Oz(仅限 LLVM):优化大小甚至超过-Os. -fno-unroll-loops:不要展开循环。 -fno-exceptions和-fno-rtti:禁用 C++ 异常和 RTTI(运行时类型信息)。 -finline-limit=n(GCC) 和-mllvm -inline-threshold=n(LLVM):调整内联阈值。降低该值有助于防止堆栈溢出并改善混淆。 -flto和-flto=thin(仅限 LLVM):执行链接时优化。-flto=thin可以减少编译时间,但可能会生成更大的二进制文件。 -fmerge-functions(仅限 LLVM):将相同的函数合并为一个函数。
再同时添加一些资源文件(这里面可以藏一些代码),一些合法的元数据,如版权信息等,可以使外观上更像合法的应用程序。这种操作可以提高静态检测的绕过效果。
可以编译时指定,也可以用RH提取
因为“现在的大黑阔们”都是用CL编译比较多,编译特征一直被捕获,所以当出现同样编译的程序的时候就会判定类似,使用一些小众的编译例如clang llvm可以减少此类问题
number 2
导入表处理:
1.删除CRT库
CRT(即 C 运行时库)是 C 编程语言的标准接口。CRT 包含函数和宏的集合。为标准 C 和 C++ 程序提供基本功能。它包括内存管理函数(如 malloc、memset 和 free)、字符串操作函数(如 strcpy 和 strlen)和 I/O 函数(如 printf、wprintf 和 scanf)。
CRT DLL 文件名为 vcruntimeXXX.dll,其中 XXX 是正在使用的 CRT 库的版本号。还有 api-ms-win-crt-stdio-l1-1-0.dll、api-ms-win-crt-runtime-l1-1-0.dll 和 api-ms-win-crt 等 DLL 文件-locale-l1-1-0.dll,也链接到 CRT 库。这些 DLL 中的每一个都执行特定的功能并导出多个函数。这些 DLL 文件在编译时由编译器链接,因此可以在生成的程序的导入表 (IAT) 中找到。
Hello World”程序的 IAT 应该仅导入有关 printf 函数的信息,但它导入了以下函数
多线程 (/MT)
通过选择“多线程 (/MT)”选项,可以将 Visual Studio 编译器配置为静态链接 CRT 函数。这导致诸如 printf 之类的函数直接在生成的程序中提供,而不是从 CRT DLL 文件中导入。请注意,这将增加最终二进制文件的大小,并向导入表添加更多 WinAPI 功能,但会删除 CRT 库 DLL。
其他编译器标志更改
忽略所有默认库
将“忽略所有默认库”选项设置为“是 (/NODEFAULTLIB)”,以防止编译器将默认系统库与程序链接。这将排除 CRT 库以及其他库的链接
But,不幸的是,使用此选项进行编译会导致错误,如下所示。
第一个错误“LNK2001 - 无法解析的外部符号 mainCRTStartup”表示编译器找不到符号“mainCRTStartup”的定义。这是预期的,因为“mainCRTStartup”是与 CRT 库关联的程序的入口点,但这里并非如此。要解决此问题,应该设置一个新的入口点符号
错误“LNK2001 - 无法解析的外部符号 security_check_cookie”表示编译器未找到符号“security_check_cookie”。该字符用于执行堆栈 cookie 检查,这是一项防止堆栈缓冲区溢出的安全功能。要解决此问题,请将安全检查选项设置为“禁用安全检查 (/Gs-)”
还有2个错误
这是因为尽管 CRT 库已从程序中删除,但由于使用 printf 函数进行控制台输出,所以删除CRT库时,需要编写自己版本的函数,例如printf、memcpy、memset
#include <Windows.h> #include <stdio.h> #define PRINTA( STR, ... ) \ if (1) { \ LPSTR buf = (LPSTR)HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, 1024 ); \ if ( buf != NULL ) { \ int len = wsprintfA( buf, STR, __VA_ARGS__ ); \ WriteConsoleA( GetStdHandle( STD_OUTPUT_HANDLE ), buf, len, NULL, NULL ); \ HeapFree( GetProcessHeap(), 0, buf ); \ } \ } int main() { PRINTA("Hello World ! \n"); return 0; } void* memcpy(void* dest, const void* src, size_t n) { for (size_t i = 0; i < n; i++) { ((char*)dest)[i] = ((char*)src)[i]; } } void* memset(void* Destination, int Value, size_t Size) { unsigned char* p = (unsigned char*)Destination; while (Size > 0) { *p = (unsigned char)Value; p++; Size--; } return Destination; }
禁用 C++ 异常 启用
由于不再链接 CRT 库,因此不需要此选项,应禁用该选项。
禁用整个程序优化
禁用“生成调试信息”和“生成清单”
隐藏控制台窗口
在执行时不应创建控制台窗口,因为这是可疑的
完成所有步骤后,将获得以下结果。
1.可执行文件大小从减少到大约 3 KB
在 IAT 中找不到未使用的API
IAT 伪装
但是如果二进制文件导入很少的 WinAPI 函数,尤其是与 API 哈希结合使用,这反而会引起怀疑
让软件看起来像正常软件非常重要。使用虚拟 IAT可以解决这个问题
//在 if 条件内调用多个永远不会执行的 WinAPI 函数 int z = 4; if (z > 5) { unsigned __int64 i = MessageBoxA(NULL, NULL, NULL, NULL); i = GetLastError(); i = SetCriticalSectionSpinCount(NULL, NULL); i = GetWindowContextHelpId(NULL); i = GetWindowLongPtrW(NULL, NULL); i = RegisterClassW(NULL); i = IsWindowVisible(NULL); i = ConvertDefaultLocale(NULL); i = MultiByteToWideChar(NULL, NULL, NULL, NULL, NULL, NULL); i = IsDialogMessageW(NULL, NULL); }
效果:
number 3
Obfuscator LLVM编译混淆
LLVM 是一个编译器基础设施。
编译过程三个步骤:
-
前端,包括:
-
扫描器,对代码进行词法分析并生成标记(具有特定含义的字符串)
-
解析器,它生成一个抽象语法树(标记分组在树中,代表源代码中实现的实际算法)
-
语义分析(主要是类型检查),在此期间检查 AST 是否存在错误,例如在初始化之前错误使用类型或使用变量
-
中间表示的生成,通常基于 AST
-
-
优化,旨在通过预先计算等方式降低代码复杂性。优化不得改变算法/程序本身。
-
后端,将中间表示转换为预期输出(程序集或字节码)。
LLVM的核心思想是将编译器分为前端和后端两个部分,前端负责将源代码转换为中间表示(IR),后端负责将中间表示转换为目标机器的汇编代码。这种设计使得LLVM可以支持多种编程语言,因为只需要为每种语言编写一个前端,就可以利用后端的通用性支持多种目标架构。
OLLVM(Obfuscator-LLVM)是基于LLVM框架的混淆器,它可以对程序进行混淆以提高程序的安全性。OLLVM的设计目标是提供一种灵活的、可定制的混淆方案,使得攻击者更难理解和分析程序的行为。
OLLVM通过对程序进行多种混淆操作来实现混淆效果,例如代码替换、函数内联、控制流平坦化、加密等。这些混淆操作可以改变程序的控制流图和数据流图,使得程序更难以被理解和逆向分析。同时,OLLVM还提供了一些额外的安全机制,例如加密程序的字符串、使用栈保护和位置无关代码等,以增加程序的安全性。
由于OLLVM是基于LLVM框架开发的,它可以与现有的LLVM工具和编译器集成,例如Clang和LLDB等。这使得开发者可以轻松地在现有的开发环境中使用OLLVM,并且可以使用现有的工具对混淆后的程序进行调试和分析。
尽管OLLVM的主要目的是提高程序的安全性,但它也可以用于其他领域,例如代码保护、代码压缩和代码优化等。由于其灵活性和可定制性,OLLVM已经被广泛应用于许多领域,例如网络安全、游戏开发和金融等。
用法
首先,从以下 URL 下载 Obfuscator LLVM 并将 clang-cl.exe 可执行文件复制到目录中。
https://github.com/wwh1004/ollvm-16
创建 Visual Studio C++ 项目。在项目属性中,将平台工具集设置为 LLVM (clang-cl)。
clang-cl 可执行文件的位置包含在可执行目录中。
编译
-mllvm -bcf -mllvm -bcf_prob=73 -mllvm -bcf_loop=1 -mllvm -sub -mllvm -sub_loop=5 -mllvm -fla -mllvm -split_num=5 -mllvm -aesSeed=1234BEEFDEAD1234DEADBEEFDEAD1234
效果:
老项目:https://github.com/obfuscator-llvm/obfuscator
实验参考:https://trustedsec.com/blog/behind-the-code-assessing-public-compile-time-obfuscators-for-enhanced-opsec
number4
arsenal-kit,被打了太多签名了......
加载工具包生成beacon,会被defender匹配签名
使用ThreatCheck查找“坏字节”
加载到Ghidra 运行其自动分析。然后使用搜索内存功能查找 ThreatCheck 输出的字节序列
OK,问题出在这个For循环。搜索Cobalt Strike的Artifact Kit源代码,找到位于patch.c中的spawn功能。这个循环负责在执行之前解码Beacon shellcode,并将其写入分配的内存区域
所需要做的就是修改循环,使其编译成不同的字节序列(如何做取决于你)。然后,重新生成Beacon,Defender将不再匹配该静态签名
不过.....
所以继续......
可以看到这是在创建命名管道,命名管道是管道服务器和管道客户端之间进行通信的单向或双向管道。有关命名管道的更多信息,请参阅Win32 IPC文档。命名管道是一种并非所有沙箱都能正确模拟的技术。通过命名管道传递paylaoad将使不支持命名管道的AV工具无法看到paylaod,命名管道也是arsenal-kit工具包已有的一项技术,可以在Artifact Kit 文件夹中的src-common/bypass-pipe.c
文件找到
一般情况下,我们不希望使用模板提供的默认值,因为这些值触发签名检测的几率较高。所以先分析怎么改:
在源码中,我们可以看到该字符串用于分配管道名称。管道名称的分配由以下函数完成:
sprintf(pipename, "%c%c%c%c%c%c%c%c%cnetsvc\\%d", 92, 92, 46, 92, 112, 105, 112, 101, 92, (int)(GetTickCount() % 9898));
执行时,该字符串将被转换为//./pipe/netsvc
字符串,随后是一个使用tick 计数生成的数字。tick计数用于为字符串添加一些随机性。该字符串可能会触发签名检测,因此我们应该用其他内容替换名称。查找有效名称的方法是查看当前系统上已存在的不会被匹配签名的管道。在 Windows系统中,我们可以通过在Powershell中运行以下命令来查看所有打开的管道:
(get-childitem \\.\pipe\).FullName
可以看到最多的管道是mojo.x.x.x
系列,mojo 管道是Chromium框架的一部分。Google Chrome浏览器、Microsoft Edge、Microsoft Teams和其他许多流行的应用程序都使用Chromium。由于微软软件使用这些Chromium管道,因此这些管道在大多数windows系统中都很常见。查看谷歌浏览器源码:
https://raw.githubusercontent.com/chromium/chromium/c4d3c31083a2e1481253ff2d24298a1dfe19c754/mojo/public/cpp/platform/named_platform_channel_win.cc
我们可以看到,管道以mojo 名称开头,然后是进程ID、线程ID和一个随机整数。
So:
HANDLE hProc = GetCurrentProcess(); srand((int)(GetTickCount())); sprintf(pipename, "%c%c%c%c%c%c%c%c%c\\mojo.%d.%d.%d", 92, 92, 46, 92, 112, 105, 112, 101, 92, (int)(GetProcessId(hProc)), (int)(GetCurrentThreadId()), rand());
然后重新编译工具包:./build_arsenal_kit.sh
此时直接生成beacon,defender已经匹配不到签名
number 5
二进制patch,挂钩 ExitProcess 来重定向执行流,code cave等等,很基础的技术,学过PE的应该都会,省略。。。(懒)