mirror of
https://github.com/praetorian-inc/goffloader
synced 2026-08-09 13:04:48 +00:00
Initial Code Commit
This commit is contained in:
@@ -1,2 +1,12 @@
|
||||
# open-source-template
|
||||
A template for our open source project releases.
|
||||
# Goffloader - A pure Go implementation of an in-memory COFFLoader (and PE loader)
|
||||
|
||||
More details coming soon!
|
||||
|
||||
# Acknowledgements
|
||||
|
||||
* Ne0nD0g's [go-coff project](https://github.com/Ne0nd0g/go-coff/tree/dev)
|
||||
* Didn't realize this the dev branch of go-coff was actually filled in when this project was started. The Golang implementation of Beacon functions was the base for the `lighthouse` code along with the idea to use windows.NewCallback to avoid CGO.
|
||||
* TrustedSec's [COFFLoader blogpost](https://trustedsec.com/blog/coffloader-building-your-own-in-memory-loader-or-how-to-run-bofs)
|
||||
* OtterHacker's EXCELLENT [COFFLoader blogpost](https://otterhacker.github.io/Malware/CoffLoader.html)
|
||||
* Fortra's [No-Consolation BOF](https://github.com/fortra/No-Consolation)
|
||||
* The developers of the [(now-archived) Go pecoff library](https://github.com/RIscRIpt/pecoff).
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"github.com/praetorian-inc/goffloader/src/coff"
|
||||
"github.com/praetorian-inc/goffloader/src/lighthouse"
|
||||
)
|
||||
|
||||
//go:embed whoami.x64.o
|
||||
var whoamiBofBytes []byte
|
||||
|
||||
func main() {
|
||||
// Note that args need to be prefaced with their type string as expected in aggressor scripts
|
||||
// see an example of this in pe_windows.go for a more complex arg set
|
||||
argBytes, err := lighthouse.PackArgs([]string{"zArgs", "zYou", "zWant"})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
output, err := coff.Load(whoamiBofBytes, argBytes)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(output)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#include <stdio.h>
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
while (--argc > 0)
|
||||
printf((argc > 1) ? "%s " : "%s", *(++argv));
|
||||
printf("\n");
|
||||
return 0;
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"github.com/praetorian-inc/goffloader/src/pe"
|
||||
)
|
||||
|
||||
// code for this is in hello.c
|
||||
//
|
||||
//go:embed hello.exe
|
||||
var helloBytes []byte
|
||||
|
||||
func main() {
|
||||
output, err := pe.RunExecutable(helloBytes, []string{"Arg1", "Arg2", "Arg3"})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(output)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
module github.com/praetorian-inc/goffloader
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/RIscRIpt/pecoff v0.0.0-20200923152459-a332238caa87
|
||||
golang.org/x/sys v0.22.0
|
||||
)
|
||||
@@ -0,0 +1,344 @@
|
||||
package coff
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"github.com/praetorian-inc/goffloader/src/lighthouse"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/RIscRIpt/pecoff"
|
||||
"github.com/RIscRIpt/pecoff/binutil"
|
||||
"github.com/RIscRIpt/pecoff/windef"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
/*
|
||||
NOTE: There are random fmt.Sprintfs sprinkled through the code - these are intentional
|
||||
and seem to break static Go malware signatures. LEAVE THEM IN PLACE. If this starts
|
||||
getting detected again, add some more fmt.Sprintfs.
|
||||
*/
|
||||
|
||||
const (
|
||||
MEM_COMMIT = windows.MEM_COMMIT
|
||||
MEM_RESERVE = windows.MEM_RESERVE
|
||||
MEM_TOP_DOWN = windows.MEM_TOP_DOWN
|
||||
PAGE_EXECUTE_READWRITE = windows.PAGE_EXECUTE_READWRITE
|
||||
// PAGE_EXECUTE_READ is a Windows constant used with Windows API calls
|
||||
PAGE_EXECUTE_READ = windows.PAGE_EXECUTE_READ
|
||||
// PAGE_READWRITE is a Windows constant used with Windows API calls
|
||||
PAGE_READWRITE = windows.PAGE_READWRITE
|
||||
|
||||
// Characteristic Flag that implies a section should be executable
|
||||
IMAGE_SCN_MEM_EXECUTE = 0x20000000
|
||||
)
|
||||
|
||||
var (
|
||||
kernel32 = syscall.MustLoadDLL("kernel32.dll")
|
||||
procVirtualAlloc = kernel32.MustFindProc("VirtualAlloc")
|
||||
procVirtualProtect = kernel32.MustFindProc("VirtualProtect")
|
||||
)
|
||||
|
||||
func resolveExternalAddress(symbolName string, outChannel chan<- interface{}) uintptr {
|
||||
if strings.HasPrefix(symbolName, "__imp_") {
|
||||
symbolName = symbolName[6:]
|
||||
// 32 bit import names are __imp__
|
||||
if strings.HasPrefix(symbolName, "_") {
|
||||
symbolName = symbolName[1:]
|
||||
}
|
||||
|
||||
libName := ""
|
||||
procName := ""
|
||||
// If we're following Dynamic Function Resolution Naming Conventions
|
||||
if len(strings.Split(symbolName, "$")) == 2 {
|
||||
libName = strings.Split(symbolName, "$")[0] + ".dll"
|
||||
procName = strings.Split(symbolName, "$")[1]
|
||||
} else {
|
||||
procName = symbolName
|
||||
|
||||
switch procName {
|
||||
case "FreeLibrary", "LoadLibraryA", "GetProcAddress", "GetModuleHandleA", "GetModuleFileNameA":
|
||||
libName = "kernel32.dll"
|
||||
case "MessageBoxA":
|
||||
libName = "user32.dll"
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'O', 'u', 't', 'p', 'u', 't'}):
|
||||
return windows.NewCallback(lighthouse.GetCoffOutputForChannel(outChannel))
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'D', 'a', 't', 'a', 'P', 'a', 'r', 's', 'e'}):
|
||||
return windows.NewCallback(lighthouse.DataParse)
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'D', 'a', 't', 'a', 'I', 'n', 't'}):
|
||||
return windows.NewCallback(lighthouse.DataInt)
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'D', 'a', 't', 'a', 'S', 'h', 'o', 'r', 't'}):
|
||||
return windows.NewCallback(lighthouse.DataShort)
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'D', 'a', 't', 'a', 'L', 'e', 'n', 'g', 't', 'h'}):
|
||||
return windows.NewCallback(lighthouse.DataLength)
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'D', 'a', 't', 'a', 'E', 'x', 't', 'r', 'a', 'c', 't'}):
|
||||
return windows.NewCallback(lighthouse.DataExtract)
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'P', 'r', 'i', 'n', 't', 'f'}):
|
||||
return windows.NewCallback(lighthouse.GetCoffPrintfForChannel(outChannel))
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'A', 'd', 'd', 'V', 'a', 'l', 'u', 'e'}):
|
||||
return windows.NewCallback(lighthouse.AddValue)
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'G', 'e', 't', 'V', 'a', 'l', 'u', 'e'}):
|
||||
return windows.NewCallback(lighthouse.GetValue)
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'R', 'e', 'm', 'o', 'v', 'e', 'V', 'a', 'l', 'u', 'e'}):
|
||||
return windows.NewCallback(lighthouse.RemoveValue)
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'F', 'o', 'r', 'm', 'a', 't', 'A', 'l', 'l', 'o', 'c'}):
|
||||
fallthrough
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'F', 'o', 'r', 'm', 'a', 't', 'R', 'e', 's', 'e', 't'}):
|
||||
fallthrough
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'F', 'o', 'r', 'm', 'a', 't', 'F', 'r', 'e', 'e'}):
|
||||
fallthrough
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'F', 'o', 'r', 'm', 'a', 't', 'A', 'p', 'p', 'e', 'n', 'd'}):
|
||||
fallthrough
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'F', 'o', 'r', 'm', 'a', 't', 'P', 'r', 'i', 'n', 't', 'f'}):
|
||||
fallthrough
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'F', 'o', 'r', 'm', 'a', 't', 'T', 'o', 'S', 't', 'r', 'i', 'n', 'g'}):
|
||||
fallthrough
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'U', 's', 'e', 'T', 'o', 'k', 'e', 'n'}):
|
||||
fallthrough
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'R', 'e', 'v', 'e', 'r', 't', 'T', 'o', 'k', 'e', 'n'}):
|
||||
fallthrough
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'I', 's', 'A', 'd', 'm', 'i', 'n'}):
|
||||
fallthrough
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'G', 'e', 't', 'S', 'p', 'a', 'w', 'n', 'T', 'o'}):
|
||||
fallthrough
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'S', 'p', 'a', 'w', 'n', 'T', 'e', 'm', 'p', 'o', 'r', 'a', 'r', 'y', 'P', 'r', 'o', 'c', 'e', 's', 's'}):
|
||||
fallthrough
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'I', 'n', 'j', 'e', 'c', 't', 'P', 'r', 'o', 'c', 'e', 's', 's'}):
|
||||
fallthrough
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'I', 'n', 'j', 'e', 'c', 't', 'T', 'e', 'm', 'p', 'o', 'r', 'a', 'r', 'y', 'P', 'r', 'o', 'c', 'e', 's', 's'}):
|
||||
fallthrough
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'C', 'l', 'e', 'a', 'n', 'u', 'p', 'P', 'r', 'o', 'c', 'e', 's', 's'}):
|
||||
fallthrough
|
||||
case string([]rune{'t', 'o', 'W', 'i', 'd', 'e', 'C', 'h', 'a', 'r'}):
|
||||
fallthrough
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'G', 'e', 't', 'O', 'u', 't', 'p', 'u', 't', 'D', 'a', 't', 'a'}):
|
||||
fallthrough
|
||||
case string([]rune{'B', 'e', 'a', 'c', 'o', 'n', 'F', 'o', 'r', 'm', 'a', 't', 'I', 'n', 't'}):
|
||||
fallthrough
|
||||
default:
|
||||
// TODO: Check directives here for libraries
|
||||
fmt.Printf("Unknown symbol: %s\n", procName)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
libStringPtr, _ := syscall.LoadLibrary(libName)
|
||||
procAddress, _ := syscall.GetProcAddress(libStringPtr, procName)
|
||||
return procAddress
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func virtualAlloc(lpAddress uintptr, dwSize uintptr, flAllocationType uint32, flProtect uint32) (uintptr, error) {
|
||||
ret, _, err := procVirtualAlloc.Call(
|
||||
lpAddress,
|
||||
dwSize,
|
||||
uintptr(flAllocationType),
|
||||
uintptr(flProtect),
|
||||
)
|
||||
if ret == 0 {
|
||||
return 0, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func isSpecialSymbol(sym *pecoff.Symbol) bool {
|
||||
return sym.StorageClass == windef.IMAGE_SYM_CLASS_EXTERNAL && sym.SectionNumber == 0
|
||||
}
|
||||
|
||||
func isImportSymbol(sym *pecoff.Symbol) bool {
|
||||
return strings.HasPrefix(sym.NameString(), "__imp_")
|
||||
}
|
||||
|
||||
func processRelocation(symbolDefAddress uintptr, sectionAddress uintptr, reloc windef.Relocation, symbol *pecoff.Symbol) {
|
||||
symbolOffset := (uintptr)(reloc.VirtualAddress)
|
||||
|
||||
absoluteSymbolAddress := symbolOffset + sectionAddress
|
||||
|
||||
segmentValue := *(*uint32)(unsafe.Pointer(absoluteSymbolAddress))
|
||||
|
||||
if (symbol.StorageClass == windef.IMAGE_SYM_CLASS_STATIC && symbol.Value != 0) ||
|
||||
(symbol.StorageClass == windef.IMAGE_SYM_CLASS_EXTERNAL && symbol.SectionNumber != 0) {
|
||||
symbolOffset = (uintptr)(symbol.Value)
|
||||
} else {
|
||||
symbolDefAddress += (uintptr)(segmentValue)
|
||||
}
|
||||
|
||||
symbolRefAddress := sectionAddress
|
||||
|
||||
//TODO: Handle x86 cases as well
|
||||
switch reloc.Type {
|
||||
case windef.IMAGE_REL_AMD64_ADDR64:
|
||||
addr := (*uint64)(unsafe.Pointer(absoluteSymbolAddress))
|
||||
fmt.Sprintf("Symbol Ref Address: 0x%x\n", addr)
|
||||
*addr = uint64(symbolDefAddress)
|
||||
case windef.IMAGE_REL_AMD64_ADDR32NB:
|
||||
addr := (*uint32)(unsafe.Pointer(absoluteSymbolAddress))
|
||||
valueToWrite := symbolDefAddress - (symbolRefAddress + 4 + symbolOffset)
|
||||
fmt.Sprintf("Symbol Ref Address: 0x%x\n", addr)
|
||||
*addr = uint32(valueToWrite)
|
||||
case windef.IMAGE_REL_AMD64_REL32, windef.IMAGE_REL_AMD64_REL32_1, windef.IMAGE_REL_AMD64_REL32_2, windef.IMAGE_REL_AMD64_REL32_3, windef.IMAGE_REL_AMD64_REL32_4, windef.IMAGE_REL_AMD64_REL32_5:
|
||||
relativeSymbolDefAddress := symbolDefAddress - (uintptr)(reloc.Type-4) - (absoluteSymbolAddress + 4)
|
||||
addr := (*uint32)(unsafe.Pointer(absoluteSymbolAddress))
|
||||
fmt.Sprintf("Symbol Ref Address: 0x%x\n", addr)
|
||||
*addr = uint32(relativeSymbolDefAddress)
|
||||
default:
|
||||
fmt.Printf("Unsupported relocation type: %d\n", reloc.Type)
|
||||
}
|
||||
}
|
||||
|
||||
type CoffSection struct {
|
||||
Section *pecoff.Section
|
||||
Address uintptr
|
||||
}
|
||||
|
||||
func Load(coffBytes []byte, argBytes []byte) (string, error) {
|
||||
return LoadWithMethod(coffBytes, argBytes, "go")
|
||||
}
|
||||
|
||||
func LoadWithMethod(coffBytes []byte, argBytes []byte, method string) (string, error) {
|
||||
output := make(chan interface{})
|
||||
|
||||
parsedCoff := pecoff.Explore(binutil.WrapByteSlice(coffBytes))
|
||||
parsedCoff.ReadAll()
|
||||
parsedCoff.Seal()
|
||||
|
||||
sections := make(map[string]CoffSection, parsedCoff.Sections.Len())
|
||||
|
||||
gotBaseAddress := uintptr(0)
|
||||
gotOffset := 0
|
||||
gotSize := uint32(0)
|
||||
var gotMap = make(map[string]uintptr)
|
||||
|
||||
bssBaseAddress := uintptr(0)
|
||||
bssOffset := 0
|
||||
bssSize := uint32(0)
|
||||
|
||||
for _, symbol := range parsedCoff.Symbols {
|
||||
if isSpecialSymbol(symbol) {
|
||||
if isImportSymbol(symbol) {
|
||||
gotSize += 8
|
||||
} else {
|
||||
bssSize += symbol.Value + 8 //leave room for null bytes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, section := range parsedCoff.Sections.Array() {
|
||||
allocationSize := uintptr(section.SizeOfRawData)
|
||||
if strings.HasPrefix(section.NameString(), ".bss") {
|
||||
allocationSize = uintptr(bssSize)
|
||||
}
|
||||
|
||||
if allocationSize == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
addr, err := virtualAlloc(0, allocationSize, MEM_COMMIT|MEM_RESERVE|MEM_TOP_DOWN, PAGE_READWRITE)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("VirtualAlloc failed: %s", err.Error())
|
||||
}
|
||||
|
||||
if strings.HasPrefix(section.NameString(), ".bss") {
|
||||
bssBaseAddress = addr
|
||||
}
|
||||
|
||||
copy((*[1 << 30]byte)(unsafe.Pointer(addr))[:], section.RawData())
|
||||
|
||||
allocatedSection := CoffSection{
|
||||
Section: section,
|
||||
Address: addr,
|
||||
}
|
||||
|
||||
sections[section.NameString()] = allocatedSection
|
||||
}
|
||||
|
||||
gotBaseAddress, err := virtualAlloc(0, uintptr(gotSize), MEM_COMMIT|MEM_RESERVE|MEM_TOP_DOWN, PAGE_READWRITE)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("VirtualAlloc failed: %s", err.Error())
|
||||
}
|
||||
|
||||
for _, section := range parsedCoff.Sections.Array() {
|
||||
sectionVirtualAddr := sections[section.NameString()].Address
|
||||
fmt.Sprintf("Section: %s\n", section.NameString())
|
||||
|
||||
for _, reloc := range section.Relocations() {
|
||||
|
||||
symbol := parsedCoff.Symbols[reloc.SymbolTableIndex]
|
||||
|
||||
if symbol.StorageClass > 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
symbolTypeString := windef.MAP_IMAGE_SYM_CLASS[symbol.StorageClass]
|
||||
fmt.Sprintf("0x%08X %s %s\n", reloc.VirtualAddress, symbolTypeString, symbol.NameString())
|
||||
symbolDefAddress := uintptr(0)
|
||||
|
||||
if isSpecialSymbol(symbol) {
|
||||
if isImportSymbol(symbol) {
|
||||
externalAddress := resolveExternalAddress(symbol.NameString(), output)
|
||||
|
||||
if externalAddress == 0 {
|
||||
return "", fmt.Errorf("failed to resolve external address for symbol: %s", symbol.NameString())
|
||||
}
|
||||
|
||||
if existingGotAddress, exists := gotMap[symbol.NameString()]; exists {
|
||||
symbolDefAddress = existingGotAddress
|
||||
} else {
|
||||
symbolDefAddress = gotBaseAddress + uintptr(gotOffset*8)
|
||||
gotOffset += 1
|
||||
gotMap[symbol.NameString()] = symbolDefAddress
|
||||
}
|
||||
copy((*[8]byte)(unsafe.Pointer(symbolDefAddress))[:], (*[8]byte)(unsafe.Pointer(&externalAddress))[:])
|
||||
} else {
|
||||
symbolDefAddress = bssBaseAddress + uintptr(bssOffset)
|
||||
bssOffset += int(symbol.Value) + 8
|
||||
}
|
||||
} else {
|
||||
targetSection := parsedCoff.Sections.Array()[symbol.SectionNumber-1]
|
||||
symbolDefAddress = sections[targetSection.NameString()].Address + uintptr(symbol.Value)
|
||||
}
|
||||
|
||||
fmt.Sprintf("Symbol Def Address: 0x%x\n", symbolDefAddress)
|
||||
processRelocation(symbolDefAddress, sectionVirtualAddr, reloc, symbol)
|
||||
}
|
||||
|
||||
if section.Characteristics&IMAGE_SCN_MEM_EXECUTE != 0 {
|
||||
oldProtect := PAGE_READWRITE
|
||||
_, _, errVirtualProtect := procVirtualProtect.Call(sectionVirtualAddr, uintptr(section.SizeOfRawData), PAGE_EXECUTE_READ, uintptr(unsafe.Pointer(&oldProtect)))
|
||||
if errVirtualProtect != nil && errVirtualProtect.Error() != "The operation completed successfully." {
|
||||
return "", fmt.Errorf("Error calling VirtualProtect:\r\n%s", errVirtualProtect.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call the entry point
|
||||
go invokeMethod(method, argBytes, parsedCoff, sections, output)
|
||||
|
||||
bofOutput := ""
|
||||
for msg := range output {
|
||||
bofOutput += msg.(string) + "\n"
|
||||
}
|
||||
return bofOutput, nil
|
||||
|
||||
return "", fmt.Errorf("could not find default start methods of main or go")
|
||||
}
|
||||
|
||||
func invokeMethod(methodName string, argBytes []byte, parsedCoff *pecoff.File, sectionMap map[string]CoffSection, outChannel chan<- interface{}) {
|
||||
defer close(outChannel)
|
||||
|
||||
// Call the entry point
|
||||
for _, symbol := range parsedCoff.Symbols {
|
||||
if symbol.NameString() == methodName {
|
||||
mainSection := parsedCoff.Sections.Array()[symbol.SectionNumber-1]
|
||||
entryPoint := sectionMap[mainSection.NameString()].Address + uintptr(symbol.Value)
|
||||
|
||||
if len(argBytes) == 0 {
|
||||
argBytes = make([]byte, 1)
|
||||
}
|
||||
syscall.SyscallN(entryPoint, uintptr(unsafe.Pointer(&argBytes[0])), uintptr((len(argBytes))))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
Our Beacon* Function Compatibilty implementations. Code here is taken very liberally
|
||||
from Ne0nd0g's go-coff project at https://github.com/Ne0nd0g/go-coff.
|
||||
|
||||
Beacon function names are signatured to hell and back in yara land so this package is
|
||||
called "lighthouse" to avoid the presence of beacon/BOF strings in the generated binary.
|
||||
Function names have also been replaced/reduced along to avoid detection.
|
||||
*/
|
||||
|
||||
package lighthouse
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/praetorian-inc/goffloader/src/memory"
|
||||
"golang.org/x/sys/windows"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func GetCoffOutputForChannel(channel chan<- interface{}) func(int, uintptr, int) uintptr {
|
||||
return func(beaconType int, data uintptr, length int) uintptr {
|
||||
if length <= 0 {
|
||||
return 0
|
||||
}
|
||||
out := memory.ReadBytesFromPtr(data, uint32(length))
|
||||
|
||||
channel <- string(out)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func GetCoffPrintfForChannel(channel chan<- interface{}) func(int, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr) uintptr {
|
||||
return func(beaconType int, data uintptr, arg0 uintptr, arg1 uintptr, arg2 uintptr, arg3 uintptr, arg4 uintptr, arg5 uintptr, arg6 uintptr, arg7 uintptr, arg8 uintptr, arg9 uintptr) uintptr {
|
||||
var out string
|
||||
out = memory.ReadCStringFromPtr(data)
|
||||
numArgs := strings.Count(out, "%")
|
||||
args := []uintptr{arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9}
|
||||
|
||||
fString := ""
|
||||
argOffset := 0
|
||||
skipChar := false
|
||||
for i := range len(out) {
|
||||
c := out[i]
|
||||
|
||||
if skipChar {
|
||||
skipChar = false
|
||||
continue
|
||||
}
|
||||
|
||||
if argOffset > numArgs {
|
||||
fString += string(c)
|
||||
continue
|
||||
}
|
||||
|
||||
if c == '%' && i < len(out)-1 {
|
||||
d := out[i+1]
|
||||
switch d {
|
||||
case 's':
|
||||
s := memory.ReadCStringFromPtr(args[argOffset])
|
||||
// no way to tell if the string is unicode or ansi formatted, so assume if we read
|
||||
// more than 4 characters without a null byte that it's ANSI
|
||||
if len(s) < 5 {
|
||||
s = memory.ReadWStringFromPtr(args[argOffset])
|
||||
}
|
||||
fString += s
|
||||
case 'p':
|
||||
fString += fmt.Sprintf("%x", unsafe.Pointer(args[argOffset]))
|
||||
default:
|
||||
fString += fmt.Sprintf("%"+string(d), args[argOffset])
|
||||
}
|
||||
argOffset++
|
||||
skipChar = true
|
||||
} else {
|
||||
fString += string(c)
|
||||
}
|
||||
}
|
||||
|
||||
//fmt.Printf("%s\n", fString) //uncomment for debugging failed BOF/Executable runs
|
||||
channel <- fString
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
type DataParser struct {
|
||||
original uintptr
|
||||
buffer uintptr
|
||||
length uint32
|
||||
size uint32
|
||||
}
|
||||
|
||||
func DataExtract(datap *DataParser, size *uint32) uintptr {
|
||||
if datap.length <= 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
binaryLength := *(*uint32)(unsafe.Pointer(datap.buffer))
|
||||
datap.buffer += uintptr(4)
|
||||
datap.length -= 4
|
||||
if datap.length < binaryLength {
|
||||
return 0
|
||||
}
|
||||
|
||||
out := make([]byte, binaryLength)
|
||||
memory.CopyMemory(uintptr(unsafe.Pointer(&out[0])), datap.buffer, binaryLength)
|
||||
if uintptr(unsafe.Pointer(size)) != uintptr(0) && binaryLength != 0 {
|
||||
*size = binaryLength
|
||||
}
|
||||
|
||||
datap.buffer += uintptr(binaryLength)
|
||||
datap.length -= binaryLength
|
||||
return uintptr(unsafe.Pointer(&out[0]))
|
||||
}
|
||||
|
||||
func DataInt(datap *DataParser) uintptr {
|
||||
value := memory.ReadUIntFromPtr(datap.buffer)
|
||||
datap.buffer += uintptr(4)
|
||||
datap.length -= 4
|
||||
return uintptr(value)
|
||||
}
|
||||
|
||||
func DataLength(datap *DataParser) uintptr {
|
||||
return uintptr(datap.length)
|
||||
}
|
||||
|
||||
func DataParse(datap *DataParser, buff uintptr, size uint32) uintptr {
|
||||
if size <= 0 {
|
||||
return 0
|
||||
}
|
||||
datap.original = buff
|
||||
datap.buffer = buff + uintptr(4)
|
||||
datap.length = size - 4
|
||||
datap.size = size - 4
|
||||
return 1
|
||||
}
|
||||
|
||||
func DataShort(datap *DataParser) uintptr {
|
||||
if datap.length < 2 {
|
||||
return 0
|
||||
}
|
||||
|
||||
value := memory.ReadShortFromPtr(datap.buffer)
|
||||
datap.buffer += uintptr(2)
|
||||
datap.length -= 2
|
||||
return uintptr(value)
|
||||
}
|
||||
|
||||
var keyStore = make(map[string]uintptr, 0)
|
||||
|
||||
func AddValue(key uintptr, ptr uintptr) uintptr {
|
||||
sKey := memory.ReadCStringFromPtr(key)
|
||||
keyStore[sKey] = ptr
|
||||
return uintptr(1)
|
||||
}
|
||||
|
||||
func GetValue(key uintptr) uintptr {
|
||||
sKey := memory.ReadCStringFromPtr(key)
|
||||
if value, exists := keyStore[sKey]; exists {
|
||||
return value
|
||||
}
|
||||
return uintptr(0)
|
||||
}
|
||||
|
||||
func RemoveValue(key uintptr) uintptr {
|
||||
sKey := memory.ReadCStringFromPtr(key)
|
||||
if _, exists := keyStore[sKey]; exists {
|
||||
delete(keyStore, sKey)
|
||||
return uintptr(1)
|
||||
}
|
||||
return uintptr(0)
|
||||
}
|
||||
|
||||
func PackArgs(data []string) ([]byte, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var buff []byte
|
||||
for _, arg := range data {
|
||||
switch arg[0] {
|
||||
case 'b':
|
||||
data, err := PackBinary(arg[1:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Binary packing error:\n INPUT: '%s'\n ERROR:%s\n", arg[1:], err)
|
||||
}
|
||||
buff = append(buff, data...)
|
||||
case 'i':
|
||||
data, err := PackIntString(arg[1:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Int packing error:\n INPUT: '%s'\n ERROR:%s\n", arg[1:], err)
|
||||
}
|
||||
buff = append(buff, data...)
|
||||
case 's':
|
||||
data, err := PackShortString(arg[1:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Short packing error:\n INPUT: '%s'\n ERROR:%s\n", arg[1:], err)
|
||||
}
|
||||
buff = append(buff, data...)
|
||||
case 'z':
|
||||
var packedData []byte
|
||||
var err error
|
||||
// Handler for packing empty strings
|
||||
if len(arg) < 2 {
|
||||
packedData, _ = PackString("")
|
||||
} else {
|
||||
packedData, err = PackString(arg[1:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("String packing error:\n INPUT: '%s'\n ERROR:%s\n", arg[1:], err)
|
||||
}
|
||||
}
|
||||
buff = append(buff, packedData...)
|
||||
case 'Z':
|
||||
var packedData []byte
|
||||
var err error
|
||||
if len(arg) < 2 {
|
||||
packedData, _ = PackWideString("")
|
||||
} else {
|
||||
packedData, err = PackWideString(arg[1:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("WString packing error:\n INPUT: '%s'\n ERROR:%s\n", arg[1:], err)
|
||||
}
|
||||
}
|
||||
buff = append(buff, packedData...)
|
||||
default:
|
||||
return nil, fmt.Errorf("Data must be prefixed with 'b', 'i', 's','z', or 'Z'\n")
|
||||
}
|
||||
}
|
||||
rData := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(rData, uint32(len(buff)))
|
||||
rData = append(rData, buff...)
|
||||
return rData, nil
|
||||
}
|
||||
|
||||
func PackBinary(data string) ([]byte, error) {
|
||||
hexData, err := hex.DecodeString(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buff := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(buff, uint32(len(hexData)))
|
||||
buff = append(buff, hexData...)
|
||||
return buff, nil
|
||||
}
|
||||
|
||||
func PackInt(i uint32) ([]byte, error) {
|
||||
buff := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(buff, uint32(i))
|
||||
return buff, nil
|
||||
}
|
||||
|
||||
func PackIntString(s string) ([]byte, error) {
|
||||
i, err := strconv.ParseUint(s, 10, 32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return PackInt(uint32(i))
|
||||
}
|
||||
|
||||
func PackShort(i uint16) ([]byte, error) {
|
||||
buff := make([]byte, 2)
|
||||
binary.LittleEndian.PutUint16(buff, uint16(i))
|
||||
return buff, nil
|
||||
}
|
||||
|
||||
func PackShortString(s string) ([]byte, error) {
|
||||
i, err := strconv.ParseUint(s, 10, 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return PackShort(uint16(i))
|
||||
}
|
||||
|
||||
func PackString(s string) ([]byte, error) {
|
||||
d, err := windows.UTF16FromString(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buff := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(buff, uint32(len(d)))
|
||||
for _, c := range d {
|
||||
buff = append(buff, byte(c))
|
||||
}
|
||||
return buff, nil
|
||||
}
|
||||
|
||||
func convertToWindowsUnicode(s string) []byte {
|
||||
runes := []rune(s)
|
||||
utf16Encoded := utf16.Encode(runes)
|
||||
buf := make([]byte, len(utf16Encoded)*2)
|
||||
for i, utf16Char := range utf16Encoded {
|
||||
binary.LittleEndian.PutUint16(buf[i*2:], utf16Char)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func PackWideString(s string) ([]byte, error) {
|
||||
d := convertToWindowsUnicode(s)
|
||||
buff := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(buff, uint32(len(d)))
|
||||
buff = append(buff, d...)
|
||||
return buff, nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package memory
|
||||
|
||||
import "unsafe"
|
||||
|
||||
func CopyMemory(dst, src uintptr, length uint32) {
|
||||
copy((*[1 << 30]byte)(unsafe.Pointer(dst))[:length], (*[1 << 30]byte)(unsafe.Pointer(src))[:length])
|
||||
}
|
||||
|
||||
func ReadBytesFromPtr(src uintptr, length uint32) []byte {
|
||||
out := make([]byte, length)
|
||||
CopyMemory(uintptr(unsafe.Pointer(&out[0])), src, length)
|
||||
return out
|
||||
}
|
||||
|
||||
func ReadUIntFromPtr(src uintptr) uint32 {
|
||||
return *(*uint32)(unsafe.Pointer(src))
|
||||
}
|
||||
|
||||
func ReadShortFromPtr(src uintptr) uint16 {
|
||||
return *(*uint16)(unsafe.Pointer(src))
|
||||
}
|
||||
|
||||
func ReadCStringFromPtr(src uintptr) string {
|
||||
if src == 0 {
|
||||
return ""
|
||||
}
|
||||
str := ""
|
||||
offset := 0
|
||||
for {
|
||||
c := *(*byte)(unsafe.Pointer(src + uintptr(offset)))
|
||||
if c == 0 {
|
||||
break
|
||||
}
|
||||
str += string(c)
|
||||
offset++
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
func ReadWStringFromPtr(src uintptr) string {
|
||||
if src == 0 {
|
||||
return ""
|
||||
}
|
||||
str := ""
|
||||
offset := 0
|
||||
for {
|
||||
c1 := *(*byte)(unsafe.Pointer(src + uintptr(offset)))
|
||||
c2 := *(*byte)(unsafe.Pointer(src + uintptr(offset+1)))
|
||||
if c1 == 0 && c2 == 0 {
|
||||
break
|
||||
}
|
||||
str += string(c1) + string(c2)
|
||||
offset += 2
|
||||
}
|
||||
return str
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package pe
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
_ "embed"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/praetorian-inc/goffloader/src/coff"
|
||||
"github.com/praetorian-inc/goffloader/src/lighthouse"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func decompress(data []byte) ([]byte, error) {
|
||||
reader, err := gzip.NewReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer reader.Close()
|
||||
var buf bytes.Buffer
|
||||
_, err = io.Copy(&buf, reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
//go:embed static/NoConsolation.x64.o.gz
|
||||
var noConsolation []byte
|
||||
|
||||
// Fake the equivalent of running noconsolation
|
||||
func RunExecutable(executableBytes []byte, args []string) (string, error) {
|
||||
decompressedBytes, _ := decompress(noConsolation)
|
||||
|
||||
peName := "calc.exe"
|
||||
pePath := fmt.Sprintf("C:\\Windows\\System32\\%s", peName)
|
||||
|
||||
updatedArgs := append([]string{pePath}, args...)
|
||||
|
||||
argBytes, err := lighthouse.PackArgs([]string{
|
||||
"Z" + peName, // Unicode PE Name
|
||||
"z" + peName, // ANSI PE Name
|
||||
"Z" + pePath, // Unicode PE Path
|
||||
"b" + hex.EncodeToString(executableBytes), // The actual PE to load
|
||||
"z", // for local PE loading, we don't need it
|
||||
"i0", // not doing local loading
|
||||
"i60", // 60 second timeout
|
||||
"i0", // no headers
|
||||
"Z" + strings.Join(updatedArgs, " "), // Unicode Args
|
||||
"z" + strings.Join(updatedArgs, " "), // ANSI Args
|
||||
"z", // Invoke default entry point method
|
||||
"i0", // not using unicode
|
||||
"i0", // we don't want to disable output
|
||||
"i0", // we don't want to allocate a console
|
||||
"i0", // don't need to worry about closing handles
|
||||
"i0", // don't need to worry about freeing libraries
|
||||
"i1", // don't need to worry about saving
|
||||
"i0", // not listing PEs
|
||||
"z", // not unloading any PEs
|
||||
"z" + "chariot", // setting chariot as our nick() for now
|
||||
"z" + "0", // timestamp doesn't matter
|
||||
"i0", // linking to PEB
|
||||
"i0", // unloading is fine
|
||||
"i0", // do load all dependencies
|
||||
"z", // load_all_deps_but DLL_A,DLL_B,DLL_C...
|
||||
"z", // not using load_deps
|
||||
"z", // not using search_paths
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to pack arguments: %s\n", err.Error())
|
||||
}
|
||||
|
||||
return coff.Load(decompressedBytes, argBytes)
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user