前不久,Go官方修复了CVE-2018-6574这个漏洞,这个漏洞又是涉及软件编译环节,和2015年Xcode被污染类似,攻击者可以通过在软件编译环节插入恶意数据从而执行任意代码,虽然原理并不复杂,但有很好的警示意义。
什么是Go语言?
Go 是一个Google推出的开源编程语言,它能让构造简单、可靠且高效的软件变得更容易,且有着更高的开发效率和运行性能,因此受到了许多开发者的欢迎。
Go 程序源码 以 *.go 结尾,通过 go build 编译成native代码。
以最简单的hello world程序为例:hello.go
// hello.go package main import "fmt" func main() { fmt.Println("Hello, World!") }
通过go build编译出可执行程序,再运行 ./hello 。也可以直接通过 go run ./hello.go 直接在/tmp目录下编译生成可执行文件,并运行。
同时Go语言允许与C语言的互操作,即在Go语言中直接使用C代码,因为本身Go在语法等方面和C就很像,其设计者以及其设计目标都与C语言有着千丝万缕的联系。
例如下面的代码,我们可直接在Go源码文件中嵌入了C代码,并用注释包裹起来
package main /* #include #include void my_print(char *str){ printf("%s\n",str); } */ import "C" import "unsafe" import "fmt" func main() { fmt.Println("Hello, World!") s:="hello cgo" cs:=C.CString(s) C.my_print(cs) C.free(unsafe.Pointer(cs)) }
我们依然可以直接通过go build或go run来编译和执行,但实际编译过程中,go调用了名为cgo的工具,cgo会识别和读取Go源文件中的C元素,并将其提取后交给C编译器编译,最后与Go源码编译后的目标文件链接成一个可执行程序。
CVE-2018-6574 漏洞分析
参看CVE公告,这个漏洞是由于在源码编译时,未禁止 “-fplugin=”这类的参数导致在使用gcc/clang编译时产生代码执行。
上面已经说了在Go源码文件中可以嵌入了C代码,同时cgo会识别和读取Go源文件中的C元素,并将其提取后交给C编译器编译。
cgo调用gcc或者clang编译提取出的C代码。
gcc/clang这类的C编译器自然都有CFLAGS,LDFLAGS等编译开关让开发者在编译时指定设置。
cgo作为一个gcc的封装,自然也支持这类的编译开关选项。而gcc编译时,可以通过“-fplugin”指定额外的插件,gcc在编译时会加载这个插件。
因此,除了在Go源码文件中可以嵌入了C代码之外,还可以指定通过“#cgo CFLAGS”指定gcc编译时的恶意插件。
cgo在解析到CFLAGS关键字时,会将后面的编译选项传递给gcc。
“-fplugin”指定额外的插件,可为任意的动态库,因此就获得了代码的执行权限。
package main /* #cgo CFLAGS: -fplugin=./foo.so #include #include void print(char *str){ printf("%s\n",str); } */ import "C" import "unsafe" import "fmt" func main() { fmt.Println("Hello, World!") s:="hello cgo" cs:=C.CString(s) C.print(cs) C.free(unsafe.Pointer(cs)) }
对于本地自己编写的程序或许不会有问题,因为毕竟不会自己去加载执行恶意代码。
但当需要获取远程代码执行“go get”时,就可能出现问题。“go get”先从远程地址下载源码,再执行go build并安装。
例如,我们经常会从github上获得Go的项目或第三方包,这些第三方包如果未经检查,就完全可能借助这个漏洞执行任意代码。
我们通过本地反弹shell来演示一下这个漏洞执行的流程,如下图。
CVE-2018-6574 防御修复
Go官方在最新版本中,加强了对编译和链接环节的检查过滤。
只允许指定的编译链接选项代入gcc执行,其他未经允许的都会被禁止。
总结
本文分析了CVE-2018-6574漏洞的成因,可以看到编译是软件构建过程中一个重要环节,保证软件可信,不被来自外界的病毒、恶意代码破坏,不仅要从程序代码本身着手,还应从编译,生成,分发等各方面努力,保证整个软件供应链体系的安全可信。