cgo
是C与Go的交叉编译方法。
CGO初探
不快速入门
对着别人的博客一步一步看,反而越写越多了。
无调用
开启cgo
特性:头部添加import "C"
标识符。
1 | package main |
然后编译。
1 | go build -x .\hello.go |
-x
选项可以详细展示出编译环节中的每一步指令。
比如有
1 | gcc -fno-caret-diagnostics -c -x c - -o "$WORK\\3676135165" || true |
说明在cgo
时期,会调用gcc
编译器来进行处理。
调用puts
带有打印字符串的:
1 | package main |
开启cgo
特性后,cgo
会将上一行代码所处注释块的内容视为C代码块,被称为序文(preamble)。所以这里我们引用了C的stdio
库用于打印字符串。
1 | .\test1.exe |
逆向
1 | void __golang main_main() |
1 | mov [rsp], rax ; _r1 |
函数调用还是基于栈的。
有几个与cgo
有关的函数
1 | void __golang main__cgo_cmalloc(uint64 p0, void *r1) |
malloc Wrapper
函数,调用runtime.cgocall()
函数调用了真的malloc
。
1 | void cgo_1078dd1b7650_Cfunc__Cmalloc() |
1 | void __golang main__Cfunc_CString(main__Ctype_char *_r1, string s) |
CString
函数通过调用cmalloc
来在堆上生成一个C风格字符串。
1 | void __golang main__Cfunc_puts(main__Ctype_char *p0, main__Ctype_int r1) |
puts Wrapper
函数。调用了真的puts
。
1 | void cgo_1078dd1b7650_Cfunc_puts() |
直接的puts
,malloc
这种函数必须透过一层wrapper
函数,通过runtime.cgocall()
来调用,因为栈以及寄存器调用规定都不一样。
调用自己的函数
1 | package main |
逆向
1 | __int64 __golang main__Cfunc_SayHello(const char *fn) |
C与Go分离
和上面一样,但是是单独将C语言放在同文件夹下的另一个文件中,这样便于编写。
1 | // sayhello.c |
1 | // sayhello.go |
1 | go build -x .\sayhello.go |
这样编译的时候会自动同时编译两个文件,然后最后链接整合到一个可执行程序下。具体二进制内容和上一个例子一样。
调用C++函数
网上给的那种直接使用hello.h
的公共接口的方式我没成功,网上讲的太简洁了,感觉有点注水。中文博客不能乱信。
下面说一下单独使用dll
方式来调用C语言的方法。
1 | // hello.h |
1 | // hello.cpp |
然后
1 | g++ .\hello.cpp -fPIC -shared -o .\hello.dll |
生成动态链接库。
1 | // hello.go |
1 | go build .\hello.go |
在注释块中声明了编译与链接设置,即制定了链接文件夹,以及链接的dll
文件。
1 | .\hello.exe |
执行成功。
逆向
这样操作确实属于挺正常的操作。据说有不少人是这样来在Go中调用opencv
,ffmpeg
等知名C库函数的。
前面基本一样,不过最后跳转到了外部引用罢了。
1 | // attributes: thunk |
在Imports
窗口也能知道调用了啥函数。这里写了_IAT_start__();
大抵是由于这是在IAT表中的第一个函数引用而导致的,并不代表IDA没能识别出这个函数。
anyway,这样外部引用DLL,就使得一般混淆方法得以在Go程序中实现(算得上是0.1个Go混淆),通过混淆C库函数,从而严重影响逆向进度。
当然动态链接库应该还有其他骚操作,另分析。
Go实现C DLL库/静态库
这个例子大致的意思就是,先把Go函数通过export
导出成C函数,然后再重新链接到一个Go主程序上。
不过网上的博客又没教怎么去具体编译,所以又得我自己去找法子。
我的解决方法是:将Go编译成DLL,然后再动态链接。
hello.go
实现函数
1 | // hello.go |
go语言动态库的编译和使用 - 简书 (jianshu.com)
注意:
- 必须导入 “C” 包
- 必须在可外部调用的函数前加上
//export 函数名
的注释 - 必须是
main
包,切含有main
函数,main
函数可以什么都不干。
2.9 静态库和动态库 · Go语言高级编程 (studygolang.com)
然后编译成静态库或者动态链接库。都一样只要稍微改个参数即可。
1 | go build -buildmode=c-archive -o hello.lib hello.go |
我编译成动态库。
IDA逆一下
1 | void __golang main_SayHello(main__Ctype_char *str) |
很显著的Go风格。不过由于声明形参时使用的是*C.str
,所以函数原型属于是C语言友好的。在C语言程序中直接调用它不会产生bug。
后来我在分析bug的时候发现还另有2个函数,
void SayHello()
和void __golang cgoexp_42d13d8d5572_SayHello(struct_{_main_p0__main__Ctype_char_} *a)
函数。
cgoexp_42d13d8d5572_SayHello
函数是main_SayHello
的一个wrapper
函数。而直接的
SayHello
函数
1
2
3
4
5
6
7
8 void SayHello()
{
uintptr_t inited; // rbx
inited = cgo_wait_runtime_init_done();
crosscall2();
cgo_release_context(inited);
}没有什么实质的东西。
更正:见下
同时,编译时自动生成了hello.h
文件,里面拥有必要的结构体声明和函数声明。
然后写一份hello.c
来调用Go函数。
1 |
|
但是!现在再直接编译还是会出错。
1 | gcc -o hello.exe .\hello.c .\hello.dll |
它会爆
1 | hello.go:4:19: error: #include nested too deeply |
的错误。
(67条消息) Error #include nested too deeply_ysdaniel的专栏-CSDN博客
通常的原因是,两个.h
互相交叉引用,A引用B,B又引用A,从而使得进入另一个引用死循环。检查hello.h
1 | //... |
确实如此,自己引用了自己。
把#include "hello.h"
这一行注释掉就好了。然后编译顺利。
1 | .\hello.exe |
值得一提的是,当我准备这样子绕一圈后重新将DLL加载到Go程序中时,反而发生了错误。
1 | // go run ../demo |
1 | go build -x .\main.go |
执行后会报错。
调试后发现,main.exe
引用到了void SayHello()
这个没用的函数上面(指没有核心逻辑)(更正:见下),而有用的是main_SayHello(char*)
(Go默认会前面加包名前缀),从而导致了异常。
其他
Go 与 C 的桥梁:cgo 入门,剖析与实践 - 知乎 (zhihu.com)
后面与逆向关系不大。
gc, cgo与gccgo
gc
Go Compiler
,原生Go语言编译器。就是经典的Plan 9汇编那种东西。
cgo
简单来说就是把Go当成了一种类似于Python一样的胶水语言,能够在Go中引用C的库函数。本质上还是原生Go编译器的一个延申。
感觉和ctypes
有点类似。不过那个的话Python需要引用单独编译后的C库,而不像这个可以直接写在源码注释块内。
gccgo
一个船新的Go编译器。能够利用大部分gcc
的优化策略,使得程序运行效率得以提升(比如栈传参变成寄存器传参等)
更正
void SayHello()
正是hello.dll
的导出函数。根据调试,引用此库的C程序确实跳转到了这里,且这里能正常执行。
但是Go程序也是跳转到了这里,但是却不能正常执行。我感觉Go源码的声明应该是符合规定的,没有参数的冲突,就很奇怪。
关于此“无用”函数中的crosscall2
:
【Free Style】CGO: Go与C互操作技术(二):C调Go基本原理-云社区-华为云 (huaweicloud.com)
一种猜想,那就是这种生成C-DLL后,专门就是为C服务的,大抵无法重新用于Go中。
参考
Go 与 C 的桥梁:cgo 入门,剖析与实践 - 知乎 (zhihu.com)
- 本文作者: Taardis
- 本文链接: https://taardisaa.github.io/2022/03/07/cgo/
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!