使用"Go语言"开发客户端程序

Go的使命是为Google开发服务端软件,但是由于它编译成二进制文件的特性,我们也能用它来开发客户端软件。我就做了这么一次尝试,其中大大小小的坑踩了不少,大部分都被解决了,但是还有少数却很致命的没有办法解决的问题,写出来供大家参考。

Go写应用代码是非常爽的,这点和C++形成了很明显的对比,C++每个功能都要去找第三方库编译、引入,这些环境部署工作非常痛苦。而我的工程又有一些地方需要调用大量的Windows API,所以我选择了Go/C++这个组合,各自负责不同的代码,通过动态库互相调用。不用CGO的原因很简单,我习惯了C++的标准库和代码,受不了C语言那样原始的字符串操作。

我的C++和Go的工程都是以DLL的方式提供,所以Go的工程也要编译成DLL,这里我就踩到了第一个坑:Go编译出来的DLL导出函数不受控,会出现很多乱七八糟的导出函数,这样会给别人逆向工程提供很大的方便,我在网上搜解决方案的时候,看到了遭遇相同问题的朋友。 

开始我以为这是Go的问题,后来无意中发现了别人编译DLL的方法,然后就解决了。有一个开发者分享的编译步骤是,先把Go的DLL工程编译成静态库,再通过GCC链接成动态库,这样在Windows下就可以用Mingw64来引入def文件,控制导出函数了,具体如下:

go build -ldflags "-s -w" -buildmode=c-archive -o test.lib

gcc -m32 -shared -o test.dll test.def test.lib -static -lwinmm -lWs2_32 -s -w

这样一个干净的,没有多余导出的DLL就诞生了。这里我给GCC命令也加上了-s -w,因为如果你要自己最终链接,-ldflags设置的去除调试符号命令是无效的,所以要在链接的时候传入(-s -w)。值得一提的是,去除调试符号也很重要,不然别人开个逆向软件就把你的内部结构看得一清二楚了。

导出表的问题解决之后,我紧接着就遇到了第二个问题,由于我是在64位的Windows上开发的,但是这个工程我要编译出32位的DLL,可Mingw64只能安装64/32位其中一个版本,并不具备交叉编译的能力。所以一个问题又出现了,怎么在64位下使用32位的Mingw64。

我开了一台虚拟机,安装了i686的Mingw64,然后把目录文件复制出来,并且手写了一个用于Go编译的BAT。

set MINGW=mingw\x86\bin

set PATH=%MINGW%;%path%

set GOARCH=386

set CGO_ENABLED=1

go build -ldflags "-s -w" -buildmode=c-archive -o test.lib

gcc -m32 -shared -o test.dll test.def test.lib -static -lwinmm -lWs2_32 -s -w

遇到的第二个问题再次解决。

当事情进行到这里,你以为你已经编译了一个非常干净纯洁的DLL,但是还没有!用Go开发客户端软件这个事情并没有结束!

因为如果你写的客户端软件需要客户掏钱买,你肯定不希望一些关键代码被破解,所以你肯定就要加壳。找了半天你终于发现一款支持Go语言的壳,兴冲冲的加上了VM标记之后,发现只要程序一跑起来就崩溃了,然后你也崩溃了。

这个问题我没有找到解决办法,初步尝试的结果是与线程无关的地方可以随便VM,但是跟线程有关的地方加VM就100%崩溃,但是Go的这种异步当同步的写法,谁又能断定代码不会突然跑进线程执行呢。当然,最终还可以选择不加VM,但加壳保护的效果就会大打折扣。

等等!事情还没完,加壳的事情告一段落之后,我又发现一个巨坑,我打开远古时期的C32ASM,发现了很多内部函数名还在编译出来的二进制文件里,连源码路径都在里面。

这TM是怎么回事???

我冷静下来之后发现,这些信息是在panic的时候出现的,于是我写了一个demo。呵呵,果然文件名和函数名出错时全暴露出来了,配上堆栈追踪信息,不知道逆向工程的人分起来有多方便。但是我IDA了一番之后,发现这些信息无法移除!!因为我根本找不到这些代码在哪里。

我猜测Go应该是吧这些信息存到了一个统一的结构里,出错了再根据特征去读取这个结构的信息,而且有些功能还要依靠这个结构来完成,所以路径信息和函数名信息无法移除只能修改。

到这里,我已经无能为力,所以折腾了半个月后我只能忍痛暂时放弃使用Go来开发一部分客户端功能,暂时回到C++的路上。

最近发现Rust具备开发客户端软件的实力,没有出现Go这个编译器的问题,所以我要学习一下Rust。

别跟我提C++,头疼。