From 86c796b1f37d9c43b1e415908a3e97443e4f8a51 Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Thu, 18 Mar 2021 08:55:38 -0400 Subject: [PATCH 01/31] Update SafeArray functions, fixed example program --- appdomain.go | 29 ++- examples/EXEfromMemory/EXEfromMemory.go | 42 +++- go-clr.go | 31 ++- hresult.go | 10 + methodinfo.go | 1 + safearray.go | 283 +++++++++++++++++++++--- utils.go | 11 +- variant.go | 22 +- 8 files changed, 372 insertions(+), 57 deletions(-) create mode 100644 hresult.go diff --git a/appdomain.go b/appdomain.go index bf17322..91cf304 100644 --- a/appdomain.go +++ b/appdomain.go @@ -3,6 +3,7 @@ package clr import ( + "fmt" "syscall" "unsafe" @@ -145,12 +146,30 @@ func (obj *AppDomain) GetHashCode() uintptr { return ret } -func (obj *AppDomain) Load_3(pRawAssembly uintptr, asmbly *uintptr) uintptr { - ret, _, _ := syscall.Syscall( +// Load_3 Loads an Assembly into this application domain. +// virtual HRESULT __stdcall Load_3 ( +// /*[in]*/ SAFEARRAY * rawAssembly, +// /*[out,retval]*/ struct _Assembly * * pRetVal ) = 0; +// https://docs.microsoft.com/en-us/dotnet/api/system.appdomain.load?view=net-5.0 +func (obj *AppDomain) Load_3(rawAssembly *SafeArray) (assembly *Assembly, err error) { + debugPrint("Entering into appdomain.Load_3()...") + hr, _, err := syscall.Syscall( obj.vtbl.Load_3, 3, uintptr(unsafe.Pointer(obj)), - uintptr(unsafe.Pointer(pRawAssembly)), - uintptr(unsafe.Pointer(asmbly))) - return ret + uintptr(unsafe.Pointer(rawAssembly)), + uintptr(unsafe.Pointer(&assembly)), + ) + + if err != syscall.Errno(0) { + return + } + + if hr != S_OK { + err = fmt.Errorf("the appdomain.Load_3 function returned a non-zero HRESULT: 0x%x", hr) + return + } + err = nil + + return } diff --git a/examples/EXEfromMemory/EXEfromMemory.go b/examples/EXEfromMemory/EXEfromMemory.go index d4747d1..e16abba 100644 --- a/examples/EXEfromMemory/EXEfromMemory.go +++ b/examples/EXEfromMemory/EXEfromMemory.go @@ -8,6 +8,7 @@ import ( "log" "os" "runtime" + "strings" "syscall" "unsafe" @@ -27,8 +28,8 @@ func checkOK(hr uintptr, caller string) { } func init() { - if len(os.Args) != 2 { - fmt.Println("Usage: EXEfromMemory.exe ") + if len(os.Args) < 2 { + fmt.Println("Usage: EXEfromMemory.exe ") os.Exit(1) } } @@ -39,6 +40,11 @@ func main() { must(err) runtime.KeepAlive(exebytes) + var params []string + if len(os.Args) > 2 { + params = os.Args[2:] + } + var pMetaHost uintptr hr := clr.CLRCreateInstance(&clr.CLSID_CLRMetaHost, &clr.IID_ICLRMetaHost, &pMetaHost) checkOK(hr, "CLRCreateInstance") @@ -83,29 +89,43 @@ func main() { safeArray, err := clr.CreateSafeArray(exebytes) must(err) runtime.KeepAlive(safeArray) - fmt.Println("[+] Crated SafeArray from byte array") + fmt.Println("[+] Created SafeArray from byte array") - var pAssembly uintptr - hr = appDomain.Load_3(uintptr(unsafe.Pointer(&safeArray)), &pAssembly) - checkOK(hr, "appDomain.Load_3") - assembly := clr.NewAssemblyFromPtr(pAssembly) - fmt.Printf("[+] Executable loaded into memory at 0x%08x\n", pAssembly) + assembly, err := appDomain.Load_3(safeArray) + must(err) + fmt.Printf("[+] Executable loaded into memory at %p\n", assembly) var pEntryPointInfo uintptr hr = assembly.GetEntryPoint(&pEntryPointInfo) checkOK(hr, "assembly.GetEntryPoint") - fmt.Printf("[+] Executable entrypoint found at 0x%08x. Calling...\n", pEntryPointInfo) - fmt.Println("-------") + fmt.Printf("[+] Executable entrypoint found at 0x%x\n", pEntryPointInfo) methodInfo := clr.NewMethodInfoFromPtr(pEntryPointInfo) + var methodSignaturePtr, paramPtr uintptr + err = methodInfo.GetString(&methodSignaturePtr) + if err != nil { + return + } + methodSignature := clr.ReadUnicodeStr(unsafe.Pointer(methodSignaturePtr)) + fmt.Printf("[+] Checking if the assembly requires arguments\n") + if !strings.Contains(methodSignature, "Void Main()") { + if len(params) < 1 { + log.Fatal("the assembly requires arguments but none were provided\nUsage: EXEfromMemory.exe ") + } + if paramPtr, err = clr.PrepareParameters(params); err != nil { + log.Fatal(fmt.Sprintf("there was an error preparing the assembly arguments:\r\n%s", err)) + } + } + var pRetCode uintptr nullVariant := clr.Variant{ VT: 1, Val: uintptr(0), } + fmt.Println("[+] Invoking...") hr = methodInfo.Invoke_3( nullVariant, - uintptr(0), + paramPtr, &pRetCode) fmt.Println("-------") diff --git a/go-clr.go b/go-clr.go index 3c4af60..6328c21 100644 --- a/go-clr.go +++ b/go-clr.go @@ -159,13 +159,12 @@ func ExecuteByteArray(targetRuntime string, rawBytes []byte, params []string) (r if err != nil { return } - var pAssembly uintptr - hr = appDomain.Load_3(uintptr(safeArrayPtr), &pAssembly) - err = checkOK(hr, "appDomain.Load_3") + + assembly, err := appDomain.Load_3(safeArrayPtr) if err != nil { return } - assembly := NewAssemblyFromPtr(pAssembly) + var pEntryPointInfo uintptr hr = assembly.GetEntryPoint(&pEntryPointInfo) err = checkOK(hr, "assembly.GetEntryPoint") @@ -179,7 +178,7 @@ func ExecuteByteArray(targetRuntime string, rawBytes []byte, params []string) (r if err != nil { return } - methodSignature := readUnicodeStr(unsafe.Pointer(methodSignaturePtr)) + methodSignature := ReadUnicodeStr(unsafe.Pointer(methodSignaturePtr)) if expectsParams(methodSignature) { if paramPtr, err = PrepareParameters(params); err != nil { @@ -211,27 +210,35 @@ func ExecuteByteArray(targetRuntime string, rawBytes []byte, params []string) (r // PrepareParameters creates a safe array of strings (arguments) nested inside a Variant object, which is itself // appended to the final safe array func PrepareParameters(params []string) (uintptr, error) { - listStrSafeArrayPtr, err := CreateEmptySafeArray(0x0008, len(params)) // VT_BSTR + sab := SafeArrayBound{ + cElements: uint32(len(params)), + lLbound: 0, + } + listStrSafeArrayPtr, err := SafeArrayCreate(VT_BSTR, 1, &sab) // VT_BSTR if err != nil { return 0, err } for i, p := range params { bstr, _ := SysAllocString(p) - SafeArrayPutElement(listStrSafeArrayPtr, bstr, i) + SafeArrayPutElement(listStrSafeArrayPtr, int32(i), bstr) } paramVariant := Variant{ - VT: 0x0008 | 0x2000, // VT_BSTR | VT_ARRAY - Val: uintptr(listStrSafeArrayPtr), + VT: VT_BSTR | VT_ARRAY, // VT_BSTR | VT_ARRAY + Val: uintptr(unsafe.Pointer(listStrSafeArrayPtr)), } - paramsSafeArrayPtr, err := CreateEmptySafeArray(0x000C, 1) // VT_VARIANT + sab2 := SafeArrayBound{ + cElements: uint32(1), + lLbound: 0, + } + paramsSafeArrayPtr, err := SafeArrayCreate(VT_VARIANT, 1, &sab2) // VT_VARIANT if err != nil { return 0, err } - err = SafeArrayPutElement(paramsSafeArrayPtr, unsafe.Pointer(¶mVariant), 0) + err = SafeArrayPutElement(paramsSafeArrayPtr, int32(0), unsafe.Pointer(¶mVariant)) if err != nil { return 0, err } - return uintptr(paramsSafeArrayPtr), nil + return uintptr(unsafe.Pointer(paramsSafeArrayPtr)), nil } diff --git a/hresult.go b/hresult.go new file mode 100644 index 0000000..f5342ab --- /dev/null +++ b/hresult.go @@ -0,0 +1,10 @@ +package clr + +const ( + // COR_E_SAFEARRAYRANKMISMATCH is SafeArrayRankMismatchException + COR_E_SAFEARRAYRANKMISMATCH uint32 = 0x80131538 + // COR_E_BADIMAGEFORMAT is BadImageFormatException + COR_E_BADIMAGEFORMAT uint32 = 0x8007000b + // DISP_E_BADPARAMCOUNT is invalid number of parameters + DISP_E_BADPARAMCOUNT uint32 = 0x8002000e +) diff --git a/methodinfo.go b/methodinfo.go index 3ec3164..4f6fdf5 100644 --- a/methodinfo.go +++ b/methodinfo.go @@ -104,6 +104,7 @@ func (obj *MethodInfo) GetType(pRetVal *uintptr) uintptr { } func (obj *MethodInfo) Invoke_3(variantObj Variant, parameters uintptr, pRetVal *uintptr) uintptr { + debugPrint("Entering into appdomain.Invoke_3()...") ret, _, _ := syscall.Syscall6( obj.vtbl.Invoke_3, 4, diff --git a/safearray.go b/safearray.go index 705f1fd..d5a6dd5 100644 --- a/safearray.go +++ b/safearray.go @@ -3,59 +3,94 @@ package clr import ( + "fmt" "syscall" "unsafe" ) -// VARTYPE uint16 -// UINT uint32 -// VT_UI1 = 0x0011 -// ULONG uint32 -// LONG int32 -// USHORT uint16 - -// from OAld.h - +// SafeArray represents a safe array +// defined in OAIdl.h +// typedef struct tagSAFEARRAY { +// USHORT cDims; +// USHORT fFeatures; +// ULONG cbElements; +// ULONG cLocks; +// PVOID pvData; +// SAFEARRAYBOUND rgsabound[1]; +// } SAFEARRAY; +// https://docs.microsoft.com/en-us/windows/win32/api/oaidl/ns-oaidl-safearray +// https://docs.microsoft.com/en-us/archive/msdn-magazine/2017/march/introducing-the-safearray-data-structure type SafeArray struct { - cDims uint16 - fFeatures uint16 + // cDims is the number of dimensions + cDims uint16 + // fFeatures is the feature flags + fFeatures uint16 + // cbElements is the size of an array element cbElements uint32 - cLocks uint32 - pvData uintptr - rgsabound [1]SafeArrayBound + // cLocks is the number of times the array has been locked without a corresponding unlock + cLocks uint32 + // pvData is the data + pvData uintptr + // rgsabout is one bound for each dimension + rgsabound [1]SafeArrayBound } +// SafeArrayBound represents the bounds of one dimension of the array +// typedef struct tagSAFEARRAYBOUND { +// ULONG cElements; +// LONG lLbound; +// } SAFEARRAYBOUND, *LPSAFEARRAYBOUND; +// https://docs.microsoft.com/en-us/windows/win32/api/oaidl/ns-oaidl-safearraybound type SafeArrayBound struct { + // cElements is the number of elements in the dimension cElements uint32 - lLbound int32 + // lLbound is the lowerbound of the dimension + lLbound int32 } // CreateSafeArray is a wrapper function that takes in a Go byte array and creates a SafeArray containing unsigned bytes // by making two syscalls and copying raw memory into the correct spot. -func CreateSafeArray(rawBytes []byte) (unsafe.Pointer, error) { +func CreateSafeArray(rawBytes []byte) (*SafeArray, error) { + debugPrint("Entering into safearray.CreateSafeArray()...") + + safeArrayBounds := SafeArrayBound{ + cElements: uint32(len(rawBytes)), + lLbound: int32(0), + } - saPtr, err := CreateEmptySafeArray(0x11, len(rawBytes)) // VT_UI1 + safeArray, err := SafeArrayCreate(VT_UI1, 1, &safeArrayBounds) if err != nil { return nil, err } // now we need to use RtlCopyMemory to copy our bytes to the SafeArray modNtDll := syscall.MustLoadDLL("ntdll.dll") procRtlCopyMemory := modNtDll.MustFindProc("RtlCopyMemory") - sa := (*SafeArray)(saPtr) + + // TODO Replace RtlCopyMemory with SafeArrayPutElement or SafeArrayAccessData + + // void RtlCopyMemory( + // void* Destination, + // const void* Source, + // size_t Length + // ); + // https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-rtlcopymemory _, _, err = procRtlCopyMemory.Call( - sa.pvData, + safeArray.pvData, uintptr(unsafe.Pointer(&rawBytes[0])), - uintptr(len(rawBytes))) + uintptr(len(rawBytes)), + ) + if err != syscall.Errno(0) { return nil, err } - return saPtr, nil + return safeArray, nil } // CreateEmptySafeArray is a wrapper function that takes an array type and a size and creates a safe array with corresponding // properties. It returns a pointer to that empty array. -func CreateEmptySafeArray(arrayType int, size int) (unsafe.Pointer, error) { +func CreateEmptySafeArray(arrayType uint16, size int) (unsafe.Pointer, error) { + debugPrint("Entering into safearray.CreateEmptySafeArray()...") modOleAuto := syscall.MustLoadDLL("OleAut32.dll") procSafeArrayCreate := modOleAuto.MustFindProc("SafeArrayCreate") @@ -74,35 +109,229 @@ func CreateEmptySafeArray(arrayType int, size int) (unsafe.Pointer, error) { } return unsafe.Pointer(ret), nil +} + +// SafeArrayCreate creates a new array descriptor, allocates and initializes the data for the array, and returns a pointer to the new array descriptor. +// SAFEARRAY * SafeArrayCreate( +// VARTYPE vt, +// UINT cDims, +// SAFEARRAYBOUND *rgsabound +// ); +// Varient types: https://docs.microsoft.com/en-us/windows/win32/api/wtypes/ne-wtypes-varenum +// https://docs.microsoft.com/en-us/windows/win32/api/oleauto/nf-oleauto-safearraycreate +func SafeArrayCreate(vt uint16, cDims uint32, rgsabound *SafeArrayBound) (safeArray *SafeArray, err error) { + debugPrint("Entering into safearray.SafeArrayCreate()...") + + modOleAuto := syscall.MustLoadDLL("OleAut32.dll") + procSafeArrayCreate := modOleAuto.MustFindProc("SafeArrayCreate") + + ret, _, err := procSafeArrayCreate.Call( + uintptr(vt), + uintptr(cDims), + uintptr(unsafe.Pointer(rgsabound)), + ) + + if err != syscall.Errno(0) { + return + } + err = nil + + if ret == 0 { + err = fmt.Errorf("the OleAut32!SafeArrayCreate function return 0x%x and the SafeArray was not created", ret) + return + } + // Unable to avoid misuse of unsafe.Pointer because the Windows API call returns the safeArray pointer in the "ret" value. This is a go vet false positive + safeArray = (*SafeArray)(unsafe.Pointer(ret)) + return } // SysAllocString converts a Go string to a BTSR string, that is a unicode string prefixed with its length. +// Allocates a new string and copies the passed string into it. // It returns a pointer to the string's content. +// BSTR SysAllocString( +// const OLECHAR *psz +// ); +// https://docs.microsoft.com/en-us/windows/win32/api/oleauto/nf-oleauto-sysallocstring func SysAllocString(str string) (unsafe.Pointer, error) { + debugPrint("Entering into safearray.SysAllocString()...") + modOleAuto := syscall.MustLoadDLL("OleAut32.dll") sysAllocString := modOleAuto.MustFindProc("SysAllocString") + input := utf16Le(str) ret, _, err := sysAllocString.Call( uintptr(unsafe.Pointer(&input[0])), ) + if err != syscall.Errno(0) { return nil, err } + // TODO Return a pointer to a BSTR instead of an unsafe.Pointer return unsafe.Pointer(ret), nil } // SafeArrayPutElement pushes an element to the safe array at a given index -func SafeArrayPutElement(array, btsr unsafe.Pointer, index int) (err error) { +// HRESULT SafeArrayPutElement( +// SAFEARRAY *psa, +// LONG *rgIndices, +// void *pv +// ); +// https://docs.microsoft.com/en-us/windows/win32/api/oleauto/nf-oleauto-safearrayputelement +func SafeArrayPutElement(psa *SafeArray, rgIndices int32, pv unsafe.Pointer) error { + debugPrint("Entering into safearray.SafeArrayPutElement()...") + modOleAuto := syscall.MustLoadDLL("OleAut32.dll") safeArrayPutElement := modOleAuto.MustFindProc("SafeArrayPutElement") - _, _, err = safeArrayPutElement.Call( - uintptr(array), - uintptr(unsafe.Pointer(&index)), - uintptr(btsr), + + hr, _, err := safeArrayPutElement.Call( + uintptr(unsafe.Pointer(psa)), + uintptr(unsafe.Pointer(&rgIndices)), + uintptr(pv), ) if err != syscall.Errno(0) { return err } + if hr != S_OK { + return fmt.Errorf("the OleAut32!SafeArrayPutElement call returned a non-zero HRESULT: 0x%x", hr) + } return nil } + +// SafeArrayLock increments the lock count of an array, and places a pointer to the array data in pvData of the array descriptor +// HRESULT SafeArrayLock( +// SAFEARRAY *psa +// ); +// https://docs.microsoft.com/en-us/windows/win32/api/oleauto/nf-oleauto-safearraylock +func SafeArrayLock(psa *SafeArray) error { + debugPrint("Entering into safearray.SafeArrayLock()...") + + modOleAuto := syscall.MustLoadDLL("OleAut32.dll") + safeArrayCreate := modOleAuto.MustFindProc("SafeArrayCreate") + + hr, _, err := safeArrayCreate.Call(uintptr(unsafe.Pointer(psa))) + + if err != syscall.Errno(0) { + return err + } + + if hr != S_OK { + return fmt.Errorf("the OleAut32!SafeArrayCreate function returned a non-zero HRESULT: 0x%x", hr) + } + + return nil +} + +// SafeArrayGetVartype gets the VARTYPE stored in the specified safe array +// HRESULT SafeArrayGetVartype( +// SAFEARRAY *psa, +// VARTYPE *pvt +// ); +// https://docs.microsoft.com/en-us/windows/win32/api/oleauto/nf-oleauto-safearraygetvartype +func SafeArrayGetVartype(psa *SafeArray) (uint16, error) { + debugPrint("Entering into safearray.SafeArrayGetVartype()...") + + var vt uint16 + + modOleAuto := syscall.MustLoadDLL("OleAut32.dll") + safeArrayGetVartype := modOleAuto.MustFindProc("SafeArrayGetVartype") + + hr, _, err := safeArrayGetVartype.Call( + uintptr(unsafe.Pointer(psa)), + uintptr(unsafe.Pointer(&vt)), + ) + + if err != syscall.Errno(0) { + return 0, err + } + if hr != S_OK { + return 0, fmt.Errorf("the OleAut32!SafeArrayGetVartype function returned a non-zero HRESULT: 0x%x", hr) + } + return vt, nil +} + +// SafeArrayAccessData increments the lock count of an array, and retrieves a pointer to the array data +// HRESULT SafeArrayAccessData( +// SAFEARRAY *psa, +// void HUGEP **ppvData +// ); +// https://docs.microsoft.com/en-us/windows/win32/api/oleauto/nf-oleauto-safearrayaccessdata +func SafeArrayAccessData(psa *SafeArray) (*uintptr, error) { + debugPrint("Entering into safearray.SafeArrayAccessData()...") + + var ppvData *uintptr + + modOleAuto := syscall.MustLoadDLL("OleAut32.dll") + safeArrayAccessData := modOleAuto.MustFindProc("SafeArrayAccessData") + + hr, _, err := safeArrayAccessData.Call( + uintptr(unsafe.Pointer(psa)), + uintptr(unsafe.Pointer(&ppvData)), + ) + + if err != syscall.Errno(0) { + return nil, err + } + if hr != S_OK { + return nil, fmt.Errorf("the oleaut32!SafeArrayAccessData function returned a non-zero HRESULT: 0x%x", hr) + } + return ppvData, nil +} + +// SafeArrayGetLBound gets the lower bound for any dimension of the specified safe array +// HRESULT SafeArrayGetLBound( +// SAFEARRAY *psa, +// UINT nDim, +// LONG *plLbound +// ); +// https://docs.microsoft.com/en-us/windows/win32/api/oleauto/nf-oleauto-safearraygetlbound +func SafeArrayGetLBound(psa *SafeArray, nDim uint32) (uint32, error) { + debugPrint("Entering into safearray.SafeArrayGetLBound()...") + var plLbound uint32 + modOleAuto := syscall.MustLoadDLL("OleAut32.dll") + safeArrayGetLBound := modOleAuto.MustFindProc("SafeArrayGetLBound") + + hr, _, err := safeArrayGetLBound.Call( + uintptr(unsafe.Pointer(psa)), + uintptr(nDim), + uintptr(unsafe.Pointer(&plLbound)), + ) + + if err != syscall.Errno(0) { + return 0, err + } + if hr != S_OK { + return 0, fmt.Errorf("the oleaut32!SafeArrayGetLBound function returned a non-zero HRESULT: 0x%x", hr) + } + return plLbound, nil +} + +// SafeArrayGetUBound gets the upper bound for any dimension of the specified safe array +// HRESULT SafeArrayGetUBound( +// SAFEARRAY *psa, +// UINT nDim, +// LONG *plUbound +// ); +// https://docs.microsoft.com/en-us/windows/win32/api/oleauto/nf-oleauto-safearraygetubound +func SafeArrayGetUBound(psa *SafeArray, nDim uint32) (uint32, error) { + debugPrint("Entering into safearray.SafeArrayGetUBound()...") + + var plUbound uint32 + + modOleAuto := syscall.MustLoadDLL("OleAut32.dll") + safeArrayGetUBound := modOleAuto.MustFindProc("SafeArrayGetUBound") + + hr, _, err := safeArrayGetUBound.Call( + uintptr(unsafe.Pointer(psa)), + uintptr(nDim), + uintptr(unsafe.Pointer(&plUbound)), + ) + + if err != syscall.Errno(0) { + return 0, err + } + if hr != S_OK { + return 0, fmt.Errorf("the oleaut32!SafeArrayGetUBound function returned a non-zero HRESULT: 0x%x", hr) + } + return plUbound, nil +} diff --git a/utils.go b/utils.go index 42f1389..18c815c 100644 --- a/utils.go +++ b/utils.go @@ -16,6 +16,8 @@ import ( const S_OK = 0x0 +var Debug = false + func checkOK(hr uintptr, caller string) error { if hr != S_OK { return fmt.Errorf("%s returned 0x%08x", caller, hr) @@ -42,7 +44,7 @@ func expectsParams(input string) bool { return !strings.Contains(input, "Void Main()") } -func readUnicodeStr(ptr unsafe.Pointer) string { +func ReadUnicodeStr(ptr unsafe.Pointer) string { var byteVal uint16 out := make([]uint16, 0) for i := 0; ; i++ { @@ -55,3 +57,10 @@ func readUnicodeStr(ptr unsafe.Pointer) string { } return string(utf16.Decode(out)) } + +// debugPrint is used to print messages only when debug has been enabled +func debugPrint(message string) { + if Debug { + fmt.Println("[DEBUG] " + message) + } +} diff --git a/variant.go b/variant.go index 85a3005..f8b1626 100644 --- a/variant.go +++ b/variant.go @@ -4,8 +4,28 @@ package clr import "unsafe" -// from https://github.com/go-ole/go-ole/blob/master/variant_amd64.go +const ( + VT_EMPTY uint16 = 0x0000 + VT_NULL uint16 = 0x0001 + // VT_UI1 is a Variant Type of Unsigned Integer of 1-byte + VT_UI1 uint16 = 0x0011 + // VT_UT4 is a Varriant Type of Unsigned Integer of 4-byte + VT_UI4 uint16 = 0x0013 + // VT_BSTR is a Variant Type of BSTR, an OLE automation type for transfering length-prefixed strings + // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-oaut/9c5a5ce4-ff5b-45ce-b915-ada381b34ac1 + VT_BSTR uint16 = 0x0008 + // VT_VARIANT is a Variant Type of VARIANT, a container for a union that can hold many types of data + VT_VARIANT uint16 = 0x000c + // VT_ARRAY is a Variant Type of a SAFEARRAY + // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-oaut/2e87a537-9305-41c6-a88b-b79809b3703a + VT_ARRAY uint16 = 0x2000 +) +// from https://github.com/go-ole/go-ole/blob/master/variant_amd64.go +// https://docs.microsoft.com/en-us/windows/win32/winauto/variant-structure +// https://docs.microsoft.com/en-us/windows/win32/api/oaidl/ns-oaidl-variant +// https://docs.microsoft.com/en-us/previous-versions/windows/embedded/ms891678(v=msdn.10)?redirectedfrom=MSDN +// VARIANT Type Constants https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-oaut/3fe7db9f-5803-4dc4-9d14-5425d3f5461f type Variant struct { VT uint16 // VARTYPE wReserved1 uint16 From 46d27d562aeabd930d51c93cdfc96986a8e7fcbd Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Thu, 18 Mar 2021 09:03:51 -0400 Subject: [PATCH 02/31] Fixed composite literal use of unkeyed fields --- guids.go | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/guids.go b/guids.go index 4bdef69..0b65cec 100644 --- a/guids.go +++ b/guids.go @@ -7,15 +7,12 @@ import ( ) var ( - CLSID_CLRMetaHost = windows.GUID{0x9280188d, 0xe8e, 0x4867, [8]byte{0xb3, 0xc, 0x7f, 0xa8, 0x38, 0x84, 0xe8, 0xde}} - IID_ICLRMetaHost = windows.GUID{0xD332DB9E, 0xB9B3, 0x4125, [8]byte{0x82, 0x07, 0xA1, 0x48, 0x84, 0xF5, 0x32, 0x16}} - IID_ICLRRuntimeInfo = windows.GUID{0xBD39D1D2, 0xBA2F, 0x486a, [8]byte{0x89, 0xB0, 0xB4, 0xB0, 0xCB, 0x46, 0x68, 0x91}} - - CLSID_CLRRuntimeHost = windows.GUID{0x90F1A06E, 0x7712, 0x4762, [8]byte{0x86, 0xB5, 0x7A, 0x5E, 0xBA, 0x6B, 0xDB, 0x02}} - IID_ICLRRuntimeHost = windows.GUID{0x90F1A06C, 0x7712, 0x4762, [8]byte{0x86, 0xB5, 0x7A, 0x5E, 0xBA, 0x6B, 0xDB, 0x02}} - - IID_ICorRuntimeHost = windows.GUID{0xcb2f6722, 0xab3a, 0x11d2, [8]byte{0x9c, 0x40, 0x00, 0xc0, 0x4f, 0xa3, 0x0a, 0x3e}} - CLSID_CorRuntimeHost = windows.GUID{0xcb2f6723, 0xab3a, 0x11d2, [8]byte{0x9c, 0x40, 0x00, 0xc0, 0x4f, 0xa3, 0x0a, 0x3e}} - - IID_AppDomain = windows.GUID{0x5f696dc, 0x2b29, 0x3663, [8]uint8{0xad, 0x8b, 0xc4, 0x38, 0x9c, 0xf2, 0xa7, 0x13}} + CLSID_CLRMetaHost = windows.GUID{Data1: 0x9280188d, Data2: 0x0e8e, Data3: 0x4867, Data4: [8]byte{0xb3, 0x0c, 0x7f, 0xa8, 0x38, 0x84, 0xe8, 0xde}} + IID_ICLRMetaHost = windows.GUID{Data1: 0xD332DB9E, Data2: 0xB9B3, Data3: 0x4125, Data4: [8]byte{0x82, 0x07, 0xA1, 0x48, 0x84, 0xF5, 0x32, 0x16}} + IID_ICLRRuntimeInfo = windows.GUID{Data1: 0xBD39D1D2, Data2: 0xBA2F, Data3: 0x486a, Data4: [8]byte{0x89, 0xB0, 0xB4, 0xB0, 0xCB, 0x46, 0x68, 0x91}} + CLSID_CLRRuntimeHost = windows.GUID{Data1: 0x90F1A06E, Data2: 0x7712, Data3: 0x4762, Data4: [8]byte{0x86, 0xB5, 0x7A, 0x5E, 0xBA, 0x6B, 0xDB, 0x02}} + IID_ICLRRuntimeHost = windows.GUID{Data1: 0x90F1A06C, Data2: 0x7712, Data3: 0x4762, Data4: [8]byte{0x86, 0xB5, 0x7A, 0x5E, 0xBA, 0x6B, 0xDB, 0x02}} + IID_ICorRuntimeHost = windows.GUID{Data1: 0xcb2f6722, Data2: 0xab3a, Data3: 0x11d2, Data4: [8]byte{0x9c, 0x40, 0x00, 0xc0, 0x4f, 0xa3, 0x0a, 0x3e}} + CLSID_CorRuntimeHost = windows.GUID{Data1: 0xcb2f6723, Data2: 0xab3a, Data3: 0x11d2, Data4: [8]byte{0x9c, 0x40, 0x00, 0xc0, 0x4f, 0xa3, 0x0a, 0x3e}} + IID_AppDomain = windows.GUID{Data1: 0x05f696dc, Data2: 0x2b29, Data3: 0x3663, Data4: [8]byte{0xad, 0x8b, 0xc4, 0x38, 0x9c, 0xf2, 0xa7, 0x13}} ) From d54978b3d76b2588aac2ac83db8e35e4065df504 Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Thu, 18 Mar 2021 10:55:11 -0400 Subject: [PATCH 03/31] Fixed misuse of unsafe.Pointer in iclrmetahost --- examples/EXEfromMemory/EXEfromMemory.go | 11 ++--- iclrmetahost.go | 56 +++++++++++++++---------- 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/examples/EXEfromMemory/EXEfromMemory.go b/examples/EXEfromMemory/EXEfromMemory.go index e16abba..6cb0a84 100644 --- a/examples/EXEfromMemory/EXEfromMemory.go +++ b/examples/EXEfromMemory/EXEfromMemory.go @@ -45,15 +45,13 @@ func main() { params = os.Args[2:] } - var pMetaHost uintptr - hr := clr.CLRCreateInstance(&clr.CLSID_CLRMetaHost, &clr.IID_ICLRMetaHost, &pMetaHost) - checkOK(hr, "CLRCreateInstance") - metaHost := clr.NewICLRMetaHostFromPtr(pMetaHost) + metaHost, err := clr.CLRCreateInstance(clr.CLSID_CLRMetaHost, clr.IID_ICLRMetaHost) + must(err) versionString := "v4.0.30319" pwzVersion, _ := syscall.UTF16PtrFromString(versionString) var pRuntimeInfo uintptr - hr = metaHost.GetRuntime(pwzVersion, &clr.IID_ICLRRuntimeInfo, &pRuntimeInfo) + hr := metaHost.GetRuntime(pwzVersion, &clr.IID_ICLRRuntimeInfo, &pRuntimeInfo) checkOK(hr, "metahost.GetRuntime") runtimeInfo := clr.NewICLRRuntimeInfoFromPtr(pRuntimeInfo) @@ -84,8 +82,6 @@ func main() { appDomain := clr.NewAppDomainFromPtr(pAppDomain) fmt.Println("[+] Got default AppDomain") - fmt.Printf("[+] Loaded %d bytes into memory from %s\n", len(exebytes), filename) - safeArray, err := clr.CreateSafeArray(exebytes) must(err) runtime.KeepAlive(safeArray) @@ -93,6 +89,7 @@ func main() { assembly, err := appDomain.Load_3(safeArray) must(err) + fmt.Printf("[+] Loaded %d bytes into memory from %s\n", len(exebytes), filename) fmt.Printf("[+] Executable loaded into memory at %p\n", assembly) var pEntryPointInfo uintptr diff --git a/iclrmetahost.go b/iclrmetahost.go index a4f1c52..9d39312 100644 --- a/iclrmetahost.go +++ b/iclrmetahost.go @@ -3,27 +3,13 @@ package clr import ( + "fmt" "syscall" "unsafe" "golang.org/x/sys/windows" ) -var ( - modMSCoree = syscall.NewLazyDLL("mscoree.dll") - procCLRCreateInstance = modMSCoree.NewProc("CLRCreateInstance") -) - -// Wrapper for the mscorree.dll CLRCreateInstance syscall -func CLRCreateInstance(clsid, riid *windows.GUID, ppInterface *uintptr) uintptr { - ret, _, _ := procCLRCreateInstance.Call( - uintptr(unsafe.Pointer(clsid)), - uintptr(unsafe.Pointer(riid)), - uintptr(unsafe.Pointer(ppInterface))) - return ret -} - - // Couldnt have done any of this without this SO answer I stumbled on: // https://stackoverflow.com/questions/37781676/how-to-use-com-component-object-model-in-golang @@ -45,18 +31,44 @@ type ICLRMetaHostVtbl struct { ExitProcess uintptr } -// GetICLRMetaHost is a wrapper function to create and return an ICLRMetahost object -func GetICLRMetaHost() (metahost *ICLRMetaHost, err error) { - var pMetaHost uintptr - hr := CLRCreateInstance(&CLSID_CLRMetaHost, &IID_ICLRMetaHost, &pMetaHost) - err = checkOK(hr, "CLRCreateInstance") - if err != nil { +// CLRCreateInstance provides one of three interfaces: ICLRMetaHost, ICLRMetaHostPolicy, or ICLRDebugging. +// HRESULT CLRCreateInstance( +// [in] REFCLSID clsid, +// [in] REFIID riid, +// [out] LPVOID * ppInterface +// ); +// https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/hosting/clrcreateinstance-function +func CLRCreateInstance(clsid, riid windows.GUID) (ppInterface *ICLRMetaHost, err error) { + debugPrint("Entering into iclrmetahost.CLRCreateInstance()...") + + if clsid != CLSID_CLRMetaHost { + err = fmt.Errorf("the input Class ID (CLSID) is not supported: %s", clsid) return } - metahost = NewICLRMetaHostFromPtr(pMetaHost) + + modMSCoree := syscall.MustLoadDLL("mscoree.dll") + procCLRCreateInstance := modMSCoree.MustFindProc("CLRCreateInstance") + + // For some reason this procedure call returns "The specified procedure could not be found." even though it works + hr, _, _ := procCLRCreateInstance.Call( + uintptr(unsafe.Pointer(&clsid)), + uintptr(unsafe.Pointer(&riid)), + uintptr(unsafe.Pointer(&ppInterface)), + ) + + if hr != S_OK { + err = fmt.Errorf("the mscoree!CLRCreateInstance function returned a non-zero HRESULT: 0x%x", hr) + return + } + err = nil return } +// GetICLRMetaHost is a wrapper function to create and return an ICLRMetahost object +func GetICLRMetaHost() (metahost *ICLRMetaHost, err error) { + return CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost) +} + // NewICLRMetaHost takes a uintptr to an ICLRMetahost struct in memory. This pointer should come from the syscall CLRCreateInstance func NewICLRMetaHostFromPtr(ppv uintptr) *ICLRMetaHost { return (*ICLRMetaHost)(unsafe.Pointer(ppv)) From b3af3fa1d0aa4e4d9c2f13ebfec6fbd86afdfd16 Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Thu, 18 Mar 2021 12:17:42 -0400 Subject: [PATCH 04/31] Fixed misuse of unsafe.Pointer for iclrmetahost --- examples/EXEfromMemory/EXEfromMemory.go | 11 ++--- go-clr.go | 12 ++--- iclrmetahost.go | 63 +++++++++++++++++++------ iclrruntimeinfo.go | 15 ++---- ienumunknown.go | 31 +++++++++--- 5 files changed, 85 insertions(+), 47 deletions(-) diff --git a/examples/EXEfromMemory/EXEfromMemory.go b/examples/EXEfromMemory/EXEfromMemory.go index 6cb0a84..264427e 100644 --- a/examples/EXEfromMemory/EXEfromMemory.go +++ b/examples/EXEfromMemory/EXEfromMemory.go @@ -49,14 +49,13 @@ func main() { must(err) versionString := "v4.0.30319" - pwzVersion, _ := syscall.UTF16PtrFromString(versionString) - var pRuntimeInfo uintptr - hr := metaHost.GetRuntime(pwzVersion, &clr.IID_ICLRRuntimeInfo, &pRuntimeInfo) - checkOK(hr, "metahost.GetRuntime") - runtimeInfo := clr.NewICLRRuntimeInfoFromPtr(pRuntimeInfo) + pwzVersion, err := syscall.UTF16PtrFromString(versionString) + must(err) + runtimeInfo, err := metaHost.GetRuntime(pwzVersion, clr.IID_ICLRRuntimeInfo) + must(err) var isLoadable bool - hr = runtimeInfo.IsLoadable(&isLoadable) + hr := runtimeInfo.IsLoadable(&isLoadable) checkOK(hr, "runtimeInfo.IsLoadable") if !isLoadable { log.Fatal("[!] IsLoadable returned false. Bailing...") diff --git a/go-clr.go b/go-clr.go index 6328c21..c85b440 100644 --- a/go-clr.go +++ b/go-clr.go @@ -14,25 +14,21 @@ import ( // GetInstallRuntimes is a wrapper function that returns an array of installed runtimes. Requires an existing ICLRMetaHost func GetInstalledRuntimes(metahost *ICLRMetaHost) ([]string, error) { var runtimes []string - var pInstalledRuntimes uintptr - hr := metahost.EnumerateInstalledRuntimes(&pInstalledRuntimes) - err := checkOK(hr, "EnumerateInstalledRuntimes") + installedRuntimes, err := metahost.EnumerateInstalledRuntimes() if err != nil { return runtimes, err } - installedRuntimes := NewIEnumUnknownFromPtr(pInstalledRuntimes) - var pRuntimeInfo uintptr + var fetched = uint32(0) var versionString string versionStringBytes := make([]uint16, 20) versionStringSize := uint32(len(versionStringBytes)) var runtimeInfo *ICLRRuntimeInfo for { - hr = installedRuntimes.Next(1, &pRuntimeInfo, &fetched) - if hr != S_OK { + err := installedRuntimes.Next(1, unsafe.Pointer(runtimeInfo), &fetched) + if err != nil { break } - runtimeInfo = NewICLRRuntimeInfoFromPtr(pRuntimeInfo) if ret := runtimeInfo.GetVersionString(&versionStringBytes[0], &versionStringSize); ret != S_OK { return runtimes, fmt.Errorf("GetVersionString returned 0x%08x", ret) } diff --git a/iclrmetahost.go b/iclrmetahost.go index 9d39312..2f5fcd5 100644 --- a/iclrmetahost.go +++ b/iclrmetahost.go @@ -66,14 +66,10 @@ func CLRCreateInstance(clsid, riid windows.GUID) (ppInterface *ICLRMetaHost, err // GetICLRMetaHost is a wrapper function to create and return an ICLRMetahost object func GetICLRMetaHost() (metahost *ICLRMetaHost, err error) { + // TODO Get rid of ^ function return CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost) } -// NewICLRMetaHost takes a uintptr to an ICLRMetahost struct in memory. This pointer should come from the syscall CLRCreateInstance -func NewICLRMetaHostFromPtr(ppv uintptr) *ICLRMetaHost { - return (*ICLRMetaHost)(unsafe.Pointer(ppv)) -} - func (obj *ICLRMetaHost) AddRef() uintptr { ret, _, _ := syscall.Syscall( obj.vtbl.AddRef, @@ -94,25 +90,62 @@ func (obj *ICLRMetaHost) Release() uintptr { return ret } -func (obj *ICLRMetaHost) EnumerateInstalledRuntimes(pInstalledRuntimes *uintptr) uintptr { - ret, _, _ := syscall.Syscall( +// EnumerateInstalledRuntimes returns an enumeration that contains a valid ICLRRuntimeInfo interface for each +// version of the common language runtime (CLR) that is installed on a computer. +// HRESULT EnumerateInstalledRuntimes ( +// [out, retval] IEnumUnknown **ppEnumerator); +// https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/hosting/iclrmetahost-enumerateinstalledruntimes-method +func (obj *ICLRMetaHost) EnumerateInstalledRuntimes() (ppEnumerator *IEnumUnknown, err error) { + debugPrint("Entering into iclrmetahost.EnumerateInstalledRuntimes()...") + hr, _, err := syscall.Syscall( obj.vtbl.EnumerateInstalledRuntimes, 2, uintptr(unsafe.Pointer(obj)), - uintptr(unsafe.Pointer(pInstalledRuntimes)), - 0) - return ret + uintptr(unsafe.Pointer(ppEnumerator)), + 0, + ) + if err != syscall.Errno(0) { + err = fmt.Errorf("there was an error calling the ICLRMetaHost::GetRuntime method:\r\n%s", err) + return + } + if hr != S_OK { + err = fmt.Errorf("the ICLRMetaHost::GetRuntime method returned a non-zero HRESULT: 0x%x", hr) + return + } + err = nil + return } -func (obj *ICLRMetaHost) GetRuntime(pwzVersion *uint16, riid *windows.GUID, pRuntimeHost *uintptr) uintptr { - ret, _, _ := syscall.Syscall6( +// GetRuntime gets the ICLRRuntimeInfo interface that corresponds to a particular version of the common language runtime (CLR). +// This method supersedes the CorBindToRuntimeEx function used with the STARTUP_LOADER_SAFEMODE flag. +// HRESULT GetRuntime ( +// [in] LPCWSTR pwzVersion, +// [in] REFIID riid, +// [out,iid_is(riid), retval] LPVOID *ppRuntime +// ); +// https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/hosting/iclrmetahost-getruntime-method +func (obj *ICLRMetaHost) GetRuntime(pwzVersion *uint16, riid windows.GUID) (ppRuntime *ICLRRuntimeInfo, err error) { + debugPrint("Entering into iclrmetahost.GetRuntime()...") + + hr, _, err := syscall.Syscall6( obj.vtbl.GetRuntime, 4, uintptr(unsafe.Pointer(obj)), uintptr(unsafe.Pointer(pwzVersion)), uintptr(unsafe.Pointer(&IID_ICLRRuntimeInfo)), - uintptr(unsafe.Pointer(pRuntimeHost)), + uintptr(unsafe.Pointer(&ppRuntime)), 0, - 0) - return ret + 0, + ) + + if err != syscall.Errno(0) { + err = fmt.Errorf("there was an error calling the ICLRMetaHost::GetRuntime method:\r\n%s", err) + return + } + if hr != S_OK { + err = fmt.Errorf("the ICLRMetaHost::GetRuntime method returned a non-zero HRESULT: 0x%x", hr) + return + } + err = nil + return } diff --git a/iclrruntimeinfo.go b/iclrruntimeinfo.go index 5d8a5d5..a1c258e 100644 --- a/iclrruntimeinfo.go +++ b/iclrruntimeinfo.go @@ -3,9 +3,10 @@ package clr import ( - "golang.org/x/sys/windows" "syscall" "unsafe" + + "golang.org/x/sys/windows" ) type ICLRRuntimeInfo struct { @@ -36,17 +37,7 @@ func GetRuntimeInfo(metahost *ICLRMetaHost, version string) (*ICLRRuntimeInfo, e if err != nil { return nil, err } - var pRuntimeInfo uintptr - hr := metahost.GetRuntime(pwzVersion, &IID_ICLRRuntimeInfo, &pRuntimeInfo) - err = checkOK(hr, "metahost.GetRuntime") - if err != nil { - return nil, err - } - return NewICLRRuntimeInfoFromPtr(pRuntimeInfo), nil -} - -func NewICLRRuntimeInfoFromPtr(ppv uintptr) *ICLRRuntimeInfo { - return (*ICLRRuntimeInfo)(unsafe.Pointer(ppv)) + return metahost.GetRuntime(pwzVersion, IID_ICLRRuntimeInfo) } func (obj *ICLRRuntimeInfo) AddRef() uintptr { diff --git a/ienumunknown.go b/ienumunknown.go index bf1d457..73d1470 100644 --- a/ienumunknown.go +++ b/ienumunknown.go @@ -3,6 +3,7 @@ package clr import ( + "fmt" "syscall" "unsafe" ) @@ -45,15 +46,33 @@ func (obj *IEnumUnknown) Release() uintptr { return ret } -func (obj *IEnumUnknown) Next(celt uint32, pEnumRuntime *uintptr, pCeltFetched *uint32) uintptr { - ret, _, _ := syscall.Syscall6( +// Next retrieves the specified number of items in the enumeration sequence. +// HRESULT Next( +// ULONG celt, +// IUnknown **rgelt, +// ULONG *pceltFetched +// ); +// https://docs.microsoft.com/en-us/windows/win32/api/objidl/nf-objidl-ienumunknown-next +func (obj *IEnumUnknown) Next(celt uint32, pEnumRuntime unsafe.Pointer, pceltFetched *uint32) (err error) { + debugPrint("Entering into ienumunknown.Next()...") + hr, _, err := syscall.Syscall6( obj.vtbl.Next, 4, uintptr(unsafe.Pointer(obj)), uintptr(celt), - uintptr(unsafe.Pointer(pEnumRuntime)), - uintptr(unsafe.Pointer(pCeltFetched)), + uintptr(pEnumRuntime), + uintptr(unsafe.Pointer(pceltFetched)), 0, - 0) - return ret + 0, + ) + if err != syscall.Errno(0) { + err = fmt.Errorf("there was an error calling the IEnumUnknown::Next method:\r\n%s", err) + return + } + if hr != S_OK { + err = fmt.Errorf("the IEnumUnknown::Next method method returned a non-zero HRESULT: 0x%x", hr) + return + } + err = nil + return } From d17a78a5c9a4eedd6bdfaa78d5f4c24666f03137 Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Thu, 18 Mar 2021 12:44:57 -0400 Subject: [PATCH 05/31] Modified iclrruntimeinfo methods --- examples/EXEfromMemory/EXEfromMemory.go | 10 +++---- go-clr.go | 10 +++---- iclrruntimeinfo.go | 35 ++++++++++++++++++++----- 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/examples/EXEfromMemory/EXEfromMemory.go b/examples/EXEfromMemory/EXEfromMemory.go index 264427e..a483fb5 100644 --- a/examples/EXEfromMemory/EXEfromMemory.go +++ b/examples/EXEfromMemory/EXEfromMemory.go @@ -55,17 +55,17 @@ func main() { must(err) var isLoadable bool - hr := runtimeInfo.IsLoadable(&isLoadable) - checkOK(hr, "runtimeInfo.IsLoadable") + err = runtimeInfo.IsLoadable(&isLoadable) + must(err) if !isLoadable { log.Fatal("[!] IsLoadable returned false. Bailing...") } - hr = runtimeInfo.BindAsLegacyV2Runtime() - checkOK(hr, "runtimeInfo.BindAsLegacyV2Runtime") + err = runtimeInfo.BindAsLegacyV2Runtime() + must(err) var pRuntimeHost uintptr - hr = runtimeInfo.GetInterface(&clr.CLSID_CorRuntimeHost, &clr.IID_ICorRuntimeHost, &pRuntimeHost) + hr := runtimeInfo.GetInterface(&clr.CLSID_CorRuntimeHost, &clr.IID_ICorRuntimeHost, &pRuntimeHost) runtimeHost := clr.NewICORRuntimeHostFromPtr(pRuntimeHost) hr = runtimeHost.Start() checkOK(hr, "runtimeHost.Start") diff --git a/go-clr.go b/go-clr.go index c85b440..2b0d7e0 100644 --- a/go-clr.go +++ b/go-clr.go @@ -73,8 +73,7 @@ func ExecuteDLLFromDisk(targetRuntime, dllpath, typeName, methodName, argument s return } var isLoadable bool - hr := runtimeInfo.IsLoadable(&isLoadable) - err = checkOK(hr, "runtimeInfo.IsLoadable") + err = runtimeInfo.IsLoadable(&isLoadable) if err != nil { return } @@ -91,7 +90,7 @@ func ExecuteDLLFromDisk(targetRuntime, dllpath, typeName, methodName, argument s pMethodName, _ := syscall.UTF16PtrFromString(methodName) pArgument, _ := syscall.UTF16PtrFromString(argument) var pReturnVal uint16 - hr = runtimeHost.ExecuteInDefaultAppDomain(pDLLPath, pTypeName, pMethodName, pArgument, &pReturnVal) + hr := runtimeHost.ExecuteInDefaultAppDomain(pDLLPath, pTypeName, pMethodName, pArgument, &pReturnVal) err = checkOK(hr, "runtimeHost.ExecuteInDefaultAppDomain") if err != nil { return int16(pReturnVal), err @@ -135,8 +134,7 @@ func ExecuteByteArray(targetRuntime string, rawBytes []byte, params []string) (r return } var isLoadable bool - hr := runtimeInfo.IsLoadable(&isLoadable) - err = checkOK(hr, "runtimeInfo.IsLoadable") + err = runtimeInfo.IsLoadable(&isLoadable) if err != nil { return } @@ -162,7 +160,7 @@ func ExecuteByteArray(targetRuntime string, rawBytes []byte, params []string) (r } var pEntryPointInfo uintptr - hr = assembly.GetEntryPoint(&pEntryPointInfo) + hr := assembly.GetEntryPoint(&pEntryPointInfo) err = checkOK(hr, "assembly.GetEntryPoint") if err != nil { return diff --git a/iclrruntimeinfo.go b/iclrruntimeinfo.go index a1c258e..5f5d7b6 100644 --- a/iclrruntimeinfo.go +++ b/iclrruntimeinfo.go @@ -3,6 +3,7 @@ package clr import ( + "fmt" "syscall" "unsafe" @@ -83,23 +84,45 @@ func (obj *ICLRRuntimeInfo) GetInterface(rclsid *windows.GUID, riid *windows.GUI return ret } -func (obj *ICLRRuntimeInfo) BindAsLegacyV2Runtime() uintptr { - ret, _, _ := syscall.Syscall( +// BindAsLegacyV2Runtime binds the current runtime for all legacy common language runtime (CLR) version 2 activation policy decisions. +// HRESULT BindAsLegacyV2Runtime (); +// https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/hosting/iclrruntimeinfo-bindaslegacyv2runtime-method +func (obj *ICLRRuntimeInfo) BindAsLegacyV2Runtime() error { + debugPrint("Entering into iclrruntimeinfo.BindAsLegacyV2Runtime()...") + hr, _, err := syscall.Syscall( obj.vtbl.BindAsLegacyV2Runtime, 1, uintptr(unsafe.Pointer(obj)), 0, 0, ) - return ret + if err != syscall.Errno(0) { + return fmt.Errorf("the ICLRRuntimeInfo::BindAsLegacyV2Runtime method returned an error:\r\n%s", err) + } + if hr != S_OK { + return fmt.Errorf("the ICLRRuntimeInfo::BindAsLegacyV2Runtime method returned a non-zero HRESULT: 0x%x", hr) + } + return nil } -func (obj *ICLRRuntimeInfo) IsLoadable(pbLoadable *bool) uintptr { - ret, _, _ := syscall.Syscall( +// IsLoadable indicates whether the runtime associated with this interface can be loaded into the current process, +// taking into account other runtimes that might already be loaded into the process. +// HRESULT IsLoadable( +// [out, retval] BOOL *pbLoadable); +// https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/hosting/iclrruntimeinfo-isloadable-method +func (obj *ICLRRuntimeInfo) IsLoadable(pbLoadable *bool) error { + debugPrint("Entering into iclrruntimeinfo.IsLoadable()...") + hr, _, err := syscall.Syscall( obj.vtbl.IsLoadable, 2, uintptr(unsafe.Pointer(obj)), uintptr(unsafe.Pointer(pbLoadable)), 0) - return ret + if err != syscall.Errno(0) { + return fmt.Errorf("the ICLRRuntimeInfo::IsLoadable method returned an error:\r\n%s", err) + } + if hr != S_OK { + return fmt.Errorf("the ICLRRuntimeInfo::IsLoadable method returned a non-zero HRESULT: 0x%x", hr) + } + return nil } From 4715ed5709075e5ec0d3ad49715489f3ab784622 Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Thu, 18 Mar 2021 20:59:42 -0400 Subject: [PATCH 06/31] Fixed misuse of unsafe.Pointer --- examples/EXEfromMemory/EXEfromMemory.go | 12 ++++---- iclrruntimehost.go | 36 ++++++++++++++---------- iclrruntimeinfo.go | 33 ++++++++++++++++------ icorruntimehost.go | 37 +++++++++++++++---------- 4 files changed, 76 insertions(+), 42 deletions(-) diff --git a/examples/EXEfromMemory/EXEfromMemory.go b/examples/EXEfromMemory/EXEfromMemory.go index a483fb5..a482be3 100644 --- a/examples/EXEfromMemory/EXEfromMemory.go +++ b/examples/EXEfromMemory/EXEfromMemory.go @@ -64,16 +64,16 @@ func main() { err = runtimeInfo.BindAsLegacyV2Runtime() must(err) - var pRuntimeHost uintptr - hr := runtimeInfo.GetInterface(&clr.CLSID_CorRuntimeHost, &clr.IID_ICorRuntimeHost, &pRuntimeHost) - runtimeHost := clr.NewICORRuntimeHostFromPtr(pRuntimeHost) - hr = runtimeHost.Start() - checkOK(hr, "runtimeHost.Start") + var runtimeHost *clr.ICORRuntimeHost + err = runtimeInfo.GetInterface(clr.CLSID_CorRuntimeHost, clr.IID_ICorRuntimeHost, unsafe.Pointer(&runtimeHost)) + must(err) + err = runtimeHost.Start() + must(err) fmt.Println("[+] Loaded CLR into this process") var pAppDomain uintptr var pIUnknown uintptr - hr = runtimeHost.GetDefaultDomain(&pIUnknown) + hr := runtimeHost.GetDefaultDomain(&pIUnknown) checkOK(hr, "runtimeHost.GetDefaultDomain") iu := clr.NewIUnknownFromPtr(pIUnknown) hr = iu.QueryInterface(&clr.IID_AppDomain, &pAppDomain) diff --git a/iclrruntimehost.go b/iclrruntimehost.go index f596a9f..0b6e29c 100644 --- a/iclrruntimehost.go +++ b/iclrruntimehost.go @@ -3,6 +3,7 @@ package clr import ( + "fmt" "syscall" "unsafe" ) @@ -25,23 +26,19 @@ type ICLRRuntimeHostVtbl struct { ExecuteApplication uintptr ExecuteInDefaultAppDomain uintptr } + // GetICLRRuntimeHost is a wrapper function that takes an ICLRRuntimeInfo object and // returns an ICLRRuntimeHost and loads it into the current process func GetICLRRuntimeHost(runtimeInfo *ICLRRuntimeInfo) (*ICLRRuntimeHost, error) { - var pRuntimeHost uintptr - hr := runtimeInfo.GetInterface(&CLSID_CLRRuntimeHost, &IID_ICLRRuntimeHost, &pRuntimeHost) - err := checkOK(hr, "runtimeInfo.GetInterface") + debugPrint("Entering into iclrruntimehost.GetICLRRuntimeHost()...") + var runtimeHost *ICLRRuntimeHost + err := runtimeInfo.GetInterface(CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost, unsafe.Pointer(&runtimeHost)) if err != nil { return nil, err } - runtimeHost := NewICLRRuntimeHostFromPtr(pRuntimeHost) - hr = runtimeHost.Start() - err = checkOK(hr, "runtimeHost.Start") - return runtimeHost, err -} -func NewICLRRuntimeHostFromPtr(ppv uintptr) *ICLRRuntimeHost { - return (*ICLRRuntimeHost)(unsafe.Pointer(ppv)) + err = runtimeHost.Start() + return runtimeHost, err } func (obj *ICLRRuntimeHost) AddRef() uintptr { @@ -64,14 +61,25 @@ func (obj *ICLRRuntimeHost) Release() uintptr { return ret } -func (obj *ICLRRuntimeHost) Start() uintptr { - ret, _, _ := syscall.Syscall( +// Start Initializes the common language runtime (CLR) into a process. +// HRESULT Start(); +// https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/hosting/iclrruntimehost-start-method +func (obj *ICLRRuntimeHost) Start() error { + debugPrint("Entering into iclrruntimehost.Start()...") + hr, _, err := syscall.Syscall( obj.vtbl.Start, 1, uintptr(unsafe.Pointer(obj)), 0, - 0) - return ret + 0, + ) + if err != syscall.Errno(0) { + return fmt.Errorf("the ICLRRuntimeHost::Start method returned an error:\r\n%s", err) + } + if hr != S_OK { + return fmt.Errorf("the ICLRRuntimeHost::Start method method returned a non-zero HRESULT: 0x%x", hr) + } + return nil } func (obj *ICLRRuntimeHost) ExecuteInDefaultAppDomain(pwzAssemblyPath, pwzTypeName, pwzMethodName, pwzArgument, pReturnValue *uint16) uintptr { diff --git a/iclrruntimeinfo.go b/iclrruntimeinfo.go index 5f5d7b6..55e2c33 100644 --- a/iclrruntimeinfo.go +++ b/iclrruntimeinfo.go @@ -71,17 +71,34 @@ func (obj *ICLRRuntimeInfo) GetVersionString(pcchBuffer *uint16, pVersionstringS return ret } -func (obj *ICLRRuntimeInfo) GetInterface(rclsid *windows.GUID, riid *windows.GUID, ppUnk *uintptr) uintptr { - ret, _, _ := syscall.Syscall6( +// GetInterface loads the CLR into the current process and returns runtime interface pointers, +// such as ICLRRuntimeHost, ICLRStrongName, and IMetaDataDispenserEx. +// HRESULT GetInterface( +// [in] REFCLSID rclsid, +// [in] REFIID riid, +// [out, iid_is(riid), retval] LPVOID *ppUnk); unsafe pointer of a pointer to an object pointer +// https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/hosting/iclrruntimeinfo-getinterface-method +func (obj *ICLRRuntimeInfo) GetInterface(rclsid windows.GUID, riid windows.GUID, ppUnk unsafe.Pointer) error { + debugPrint("Entering into iclrruntimeinfo.GetInterface()...") + hr, _, err := syscall.Syscall6( obj.vtbl.GetInterface, 4, uintptr(unsafe.Pointer(obj)), - uintptr(unsafe.Pointer(rclsid)), - uintptr(unsafe.Pointer(riid)), - uintptr(unsafe.Pointer(ppUnk)), + uintptr(unsafe.Pointer(&rclsid)), + uintptr(unsafe.Pointer(&riid)), + uintptr(ppUnk), 0, - 0) - return ret + 0, + ) + // The syscall returns "The requested lookup key was not found in any active activation context." in the error position + // TODO Why is this error message returned? + if err.Error() != "The requested lookup key was not found in any active activation context." { + return fmt.Errorf("the ICLRRuntimeInfo::GetInterface method returned an error:\r\n%s", err) + } + if hr != S_OK { + return fmt.Errorf("the ICLRRuntimeInfo::GetInterface method returned a non-zero HRESULT: 0x%x", hr) + } + return nil } // BindAsLegacyV2Runtime binds the current runtime for all legacy common language runtime (CLR) version 2 activation policy decisions. @@ -100,7 +117,7 @@ func (obj *ICLRRuntimeInfo) BindAsLegacyV2Runtime() error { return fmt.Errorf("the ICLRRuntimeInfo::BindAsLegacyV2Runtime method returned an error:\r\n%s", err) } if hr != S_OK { - return fmt.Errorf("the ICLRRuntimeInfo::BindAsLegacyV2Runtime method returned a non-zero HRESULT: 0x%x", hr) + return fmt.Errorf("the ICLRRuntimeInfo::BindAsLegacyV2Runtime method returned a non-zero HRESULT: 0x%x", hr) } return nil } diff --git a/icorruntimehost.go b/icorruntimehost.go index e5a4173..86c47a3 100644 --- a/icorruntimehost.go +++ b/icorruntimehost.go @@ -3,6 +3,7 @@ package clr import ( + "fmt" "syscall" "unsafe" ) @@ -40,20 +41,15 @@ type ICORRuntimeHostVtbl struct { // and loads it into the current process. This is the "deprecated" API, but the only way currently to load an assembly // from memory (afaict) func GetICORRuntimeHost(runtimeInfo *ICLRRuntimeInfo) (*ICORRuntimeHost, error) { - var pRuntimeHost uintptr - hr := runtimeInfo.GetInterface(&CLSID_CorRuntimeHost, &IID_ICorRuntimeHost, &pRuntimeHost) - err := checkOK(hr, "runtimeInfo.GetInterface") + debugPrint("Entering into icorruntimehost.GetICORRuntimeHost()...") + var runtimeHost *ICORRuntimeHost + err := runtimeInfo.GetInterface(CLSID_CorRuntimeHost, IID_ICorRuntimeHost, unsafe.Pointer(&runtimeHost)) if err != nil { return nil, err } - runtimeHost := NewICORRuntimeHostFromPtr(pRuntimeHost) - hr = runtimeHost.Start() - err = checkOK(hr, "runtimeHost.Start") - return runtimeHost, err -} -func NewICORRuntimeHostFromPtr(ppv uintptr) *ICORRuntimeHost { - return (*ICORRuntimeHost)(unsafe.Pointer(ppv)) + err = runtimeHost.Start() + return runtimeHost, err } func (obj *ICORRuntimeHost) AddRef() uintptr { @@ -76,14 +72,27 @@ func (obj *ICORRuntimeHost) Release() uintptr { return ret } -func (obj *ICORRuntimeHost) Start() uintptr { - ret, _, _ := syscall.Syscall( +// Start starts the common language runtime (CLR). +// HRESULT Start (); +// https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/hosting/icorruntimehost-start-method +func (obj *ICORRuntimeHost) Start() error { + debugPrint("Entering into icorruntimehost.Start()...") + hr, _, err := syscall.Syscall( obj.vtbl.Start, 1, uintptr(unsafe.Pointer(obj)), 0, - 0) - return ret + 0, + ) + if err != syscall.Errno(0) { + // The system could not find the environment option that was entered. + // TODO Why is this error message returned? + fmt.Printf("the ICORRuntimeHost::Start method returned an error:\r\n%s\n", err) + } + if hr != S_OK { + return fmt.Errorf("the ICORRuntimeHost::Start method method returned a non-zero HRESULT: 0x%x", hr) + } + return nil } func (obj *ICORRuntimeHost) GetDefaultDomain(pAppDomain *uintptr) uintptr { From e81f8ebd0f79e5bb34f3cfe4d3b38514e611144c Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Thu, 18 Mar 2021 21:25:24 -0400 Subject: [PATCH 07/31] Update GetDefaultDomain --- appdomain.go | 11 +++++----- examples/EXEfromMemory/EXEfromMemory.go | 11 +++++----- icorruntimehost.go | 28 +++++++++++++++++++------ 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/appdomain.go b/appdomain.go index 91cf304..3ebf825 100644 --- a/appdomain.go +++ b/appdomain.go @@ -90,16 +90,15 @@ type AppDomainVtbl struct { // GetAppDomain is a wrapper function that returns an appDomain from an existing ICORRuntimeHost object func GetAppDomain(runtimeHost *ICORRuntimeHost) (appDomain *AppDomain, err error) { var pAppDomain uintptr - var pIUnknown uintptr - hr := runtimeHost.GetDefaultDomain(&pIUnknown) - err = checkOK(hr, "runtimeHost.GetDefaultDomain") + //var pIUnknown uintptr + iu, err := runtimeHost.GetDefaultDomain() if err != nil { return } - iu := NewIUnknownFromPtr(pIUnknown) - hr = iu.QueryInterface(&IID_AppDomain, &pAppDomain) + //iu := NewIUnknownFromPtr(pIUnknown) + hr := iu.QueryInterface(&IID_AppDomain, &pAppDomain) err = checkOK(hr, "IUnknown.QueryInterface") - return NewAppDomainFromPtr(pAppDomain), err + return } func NewAppDomainFromPtr(ppv uintptr) *AppDomain { diff --git a/examples/EXEfromMemory/EXEfromMemory.go b/examples/EXEfromMemory/EXEfromMemory.go index a482be3..82a5fbf 100644 --- a/examples/EXEfromMemory/EXEfromMemory.go +++ b/examples/EXEfromMemory/EXEfromMemory.go @@ -72,11 +72,12 @@ func main() { fmt.Println("[+] Loaded CLR into this process") var pAppDomain uintptr - var pIUnknown uintptr - hr := runtimeHost.GetDefaultDomain(&pIUnknown) - checkOK(hr, "runtimeHost.GetDefaultDomain") - iu := clr.NewIUnknownFromPtr(pIUnknown) - hr = iu.QueryInterface(&clr.IID_AppDomain, &pAppDomain) + //var pIUnknown uintptr + iu, err := runtimeHost.GetDefaultDomain() + must(err) + + //iu := clr.NewIUnknownFromPtr(uintptr(unsafe.Pointer(appDomain2))) + hr := iu.QueryInterface(&clr.IID_AppDomain, &pAppDomain) checkOK(hr, "iu.QueryInterface") appDomain := clr.NewAppDomainFromPtr(pAppDomain) fmt.Println("[+] Got default AppDomain") diff --git a/icorruntimehost.go b/icorruntimehost.go index 86c47a3..f1a7609 100644 --- a/icorruntimehost.go +++ b/icorruntimehost.go @@ -87,7 +87,7 @@ func (obj *ICORRuntimeHost) Start() error { if err != syscall.Errno(0) { // The system could not find the environment option that was entered. // TODO Why is this error message returned? - fmt.Printf("the ICORRuntimeHost::Start method returned an error:\r\n%s\n", err) + debugPrint(fmt.Sprintf("the ICORRuntimeHost::Start method returned an error:\r\n%s", err)) } if hr != S_OK { return fmt.Errorf("the ICORRuntimeHost::Start method method returned a non-zero HRESULT: 0x%x", hr) @@ -95,12 +95,28 @@ func (obj *ICORRuntimeHost) Start() error { return nil } -func (obj *ICORRuntimeHost) GetDefaultDomain(pAppDomain *uintptr) uintptr { - ret, _, _ := syscall.Syscall( +// GetDefaultDomain gets an interface pointer of type System._AppDomain that represents the default domain for the current process. +// HRESULT GetDefaultDomain ( +// [out] IUnknown** pAppDomain +// ); +// https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/hosting/icorruntimehost-getdefaultdomain-method +func (obj *ICORRuntimeHost) GetDefaultDomain() (IUnknown *IUnknown, err error) { + debugPrint("Entering into icorruntimehost.GetDefaultDomain()...") + hr, _, err := syscall.Syscall( obj.vtbl.GetDefaultDomain, 2, uintptr(unsafe.Pointer(obj)), - uintptr(unsafe.Pointer(pAppDomain)), - 0) - return ret + uintptr(unsafe.Pointer(&IUnknown)), + 0, + ) + if err != syscall.Errno(0) { + // The specified procedure could not be found. + // TODO Why is this error message returned? + debugPrint(fmt.Sprintf("the ICORRuntimeHost::GetDefaultDomain method returned an error:\r\n%s", err)) + } + if hr != S_OK { + err = fmt.Errorf("the ICORRuntimeHost::GetDefaultDomain method method returned a non-zero HRESULT: 0x%x", hr) + } + err = nil + return } From a46d3ab815b5eeb914d723783e685c06b9d022d6 Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Thu, 18 Mar 2021 21:39:12 -0400 Subject: [PATCH 08/31] Fixed misuse of unsafe.Pointer --- appdomain.go | 7 ++---- examples/EXEfromMemory/EXEfromMemory.go | 11 ++++----- icorruntimehost.go | 1 + iunknown.go | 31 ++++++++++++++++++------- 4 files changed, 29 insertions(+), 21 deletions(-) diff --git a/appdomain.go b/appdomain.go index 3ebf825..705890a 100644 --- a/appdomain.go +++ b/appdomain.go @@ -89,15 +89,12 @@ type AppDomainVtbl struct { // GetAppDomain is a wrapper function that returns an appDomain from an existing ICORRuntimeHost object func GetAppDomain(runtimeHost *ICORRuntimeHost) (appDomain *AppDomain, err error) { - var pAppDomain uintptr - //var pIUnknown uintptr + debugPrint("Entering into appdomain.GetAppDomain()...") iu, err := runtimeHost.GetDefaultDomain() if err != nil { return } - //iu := NewIUnknownFromPtr(pIUnknown) - hr := iu.QueryInterface(&IID_AppDomain, &pAppDomain) - err = checkOK(hr, "IUnknown.QueryInterface") + err = iu.QueryInterface(IID_AppDomain, unsafe.Pointer(&appDomain)) return } diff --git a/examples/EXEfromMemory/EXEfromMemory.go b/examples/EXEfromMemory/EXEfromMemory.go index 82a5fbf..476762f 100644 --- a/examples/EXEfromMemory/EXEfromMemory.go +++ b/examples/EXEfromMemory/EXEfromMemory.go @@ -71,15 +71,12 @@ func main() { must(err) fmt.Println("[+] Loaded CLR into this process") - var pAppDomain uintptr - //var pIUnknown uintptr iu, err := runtimeHost.GetDefaultDomain() must(err) - //iu := clr.NewIUnknownFromPtr(uintptr(unsafe.Pointer(appDomain2))) - hr := iu.QueryInterface(&clr.IID_AppDomain, &pAppDomain) - checkOK(hr, "iu.QueryInterface") - appDomain := clr.NewAppDomainFromPtr(pAppDomain) + var appDomain *clr.AppDomain + err = iu.QueryInterface(clr.IID_AppDomain, unsafe.Pointer(&appDomain)) + must(err) fmt.Println("[+] Got default AppDomain") safeArray, err := clr.CreateSafeArray(exebytes) @@ -93,7 +90,7 @@ func main() { fmt.Printf("[+] Executable loaded into memory at %p\n", assembly) var pEntryPointInfo uintptr - hr = assembly.GetEntryPoint(&pEntryPointInfo) + hr := assembly.GetEntryPoint(&pEntryPointInfo) checkOK(hr, "assembly.GetEntryPoint") fmt.Printf("[+] Executable entrypoint found at 0x%x\n", pEntryPointInfo) methodInfo := clr.NewMethodInfoFromPtr(pEntryPointInfo) diff --git a/icorruntimehost.go b/icorruntimehost.go index f1a7609..dc2c92d 100644 --- a/icorruntimehost.go +++ b/icorruntimehost.go @@ -116,6 +116,7 @@ func (obj *ICORRuntimeHost) GetDefaultDomain() (IUnknown *IUnknown, err error) { } if hr != S_OK { err = fmt.Errorf("the ICORRuntimeHost::GetDefaultDomain method method returned a non-zero HRESULT: 0x%x", hr) + return } err = nil return diff --git a/iunknown.go b/iunknown.go index 02808f0..f82f582 100644 --- a/iunknown.go +++ b/iunknown.go @@ -3,6 +3,7 @@ package clr import ( + "fmt" "syscall" "unsafe" @@ -19,18 +20,30 @@ type IUnknownVtbl struct { Release uintptr } -func NewIUnknownFromPtr(ppv uintptr) *IUnknown { - return (*IUnknown)(unsafe.Pointer(ppv)) -} - -func (obj *IUnknown) QueryInterface(riid *windows.GUID, ppvObject *uintptr) uintptr { - ret, _, _ := syscall.Syscall( +// QueryInterface queries a COM object for a pointer to one of its interface; +// identifying the interface by a reference to its interface identifier (IID). +// If the COM object implements the interface, then it returns a pointer to that interface after calling IUnknown::AddRef on it. +// HRESULT QueryInterface( +// REFIID riid, +// void **ppvObject +// ); +// https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-queryinterface(refiid_void) +func (obj *IUnknown) QueryInterface(riid windows.GUID, ppvObject unsafe.Pointer) error { + debugPrint("Entering into iunknown.QueryInterface()...") + hr, _, err := syscall.Syscall( obj.vtbl.QueryInterface, 3, uintptr(unsafe.Pointer(obj)), - uintptr(unsafe.Pointer(riid)), - uintptr(unsafe.Pointer(ppvObject))) - return ret + uintptr(unsafe.Pointer(&riid)), // A reference to the interface identifier (IID) of the interface being queried for. + uintptr(ppvObject), + ) + if err != syscall.Errno(0) { + return fmt.Errorf("the ICORRuntimeHost::GetDefaultDomain method returned an error:\r\n%s", err) + } + if hr != S_OK { + return fmt.Errorf("the ICORRuntimeHost::GetDefaultDomain method method returned a non-zero HRESULT: 0x%x", hr) + } + return nil } func (obj *IUnknown) AddRef() uintptr { From f0888b85b4f64461b88d08c018fc8987330e4d57 Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Fri, 19 Mar 2021 07:25:30 -0400 Subject: [PATCH 09/31] Fixed misuse of unsafe.Pointer --- appdomain.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/appdomain.go b/appdomain.go index 705890a..424ca3d 100644 --- a/appdomain.go +++ b/appdomain.go @@ -98,10 +98,6 @@ func GetAppDomain(runtimeHost *ICORRuntimeHost) (appDomain *AppDomain, err error return } -func NewAppDomainFromPtr(ppv uintptr) *AppDomain { - return (*AppDomain)(unsafe.Pointer(ppv)) -} - func (obj *AppDomain) QueryInterface(riid *windows.GUID, ppvObject *uintptr) uintptr { ret, _, _ := syscall.Syscall( obj.vtbl.QueryInterface, From a88b841aab6482c34019132d87b9b42b29aa9250 Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Fri, 19 Mar 2021 07:40:46 -0400 Subject: [PATCH 10/31] Fixed misuse of unsafe.Pointer --- assembly.go | 30 ++++++++++++++++++++----- examples/EXEfromMemory/EXEfromMemory.go | 10 ++++----- go-clr.go | 7 ++---- methodinfo.go | 4 ---- 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/assembly.go b/assembly.go index f38c0f0..01298c9 100644 --- a/assembly.go +++ b/assembly.go @@ -3,9 +3,11 @@ package clr import ( - "golang.org/x/sys/windows" + "fmt" "syscall" "unsafe" + + "golang.org/x/sys/windows" ) // from mscorlib.tlh @@ -102,12 +104,28 @@ func (obj *Assembly) Release() uintptr { return ret } -func (obj *Assembly) GetEntryPoint(pMethodInfo *uintptr) uintptr { - ret, _, _ := syscall.Syscall( +// GetEntryPoint returns the assembly's MethodInfo +// virtual HRESULT __stdcall get_EntryPoint ( +// /*[out,retval]*/ struct _MethodInfo * * pRetVal ) = 0; +// https://docs.microsoft.com/en-us/dotnet/api/system.reflection.assembly.entrypoint?view=netframework-4.8#System_Reflection_Assembly_EntryPoint +// https://docs.microsoft.com/en-us/dotnet/api/system.reflection.methodinfo?view=netframework-4.8 +func (obj *Assembly) GetEntryPoint() (pRetVal *MethodInfo, err error) { + debugPrint("Entering into assembly.GetEntryPoint()...") + hr, _, err := syscall.Syscall( obj.vtbl.get_EntryPoint, 2, uintptr(unsafe.Pointer(obj)), - uintptr(unsafe.Pointer(pMethodInfo)), - 0) - return ret + uintptr(unsafe.Pointer(&pRetVal)), + 0, + ) + if err != syscall.Errno(0) { + err = fmt.Errorf("the Assembly::GetEntryPoint method returned an error:\r\n%s", err) + return + } + if hr != S_OK { + err = fmt.Errorf("the Assembly::GetEntryPoint method returned a non-zero HRESULT: 0x%x", hr) + return + } + err = nil + return } diff --git a/examples/EXEfromMemory/EXEfromMemory.go b/examples/EXEfromMemory/EXEfromMemory.go index 476762f..0bfb748 100644 --- a/examples/EXEfromMemory/EXEfromMemory.go +++ b/examples/EXEfromMemory/EXEfromMemory.go @@ -89,11 +89,9 @@ func main() { fmt.Printf("[+] Loaded %d bytes into memory from %s\n", len(exebytes), filename) fmt.Printf("[+] Executable loaded into memory at %p\n", assembly) - var pEntryPointInfo uintptr - hr := assembly.GetEntryPoint(&pEntryPointInfo) - checkOK(hr, "assembly.GetEntryPoint") - fmt.Printf("[+] Executable entrypoint found at 0x%x\n", pEntryPointInfo) - methodInfo := clr.NewMethodInfoFromPtr(pEntryPointInfo) + methodInfo, err := assembly.GetEntryPoint() + must(err) + fmt.Printf("[+] Executable entrypoint found at 0x%x\n", uintptr(unsafe.Pointer(methodInfo))) var methodSignaturePtr, paramPtr uintptr err = methodInfo.GetString(&methodSignaturePtr) @@ -117,7 +115,7 @@ func main() { Val: uintptr(0), } fmt.Println("[+] Invoking...") - hr = methodInfo.Invoke_3( + hr := methodInfo.Invoke_3( nullVariant, paramPtr, &pRetCode) diff --git a/go-clr.go b/go-clr.go index 2b0d7e0..36824f7 100644 --- a/go-clr.go +++ b/go-clr.go @@ -159,13 +159,10 @@ func ExecuteByteArray(targetRuntime string, rawBytes []byte, params []string) (r return } - var pEntryPointInfo uintptr - hr := assembly.GetEntryPoint(&pEntryPointInfo) - err = checkOK(hr, "assembly.GetEntryPoint") + methodInfo, err := assembly.GetEntryPoint() if err != nil { return } - methodInfo := NewMethodInfoFromPtr(pEntryPointInfo) var methodSignaturePtr, paramPtr uintptr err = methodInfo.GetString(&methodSignaturePtr) @@ -185,7 +182,7 @@ func ExecuteByteArray(targetRuntime string, rawBytes []byte, params []string) (r VT: 1, Val: uintptr(0), } - hr = methodInfo.Invoke_3( + hr := methodInfo.Invoke_3( nullVariant, paramPtr, &pRetCode) diff --git a/methodinfo.go b/methodinfo.go index 4f6fdf5..34b1f52 100644 --- a/methodinfo.go +++ b/methodinfo.go @@ -59,10 +59,6 @@ type MethodInfoVtbl struct { GetBaseDefinition uintptr } -func NewMethodInfoFromPtr(ppv uintptr) *MethodInfo { - return (*MethodInfo)(unsafe.Pointer(ppv)) -} - func (obj *MethodInfo) QueryInterface(riid *windows.GUID, ppvObject *uintptr) uintptr { ret, _, _ := syscall.Syscall( obj.vtbl.QueryInterface, From 07d499f95bacf6d4ef2522fc165975e8e0918d53 Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Fri, 19 Mar 2021 08:02:20 -0400 Subject: [PATCH 11/31] Fixed misuse of unsafe.Pointer --- examples/EXEfromMemory/EXEfromMemory.go | 6 +++--- go-clr.go | 5 ++--- methodinfo.go | 27 +++++++++++++++++++------ utils.go | 3 ++- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/examples/EXEfromMemory/EXEfromMemory.go b/examples/EXEfromMemory/EXEfromMemory.go index 0bfb748..8b7f6c4 100644 --- a/examples/EXEfromMemory/EXEfromMemory.go +++ b/examples/EXEfromMemory/EXEfromMemory.go @@ -93,12 +93,12 @@ func main() { must(err) fmt.Printf("[+] Executable entrypoint found at 0x%x\n", uintptr(unsafe.Pointer(methodInfo))) - var methodSignaturePtr, paramPtr uintptr - err = methodInfo.GetString(&methodSignaturePtr) + var paramPtr uintptr + methodSignature, err := methodInfo.GetString() if err != nil { return } - methodSignature := clr.ReadUnicodeStr(unsafe.Pointer(methodSignaturePtr)) + fmt.Printf("[+] Checking if the assembly requires arguments\n") if !strings.Contains(methodSignature, "Void Main()") { if len(params) < 1 { diff --git a/go-clr.go b/go-clr.go index 36824f7..1e069a4 100644 --- a/go-clr.go +++ b/go-clr.go @@ -164,12 +164,11 @@ func ExecuteByteArray(targetRuntime string, rawBytes []byte, params []string) (r return } - var methodSignaturePtr, paramPtr uintptr - err = methodInfo.GetString(&methodSignaturePtr) + var paramPtr uintptr + methodSignature, err := methodInfo.GetString() if err != nil { return } - methodSignature := ReadUnicodeStr(unsafe.Pointer(methodSignaturePtr)) if expectsParams(methodSignature) { if paramPtr, err = PrepareParameters(params); err != nil { diff --git a/methodinfo.go b/methodinfo.go index 34b1f52..599a140 100644 --- a/methodinfo.go +++ b/methodinfo.go @@ -3,6 +3,7 @@ package clr import ( + "fmt" "syscall" "unsafe" @@ -100,7 +101,7 @@ func (obj *MethodInfo) GetType(pRetVal *uintptr) uintptr { } func (obj *MethodInfo) Invoke_3(variantObj Variant, parameters uintptr, pRetVal *uintptr) uintptr { - debugPrint("Entering into appdomain.Invoke_3()...") + debugPrint("Entering into methodinfo.Invoke_3()...") ret, _, _ := syscall.Syscall6( obj.vtbl.Invoke_3, 4, @@ -114,14 +115,28 @@ func (obj *MethodInfo) Invoke_3(variantObj Variant, parameters uintptr, pRetVal return ret } -// GetString returns a string version of the method's signature -func (obj *MethodInfo) GetString(addr *uintptr) error { - ret, _, _ := syscall.Syscall( +// GetString returns a string that represents the current object +// a string version of the method's signature +// public virtual string ToString (); +func (obj *MethodInfo) GetString() (str string, err error) { + debugPrint("Entering into methodinfo.GetString()...") + var object *string + hr, _, err := syscall.Syscall( obj.vtbl.get_ToString, 2, uintptr(unsafe.Pointer(obj)), - uintptr(unsafe.Pointer(addr)), + uintptr(unsafe.Pointer(&object)), 0, ) - return checkOK(ret, "get_ToString") + if err != syscall.Errno(0) { + err = fmt.Errorf("the MethodInfo::ToString method returned an error:\r\n%s", err) + return + } + if hr != S_OK { + err = fmt.Errorf("the Assembly::ToString method returned a non-zero HRESULT: 0x%x", hr) + return + } + err = nil + str = ReadUnicodeStr(unsafe.Pointer(object)) + return } diff --git a/utils.go b/utils.go index 18c815c..abc0e7c 100644 --- a/utils.go +++ b/utils.go @@ -16,7 +16,7 @@ import ( const S_OK = 0x0 -var Debug = false +var Debug = true func checkOK(hr uintptr, caller string) error { if hr != S_OK { @@ -45,6 +45,7 @@ func expectsParams(input string) bool { } func ReadUnicodeStr(ptr unsafe.Pointer) string { + debugPrint("Entering into utils.ReadUnicodeStr()...") var byteVal uint16 out := make([]uint16, 0) for i := 0; ; i++ { From ff8ffa76f13c3b9befb273cfa6d2d6df677fe3b4 Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Fri, 19 Mar 2021 08:34:16 -0400 Subject: [PATCH 12/31] Updated MethodInfo and PrepareParameters --- examples/EXEfromMemory/EXEfromMemory.go | 19 +++------- go-clr.go | 50 ++----------------------- methodinfo.go | 28 ++++++++++++-- utils.go | 39 ++++++++++++++++++- 4 files changed, 71 insertions(+), 65 deletions(-) diff --git a/examples/EXEfromMemory/EXEfromMemory.go b/examples/EXEfromMemory/EXEfromMemory.go index 8b7f6c4..1c6f101 100644 --- a/examples/EXEfromMemory/EXEfromMemory.go +++ b/examples/EXEfromMemory/EXEfromMemory.go @@ -93,41 +93,32 @@ func main() { must(err) fmt.Printf("[+] Executable entrypoint found at 0x%x\n", uintptr(unsafe.Pointer(methodInfo))) - var paramPtr uintptr + var paramSafeArray *clr.SafeArray methodSignature, err := methodInfo.GetString() if err != nil { return } - fmt.Printf("[+] Checking if the assembly requires arguments\n") + fmt.Println("[+] Checking if the assembly requires arguments...") if !strings.Contains(methodSignature, "Void Main()") { if len(params) < 1 { log.Fatal("the assembly requires arguments but none were provided\nUsage: EXEfromMemory.exe ") } - if paramPtr, err = clr.PrepareParameters(params); err != nil { + if paramSafeArray, err = clr.PrepareParameters(params); err != nil { log.Fatal(fmt.Sprintf("there was an error preparing the assembly arguments:\r\n%s", err)) } } - var pRetCode uintptr nullVariant := clr.Variant{ VT: 1, Val: uintptr(0), } fmt.Println("[+] Invoking...") - hr := methodInfo.Invoke_3( - nullVariant, - paramPtr, - &pRetCode) - - fmt.Println("-------") - - checkOK(hr, "methodInfo.Invoke_3") - fmt.Printf("[+] Executable returned code %d\n", pRetCode) + err = methodInfo.Invoke_3(nullVariant, paramSafeArray) + must(err) appDomain.Release() runtimeHost.Release() runtimeInfo.Release() metaHost.Release() - } diff --git a/go-clr.go b/go-clr.go index 1e069a4..0b787ca 100644 --- a/go-clr.go +++ b/go-clr.go @@ -164,28 +164,23 @@ func ExecuteByteArray(targetRuntime string, rawBytes []byte, params []string) (r return } - var paramPtr uintptr + var paramSafeArray *SafeArray methodSignature, err := methodInfo.GetString() if err != nil { return } if expectsParams(methodSignature) { - if paramPtr, err = PrepareParameters(params); err != nil { + if paramSafeArray, err = PrepareParameters(params); err != nil { return } } - var pRetCode uintptr nullVariant := Variant{ VT: 1, Val: uintptr(0), } - hr := methodInfo.Invoke_3( - nullVariant, - paramPtr, - &pRetCode) - err = checkOK(hr, "methodInfo.Invoke_3") + err = methodInfo.Invoke_3(nullVariant, paramSafeArray) if err != nil { return } @@ -193,42 +188,5 @@ func ExecuteByteArray(targetRuntime string, rawBytes []byte, params []string) (r runtimeHost.Release() runtimeInfo.Release() metahost.Release() - return int32(pRetCode), nil - -} - -// PrepareParameters creates a safe array of strings (arguments) nested inside a Variant object, which is itself -// appended to the final safe array -func PrepareParameters(params []string) (uintptr, error) { - sab := SafeArrayBound{ - cElements: uint32(len(params)), - lLbound: 0, - } - listStrSafeArrayPtr, err := SafeArrayCreate(VT_BSTR, 1, &sab) // VT_BSTR - if err != nil { - return 0, err - } - for i, p := range params { - bstr, _ := SysAllocString(p) - SafeArrayPutElement(listStrSafeArrayPtr, int32(i), bstr) - } - - paramVariant := Variant{ - VT: VT_BSTR | VT_ARRAY, // VT_BSTR | VT_ARRAY - Val: uintptr(unsafe.Pointer(listStrSafeArrayPtr)), - } - - sab2 := SafeArrayBound{ - cElements: uint32(1), - lLbound: 0, - } - paramsSafeArrayPtr, err := SafeArrayCreate(VT_VARIANT, 1, &sab2) // VT_VARIANT - if err != nil { - return 0, err - } - err = SafeArrayPutElement(paramsSafeArrayPtr, int32(0), unsafe.Pointer(¶mVariant)) - if err != nil { - return 0, err - } - return uintptr(unsafe.Pointer(paramsSafeArrayPtr)), nil + return 0, nil } diff --git a/methodinfo.go b/methodinfo.go index 599a140..34d35ca 100644 --- a/methodinfo.go +++ b/methodinfo.go @@ -100,19 +100,39 @@ func (obj *MethodInfo) GetType(pRetVal *uintptr) uintptr { return ret } -func (obj *MethodInfo) Invoke_3(variantObj Variant, parameters uintptr, pRetVal *uintptr) uintptr { +// Invoke_3 Invokes the method or constructor reflected by this MethodInfo instance. +// virtual HRESULT __stdcall Invoke_3 ( +// /*[in]*/ VARIANT obj, +// /*[in]*/ SAFEARRAY * parameters, +// /*[out,retval]*/ VARIANT * pRetVal ) = 0; +// https://docs.microsoft.com/en-us/dotnet/api/system.reflection.methodbase.invoke?view=net-5.0 +func (obj *MethodInfo) Invoke_3(variantObj Variant, parameters *SafeArray) (err error) { debugPrint("Entering into methodinfo.Invoke_3()...") - ret, _, _ := syscall.Syscall6( + var pRetVal *Variant + hr, _, err := syscall.Syscall6( obj.vtbl.Invoke_3, 4, uintptr(unsafe.Pointer(obj)), uintptr(unsafe.Pointer(&variantObj)), - parameters, + uintptr(unsafe.Pointer(parameters)), uintptr(unsafe.Pointer(pRetVal)), 0, 0, ) - return ret + if err != syscall.Errno(0) { + err = fmt.Errorf("the MethodInfo::Invoke_3 method returned an error:\r\n%s", err) + return + } + if hr != S_OK { + err = fmt.Errorf("the Assembly::Invoke_3 method returned a non-zero HRESULT: 0x%x", hr) + return + } + if pRetVal != nil { + err = fmt.Errorf("the Assembly::Invoke_3 method returned a non-zero pRetVal: %+v", pRetVal) + return + } + err = nil + return } // GetString returns a string that represents the current object diff --git a/utils.go b/utils.go index abc0e7c..1bece2e 100644 --- a/utils.go +++ b/utils.go @@ -16,7 +16,7 @@ import ( const S_OK = 0x0 -var Debug = true +var Debug = false func checkOK(hr uintptr, caller string) error { if hr != S_OK { @@ -44,6 +44,7 @@ func expectsParams(input string) bool { return !strings.Contains(input, "Void Main()") } +// ReadUnicodeStr takes a pointer to a unicode string in memory and returns a string value func ReadUnicodeStr(ptr unsafe.Pointer) string { debugPrint("Entering into utils.ReadUnicodeStr()...") var byteVal uint16 @@ -65,3 +66,39 @@ func debugPrint(message string) { fmt.Println("[DEBUG] " + message) } } + +// PrepareParameters creates a safe array of strings (arguments) nested inside a Variant object, which is itself +// appended to the final safe array +func PrepareParameters(params []string) (*SafeArray, error) { + sab := SafeArrayBound{ + cElements: uint32(len(params)), + lLbound: 0, + } + listStrSafeArrayPtr, err := SafeArrayCreate(VT_BSTR, 1, &sab) // VT_BSTR + if err != nil { + return nil, err + } + for i, p := range params { + bstr, _ := SysAllocString(p) + SafeArrayPutElement(listStrSafeArrayPtr, int32(i), bstr) + } + + paramVariant := Variant{ + VT: VT_BSTR | VT_ARRAY, // VT_BSTR | VT_ARRAY + Val: uintptr(unsafe.Pointer(listStrSafeArrayPtr)), + } + + sab2 := SafeArrayBound{ + cElements: uint32(1), + lLbound: 0, + } + paramsSafeArrayPtr, err := SafeArrayCreate(VT_VARIANT, 1, &sab2) // VT_VARIANT + if err != nil { + return nil, err + } + err = SafeArrayPutElement(paramsSafeArrayPtr, int32(0), unsafe.Pointer(¶mVariant)) + if err != nil { + return nil, err + } + return paramsSafeArrayPtr, nil +} From e8b430ea335a91ceda6604696cd4c70fc304f615 Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Fri, 19 Mar 2021 10:56:14 -0400 Subject: [PATCH 13/31] Updated runtime enumeration --- go-clr.go | 29 +++++++++++++++------------ hresult.go | 2 ++ iclrmetahost.go | 6 +++--- iclrruntimeinfo.go | 49 +++++++++++++++++++++++++++++++++++++++++----- ienumunknown.go | 5 +++-- utils.go | 2 -- 6 files changed, 68 insertions(+), 25 deletions(-) diff --git a/go-clr.go b/go-clr.go index 0b787ca..e96b74d 100644 --- a/go-clr.go +++ b/go-clr.go @@ -14,31 +14,34 @@ import ( // GetInstallRuntimes is a wrapper function that returns an array of installed runtimes. Requires an existing ICLRMetaHost func GetInstalledRuntimes(metahost *ICLRMetaHost) ([]string, error) { var runtimes []string - installedRuntimes, err := metahost.EnumerateInstalledRuntimes() + enumICLRRuntimeInfo, err := metahost.EnumerateInstalledRuntimes() if err != nil { return runtimes, err } - var fetched = uint32(0) - var versionString string - versionStringBytes := make([]uint16, 20) - versionStringSize := uint32(len(versionStringBytes)) - var runtimeInfo *ICLRRuntimeInfo - for { - err := installedRuntimes.Next(1, unsafe.Pointer(runtimeInfo), &fetched) + var hr int + for hr != S_FALSE { + var runtimeInfo *ICLRRuntimeInfo + var fetched = uint32(0) + hr, err = enumICLRRuntimeInfo.Next(1, unsafe.Pointer(&runtimeInfo), &fetched) if err != nil { + return runtimes, fmt.Errorf("InstalledRuntimes Next Error:\r\n%s\n", err) + } + if hr == S_FALSE { break } - if ret := runtimeInfo.GetVersionString(&versionStringBytes[0], &versionStringSize); ret != S_OK { - return runtimes, fmt.Errorf("GetVersionString returned 0x%08x", ret) + // Only release if an interface pointer was returned + runtimeInfo.Release() + + version, err := runtimeInfo.GetVersionString() + if err != nil { + return runtimes, err } - versionString = syscall.UTF16ToString(versionStringBytes) - runtimes = append(runtimes, versionString) + runtimes = append(runtimes, version) } if len(runtimes) == 0 { return runtimes, fmt.Errorf("Could not find any installed runtimes") } - runtimeInfo.Release() return runtimes, err } diff --git a/hresult.go b/hresult.go index f5342ab..7cf17cf 100644 --- a/hresult.go +++ b/hresult.go @@ -1,6 +1,8 @@ package clr const ( + S_OK = 0x00 + S_FALSE = 0x01 // COR_E_SAFEARRAYRANKMISMATCH is SafeArrayRankMismatchException COR_E_SAFEARRAYRANKMISMATCH uint32 = 0x80131538 // COR_E_BADIMAGEFORMAT is BadImageFormatException diff --git a/iclrmetahost.go b/iclrmetahost.go index 2f5fcd5..18f763c 100644 --- a/iclrmetahost.go +++ b/iclrmetahost.go @@ -101,15 +101,15 @@ func (obj *ICLRMetaHost) EnumerateInstalledRuntimes() (ppEnumerator *IEnumUnknow obj.vtbl.EnumerateInstalledRuntimes, 2, uintptr(unsafe.Pointer(obj)), - uintptr(unsafe.Pointer(ppEnumerator)), + uintptr(unsafe.Pointer(&ppEnumerator)), 0, ) if err != syscall.Errno(0) { - err = fmt.Errorf("there was an error calling the ICLRMetaHost::GetRuntime method:\r\n%s", err) + err = fmt.Errorf("there was an error calling the ICLRMetaHost::EnumerateInstalledRuntimes method:\r\n%s", err) return } if hr != S_OK { - err = fmt.Errorf("the ICLRMetaHost::GetRuntime method returned a non-zero HRESULT: 0x%x", hr) + err = fmt.Errorf("the ICLRMetaHost::EnumerateInstalledRuntimes method returned a non-zero HRESULT: 0x%x", hr) return } err = nil diff --git a/iclrruntimeinfo.go b/iclrruntimeinfo.go index 55e2c33..f812c83 100644 --- a/iclrruntimeinfo.go +++ b/iclrruntimeinfo.go @@ -52,6 +52,7 @@ func (obj *ICLRRuntimeInfo) AddRef() uintptr { } func (obj *ICLRRuntimeInfo) Release() uintptr { + debugPrint("Entering into iclrruntimeinfo.Release()...") ret, _, _ := syscall.Syscall( obj.vtbl.Release, 1, @@ -61,14 +62,52 @@ func (obj *ICLRRuntimeInfo) Release() uintptr { return ret } -func (obj *ICLRRuntimeInfo) GetVersionString(pcchBuffer *uint16, pVersionstringSize *uint32) uintptr { - ret, _, _ := syscall.Syscall( +// GetVersionString gets common language runtime (CLR) version information associated with a given ICLRRuntimeInfo interface. +// HRESULT GetVersionString( +// [out, size_is(*pcchBuffer)] LPWSTR pwzBuffer, +// [in, out] DWORD *pcchBuffer); +// https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/hosting/iclrruntimeinfo-getversionstring-method +func (obj *ICLRRuntimeInfo) GetVersionString() (version string, err error) { + debugPrint("Entering into iclrruntimeinfo.GetVersion()...") + // [in, out] Specifies the size of pwzBuffer to avoid buffer overruns. If pwzBuffer is null, pchBuffer returns the required size of pwzBuffer to allow preallocation. + var pchBuffer uint32 + hr, _, err := syscall.Syscall( obj.vtbl.GetVersionString, 3, uintptr(unsafe.Pointer(obj)), - uintptr(unsafe.Pointer(pcchBuffer)), - uintptr(unsafe.Pointer(&pVersionstringSize))) - return ret + 0, + uintptr(unsafe.Pointer(&pchBuffer)), + ) + if err != syscall.Errno(0) { + err = fmt.Errorf("there was an error calling the ICLRRuntimeInfo::GetVersionString method during preallocation:\r\n%s", err) + return + } + // 0x8007007a = The data area passed to a system call is too small, expected when passing a nil buffer for preallocation + if hr != S_OK && hr != 0x8007007a { + err = fmt.Errorf("the ICLRRuntimeInfo::GetVersionString method (preallocation) returned a non-zero HRESULT: 0x%x", hr) + return + } + + pwzBuffer := make([]uint16, 20) + + hr, _, err = syscall.Syscall( + obj.vtbl.GetVersionString, + 3, + uintptr(unsafe.Pointer(obj)), + uintptr(unsafe.Pointer(&pwzBuffer[0])), + uintptr(unsafe.Pointer(&pchBuffer)), + ) + if err != syscall.Errno(0) { + err = fmt.Errorf("there was an error calling the ICLRRuntimeInfo::GetVersionString method:\r\n%s", err) + return + } + if hr != S_OK { + err = fmt.Errorf("the ICLRRuntimeInfo::GetVersionString method returned a non-zero HRESULT: 0x%x", hr) + return + } + err = nil + version = syscall.UTF16ToString(pwzBuffer) + return } // GetInterface loads the CLR into the current process and returns runtime interface pointers, diff --git a/ienumunknown.go b/ienumunknown.go index 73d1470..fd199fc 100644 --- a/ienumunknown.go +++ b/ienumunknown.go @@ -53,7 +53,7 @@ func (obj *IEnumUnknown) Release() uintptr { // ULONG *pceltFetched // ); // https://docs.microsoft.com/en-us/windows/win32/api/objidl/nf-objidl-ienumunknown-next -func (obj *IEnumUnknown) Next(celt uint32, pEnumRuntime unsafe.Pointer, pceltFetched *uint32) (err error) { +func (obj *IEnumUnknown) Next(celt uint32, pEnumRuntime unsafe.Pointer, pceltFetched *uint32) (hresult int, err error) { debugPrint("Entering into ienumunknown.Next()...") hr, _, err := syscall.Syscall6( obj.vtbl.Next, @@ -69,10 +69,11 @@ func (obj *IEnumUnknown) Next(celt uint32, pEnumRuntime unsafe.Pointer, pceltFet err = fmt.Errorf("there was an error calling the IEnumUnknown::Next method:\r\n%s", err) return } - if hr != S_OK { + if hr != S_OK && hr != S_FALSE { err = fmt.Errorf("the IEnumUnknown::Next method method returned a non-zero HRESULT: 0x%x", hr) return } err = nil + hresult = int(hr) return } diff --git a/utils.go b/utils.go index 1bece2e..493cfcb 100644 --- a/utils.go +++ b/utils.go @@ -14,8 +14,6 @@ import ( "golang.org/x/text/transform" ) -const S_OK = 0x0 - var Debug = false func checkOK(hr uintptr, caller string) error { From fed3a5eb6cd3d90f532be7fe076070dac3b7665a Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Fri, 19 Mar 2021 11:16:18 -0400 Subject: [PATCH 14/31] Fixed misuse of unsafe.Pointer --- assembly.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/assembly.go b/assembly.go index 01298c9..6d8ab6d 100644 --- a/assembly.go +++ b/assembly.go @@ -70,10 +70,6 @@ type AssemblyVtbl struct { get_GlobalAssemblyCache uintptr } -func NewAssemblyFromPtr(ppv uintptr) *Assembly { - return (*Assembly)(unsafe.Pointer(ppv)) -} - func (obj *Assembly) QueryInterface(riid *windows.GUID, ppvObject *uintptr) uintptr { ret, _, _ := syscall.Syscall( obj.vtbl.QueryInterface, From 278992d44b59e388d8d3c1fff107485f1bfeb7fa Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Fri, 19 Mar 2021 11:17:18 -0400 Subject: [PATCH 15/31] Fixed misuse of unsafe.Pointer --- ienumunknown.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ienumunknown.go b/ienumunknown.go index fd199fc..c4b43dd 100644 --- a/ienumunknown.go +++ b/ienumunknown.go @@ -22,10 +22,6 @@ type IEnumUnknownVtbl struct { Clone uintptr } -func NewIEnumUnknownFromPtr(ppv uintptr) *IEnumUnknown { - return (*IEnumUnknown)(unsafe.Pointer(ppv)) -} - func (obj *IEnumUnknown) AddRef() uintptr { ret, _, _ := syscall.Syscall( obj.vtbl.AddRef, From d5949edff0a0b18932e74b2508bc2fb54f4b8eb1 Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Fri, 19 Mar 2021 11:18:29 -0400 Subject: [PATCH 16/31] Fixed misuse of unsafe.Pointer --- variant.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/variant.go b/variant.go index f8b1626..ea28616 100644 --- a/variant.go +++ b/variant.go @@ -2,8 +2,6 @@ package clr -import "unsafe" - const ( VT_EMPTY uint16 = 0x0000 VT_NULL uint16 = 0x0001 @@ -34,7 +32,3 @@ type Variant struct { Val uintptr _ [8]byte } - -func NewVariantFromPtr(ppv uintptr) *Variant { - return (*Variant)(unsafe.Pointer(ppv)) -} From 441da9dcdf59d1583762247757f4e803bb134769 Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Fri, 19 Mar 2021 11:32:18 -0400 Subject: [PATCH 17/31] Code cleanup --- examples/DLLfromDisk/DLLfromDisk.go | 29 ++++++++++++++--------------- safearray.go | 25 +------------------------ utils.go | 5 ++++- 3 files changed, 19 insertions(+), 40 deletions(-) diff --git a/examples/DLLfromDisk/DLLfromDisk.go b/examples/DLLfromDisk/DLLfromDisk.go index d416fbf..f399794 100644 --- a/examples/DLLfromDisk/DLLfromDisk.go +++ b/examples/DLLfromDisk/DLLfromDisk.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "syscall" + "unsafe" clr "github.com/ropnop/go-clr" ) @@ -18,7 +19,7 @@ func must(err error) { func checkOK(hr uintptr, caller string) { if hr != 0x0 { - log.Fatalf("%s returned 0x%08x", hr) + log.Fatalf("%s returned 0x%x", caller, hr) } } @@ -31,28 +32,26 @@ func main() { must(err) fmt.Printf("[+] Found installed runtimes: %s\n", installedRuntimes) versionString := "v4.0.30319" - pwzVersion, _ := syscall.UTF16PtrFromString(versionString) - var pRuntimeInfo uintptr + pwzVersion, err := syscall.UTF16PtrFromString(versionString) + must(err) - hr := metahost.GetRuntime(pwzVersion, &clr.IID_ICLRRuntimeInfo, &pRuntimeInfo) - checkOK(hr, "metaHost.GetRuntime") - runtimeInfo := clr.NewICLRRuntimeInfoFromPtr(pRuntimeInfo) + runtimeInfo, err := metahost.GetRuntime(pwzVersion, clr.IID_ICLRRuntimeInfo) + must(err) fmt.Printf("[+] Using runtime: %s\n", versionString) var isLoadable bool - hr = runtimeInfo.IsLoadable(&isLoadable) - checkOK(hr, "runtimeInfo.IsLoadable") + err = runtimeInfo.IsLoadable(&isLoadable) + must(err) if !isLoadable { log.Fatal("[!] IsLoadable returned false. Bailing...") } - var pRuntimeHost uintptr - hr = runtimeInfo.GetInterface(&clr.CLSID_CLRRuntimeHost, &clr.IID_ICLRRuntimeHost, &pRuntimeHost) - checkOK(hr, "runtimeInfo.GetInterface") - runtimeHost := clr.NewICLRRuntimeHostFromPtr(pRuntimeHost) + var runtimeHost *clr.ICLRRuntimeHost + err = runtimeInfo.GetInterface(clr.CLSID_CLRRuntimeHost, clr.IID_ICLRRuntimeHost, unsafe.Pointer(&runtimeHost)) + must(err) - hr = runtimeHost.Start() - checkOK(hr, "runtimeHost.Start") + err = runtimeHost.Start() + must(err) fmt.Println("[+] Loaded CLR into this process") fmt.Println("[+] Executing assembly...") @@ -61,7 +60,7 @@ func main() { pMethodName, _ := syscall.UTF16PtrFromString("SayHello") pArgument, _ := syscall.UTF16PtrFromString("foobar") var pReturnVal *uint16 - hr = runtimeHost.ExecuteInDefaultAppDomain( + hr := runtimeHost.ExecuteInDefaultAppDomain( pDLLPath, pTypeName, pMethodName, diff --git a/safearray.go b/safearray.go index d5a6dd5..4d55842 100644 --- a/safearray.go +++ b/safearray.go @@ -87,30 +87,6 @@ func CreateSafeArray(rawBytes []byte) (*SafeArray, error) { return safeArray, nil } -// CreateEmptySafeArray is a wrapper function that takes an array type and a size and creates a safe array with corresponding -// properties. It returns a pointer to that empty array. -func CreateEmptySafeArray(arrayType uint16, size int) (unsafe.Pointer, error) { - debugPrint("Entering into safearray.CreateEmptySafeArray()...") - modOleAuto := syscall.MustLoadDLL("OleAut32.dll") - procSafeArrayCreate := modOleAuto.MustFindProc("SafeArrayCreate") - - sab := SafeArrayBound{ - cElements: uint32(size), - lLbound: 0, - } - vt := uint16(arrayType) - ret, _, err := procSafeArrayCreate.Call( - uintptr(vt), - uintptr(1), - uintptr(unsafe.Pointer(&sab))) - - if err != syscall.Errno(0) { - return nil, err - } - - return unsafe.Pointer(ret), nil -} - // SafeArrayCreate creates a new array descriptor, allocates and initializes the data for the array, and returns a pointer to the new array descriptor. // SAFEARRAY * SafeArrayCreate( // VARTYPE vt, @@ -168,6 +144,7 @@ func SysAllocString(str string) (unsafe.Pointer, error) { return nil, err } // TODO Return a pointer to a BSTR instead of an unsafe.Pointer + // Unable to avoid misuse of unsafe.Pointer because the Windows API call returns the safeArray pointer in the "ret" value. This is a go vet false positive return unsafe.Pointer(ret), nil } diff --git a/utils.go b/utils.go index 493cfcb..79ff183 100644 --- a/utils.go +++ b/utils.go @@ -77,7 +77,10 @@ func PrepareParameters(params []string) (*SafeArray, error) { return nil, err } for i, p := range params { - bstr, _ := SysAllocString(p) + bstr, err := SysAllocString(p) + if err != nil { + return nil, err + } SafeArrayPutElement(listStrSafeArrayPtr, int32(i), bstr) } From 3c6feff2a0d89184019cd8c75b9a36628d53f981 Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Fri, 19 Mar 2021 15:11:59 -0400 Subject: [PATCH 18/31] Documentation and cleanup --- appdomain.go | 25 +++++-- assembly.go | 2 + examples/DLLfromDisk/DLLfromDisk.go | 29 +++++---- examples/EXEfromMemory/EXEfromMemory.go | 3 +- go-clr.go | 38 ++++++----- iclrmetahost.go | 50 +++++++++----- iclrruntimehost.go | 87 +++++++++++++++++++------ iclrruntimeinfo.go | 63 +++++++++++++----- icorruntimehost.go | 68 +++++++++++++------ ienumunknown.go | 15 +++-- iunknown.go | 56 ++++++++++++---- methodinfo.go | 17 ++--- utils.go | 2 + variant.go | 6 +- 14 files changed, 324 insertions(+), 137 deletions(-) diff --git a/appdomain.go b/appdomain.go index 424ca3d..46b232a 100644 --- a/appdomain.go +++ b/appdomain.go @@ -10,10 +10,18 @@ import ( "golang.org/x/sys/windows" ) +// AppDomain is a Windows COM object interface pointer for the .NET AppDomain class. +// The AppDomain object represents an application domain, which is an isolated environment where applications execute. +// This structure only contains a pointer to the AppDomain's virtual function table +// https://docs.microsoft.com/en-us/dotnet/api/system.appdomain?view=netframework-4.8 type AppDomain struct { vtbl *AppDomainVtbl } +// AppDomainVtbl is a Virtual Function Table for the AppDomain COM interface +// The Virtual Function Table contains pointers to the COM IUnkown interface +// functions (QueryInterface, AddRef, & Release) as well as the AppDomain object's methods +// https://docs.microsoft.com/en-us/dotnet/api/system.appdomain?view=netframework-4.8 type AppDomainVtbl struct { QueryInterface uintptr AddRef uintptr @@ -99,6 +107,7 @@ func GetAppDomain(runtimeHost *ICORRuntimeHost) (appDomain *AppDomain, err error } func (obj *AppDomain) QueryInterface(riid *windows.GUID, ppvObject *uintptr) uintptr { + debugPrint("Entering into appdomain.QueryInterface()...") ret, _, _ := syscall.Syscall( obj.vtbl.QueryInterface, 3, @@ -128,14 +137,22 @@ func (obj *AppDomain) Release() uintptr { return ret } -func (obj *AppDomain) GetHashCode() uintptr { - ret, _, _ := syscall.Syscall( +// GetHashCode serves as the default hash function. +// https://docs.microsoft.com/en-us/dotnet/api/system.object.gethashcode?view=netframework-4.8#System_Object_GetHashCode +func (obj *AppDomain) GetHashCode() (int32, error) { + debugPrint("Entering into appdomain.GetHashCode()...") + ret, _, err := syscall.Syscall( obj.vtbl.GetHashCode, 2, uintptr(unsafe.Pointer(obj)), 0, - 0) - return ret + 0, + ) + if err != syscall.Errno(0) { + return 0, fmt.Errorf("the appdomain.GetHashCode function returned an error:\r\n%s", err) + } + // Unable to avoid misuse of unsafe.Pointer because the Windows API call returns the safeArray pointer in the "ret" value. This is a go vet false positive + return int32(ret), nil } // Load_3 Loads an Assembly into this application domain. diff --git a/assembly.go b/assembly.go index 6d8ab6d..0e9c693 100644 --- a/assembly.go +++ b/assembly.go @@ -16,6 +16,8 @@ type Assembly struct { vtbl *AssemblyVtbl } +// AssemblyVtbl is a COM virtual table of functions for the Assembly Class +// https://docs.microsoft.com/en-us/dotnet/api/system.reflection.assembly?view=netframework-4.8 type AssemblyVtbl struct { QueryInterface uintptr AddRef uintptr diff --git a/examples/DLLfromDisk/DLLfromDisk.go b/examples/DLLfromDisk/DLLfromDisk.go index f399794..c9f7a06 100644 --- a/examples/DLLfromDisk/DLLfromDisk.go +++ b/examples/DLLfromDisk/DLLfromDisk.go @@ -24,7 +24,7 @@ func checkOK(hr uintptr, caller string) { } func main() { - metahost, err := clr.GetICLRMetaHost() + metahost, err := clr.CLRCreateInstance(clr.CLSID_CLRMetaHost, clr.IID_ICLRMetaHost) must(err) fmt.Println("[+] Got metahost") @@ -39,8 +39,7 @@ func main() { must(err) fmt.Printf("[+] Using runtime: %s\n", versionString) - var isLoadable bool - err = runtimeInfo.IsLoadable(&isLoadable) + isLoadable, err := runtimeInfo.IsLoadable() must(err) if !isLoadable { log.Fatal("[!] IsLoadable returned false. Bailing...") @@ -55,18 +54,22 @@ func main() { fmt.Println("[+] Loaded CLR into this process") fmt.Println("[+] Executing assembly...") - pDLLPath, _ := syscall.UTF16PtrFromString("TestDLL.dll") - pTypeName, _ := syscall.UTF16PtrFromString("TestDLL.HelloWorld") - pMethodName, _ := syscall.UTF16PtrFromString("SayHello") - pArgument, _ := syscall.UTF16PtrFromString("foobar") - var pReturnVal *uint16 - hr := runtimeHost.ExecuteInDefaultAppDomain( + pDLLPath, err := syscall.UTF16PtrFromString("TestDLL.dll") + must(err) + pTypeName, err := syscall.UTF16PtrFromString("TestDLL.HelloWorld") + must(err) + pMethodName, err := syscall.UTF16PtrFromString("SayHello") + must(err) + pArgument, err := syscall.UTF16PtrFromString("foobar") + must(err) + ret, err := runtimeHost.ExecuteInDefaultAppDomain( pDLLPath, pTypeName, pMethodName, pArgument, - pReturnVal) - - checkOK(hr, "runtimeHost.ExecuteInDefaultAppDomain") - fmt.Printf("[+] Assembly returned: 0x%x\n", pReturnVal) + ) + if *ret != 0 { + err = fmt.Errorf("the ICLRRuntimeHost::ExecuteInDefaultAppDomain method returned a non-zero return value: %d", *ret) + } + must(err) } diff --git a/examples/EXEfromMemory/EXEfromMemory.go b/examples/EXEfromMemory/EXEfromMemory.go index 1c6f101..04da98f 100644 --- a/examples/EXEfromMemory/EXEfromMemory.go +++ b/examples/EXEfromMemory/EXEfromMemory.go @@ -54,8 +54,7 @@ func main() { runtimeInfo, err := metaHost.GetRuntime(pwzVersion, clr.IID_ICLRRuntimeInfo) must(err) - var isLoadable bool - err = runtimeInfo.IsLoadable(&isLoadable) + isLoadable, err := runtimeInfo.IsLoadable() must(err) if !isLoadable { log.Fatal("[!] IsLoadable returned false. Bailing...") diff --git a/go-clr.go b/go-clr.go index e96b74d..70eed3d 100644 --- a/go-clr.go +++ b/go-clr.go @@ -53,7 +53,7 @@ func ExecuteDLLFromDisk(targetRuntime, dllpath, typeName, methodName, argument s if targetRuntime == "" { targetRuntime = "v4" } - metahost, err := GetICLRMetaHost() + metahost, err := CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost) if err != nil { return } @@ -75,8 +75,8 @@ func ExecuteDLLFromDisk(targetRuntime, dllpath, typeName, methodName, argument s if err != nil { return } - var isLoadable bool - err = runtimeInfo.IsLoadable(&isLoadable) + + isLoadable, err := runtimeInfo.IsLoadable() if err != nil { return } @@ -88,21 +88,25 @@ func ExecuteDLLFromDisk(targetRuntime, dllpath, typeName, methodName, argument s return } - pDLLPath, _ := syscall.UTF16PtrFromString(dllpath) - pTypeName, _ := syscall.UTF16PtrFromString(typeName) - pMethodName, _ := syscall.UTF16PtrFromString(methodName) - pArgument, _ := syscall.UTF16PtrFromString(argument) - var pReturnVal uint16 - hr := runtimeHost.ExecuteInDefaultAppDomain(pDLLPath, pTypeName, pMethodName, pArgument, &pReturnVal) - err = checkOK(hr, "runtimeHost.ExecuteInDefaultAppDomain") - if err != nil { - return int16(pReturnVal), err + pDLLPath, err := syscall.UTF16PtrFromString(dllpath) + must(err) + pTypeName, err := syscall.UTF16PtrFromString(typeName) + must(err) + pMethodName, err := syscall.UTF16PtrFromString(methodName) + must(err) + pArgument, err := syscall.UTF16PtrFromString(argument) + must(err) + + ret, err := runtimeHost.ExecuteInDefaultAppDomain(pDLLPath, pTypeName, pMethodName, pArgument) + must(err) + if *ret != 0 { + return int16(*ret), fmt.Errorf("the ICLRRuntimeHost::ExecuteInDefaultAppDomain method returned a non-zero return value: %d", *ret) } + runtimeHost.Release() runtimeInfo.Release() metahost.Release() - return int16(pReturnVal), nil - + return 0, nil } // ExecuteByteArray is a wrapper function that will automatically loads the supplied target framework into the current @@ -114,7 +118,7 @@ func ExecuteByteArray(targetRuntime string, rawBytes []byte, params []string) (r if targetRuntime == "" { targetRuntime = "v4" } - metahost, err := GetICLRMetaHost() + metahost, err := CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost) if err != nil { return } @@ -136,8 +140,8 @@ func ExecuteByteArray(targetRuntime string, rawBytes []byte, params []string) (r if err != nil { return } - var isLoadable bool - err = runtimeInfo.IsLoadable(&isLoadable) + + isLoadable, err := runtimeInfo.IsLoadable() if err != nil { return } diff --git a/iclrmetahost.go b/iclrmetahost.go index 18f763c..43a93a7 100644 --- a/iclrmetahost.go +++ b/iclrmetahost.go @@ -18,17 +18,37 @@ type ICLRMetaHost struct { vtbl *ICLRMetaHostVtbl } +// ICLRMetaHostVtbl provides methods that return a specific version of the common language runtime (CLR) +// based on its version number, list all installed CLRs, list all runtimes that are loaded in a specified +// process, discover the CLR version used to compile an assembly, exit a process with a clean runtime +// shutdown, and query legacy API binding. +// https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/hosting/iclrmetahost-interface type ICLRMetaHostVtbl struct { - QueryInterface uintptr - AddRef uintptr - Release uintptr - GetRuntime uintptr - GetVersionFromFile uintptr - EnumerateInstalledRuntimes uintptr - EnumerateLoadedRuntimes uintptr + QueryInterface uintptr + AddRef uintptr + Release uintptr + // GetRuntime gets the ICLRRuntimeInfo interface that corresponds to a particular CLR version. + // This method supersedes the CorBindToRuntimeEx function used with the STARTUP_LOADER_SAFEMODE flag. + GetRuntime uintptr + // GetVersionFromFile gets the assembly's original .NET Framework compilation version (stored in the metadata), + // given its file path. This method supersedes GetFileVersion. + GetVersionFromFile uintptr + // EnumerateInstalledRuntimes returns an enumeration that contains a valid ICLRRuntimeInfo interface + // pointer for each CLR version that is installed on a computer. + EnumerateInstalledRuntimes uintptr + // EnumerateLoadedRuntimes returns an enumeration that contains a valid ICLRRuntimeInfo interface + // pointer for each CLR that is loaded in a given process. This method supersedes GetVersionFromProcess. + EnumerateLoadedRuntimes uintptr + // RequestRuntimeLoadedNotification guarantees a callback to the specified function pointer when a + // CLR version is first loaded, but not yet started. This method supersedes LockClrVersion RequestRuntimeLoadedNotification uintptr - QueryLegacyV2RuntimeBinding uintptr - ExitProcess uintptr + // QueryLegacyV2RuntimeBinding returns an interface that represents a runtime to which legacy activation policy + // has been bound, for example by using the useLegacyV2RuntimeActivationPolicy attribute on the Element + // configuration file entry, by direct use of the legacy activation APIs, or by calling the + // ICLRRuntimeInfo::BindAsLegacyV2Runtime method. + QueryLegacyV2RuntimeBinding uintptr + // ExitProcess attempts to shut down all loaded runtimes gracefully and then terminates the process. + ExitProcess uintptr } // CLRCreateInstance provides one of three interfaces: ICLRMetaHost, ICLRMetaHostPolicy, or ICLRDebugging. @@ -50,12 +70,16 @@ func CLRCreateInstance(clsid, riid windows.GUID) (ppInterface *ICLRMetaHost, err procCLRCreateInstance := modMSCoree.MustFindProc("CLRCreateInstance") // For some reason this procedure call returns "The specified procedure could not be found." even though it works - hr, _, _ := procCLRCreateInstance.Call( + hr, _, err := procCLRCreateInstance.Call( uintptr(unsafe.Pointer(&clsid)), uintptr(unsafe.Pointer(&riid)), uintptr(unsafe.Pointer(&ppInterface)), ) + if err != nil { + // TODO Figure out why "The specified procedure could not be found." is returned even though everything works fine? + debugPrint(fmt.Sprintf("the mscoree!CLRCreateInstance function returned an error:\r\n%s", err)) + } if hr != S_OK { err = fmt.Errorf("the mscoree!CLRCreateInstance function returned a non-zero HRESULT: 0x%x", hr) return @@ -64,12 +88,6 @@ func CLRCreateInstance(clsid, riid windows.GUID) (ppInterface *ICLRMetaHost, err return } -// GetICLRMetaHost is a wrapper function to create and return an ICLRMetahost object -func GetICLRMetaHost() (metahost *ICLRMetaHost, err error) { - // TODO Get rid of ^ function - return CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost) -} - func (obj *ICLRMetaHost) AddRef() uintptr { ret, _, _ := syscall.Syscall( obj.vtbl.AddRef, diff --git a/iclrruntimehost.go b/iclrruntimehost.go index 0b6e29c..32e76da 100644 --- a/iclrruntimehost.go +++ b/iclrruntimehost.go @@ -12,18 +12,32 @@ type ICLRRuntimeHost struct { vtbl *ICLRRuntimeHostVtbl } +// ICLRRuntimeHostVtbl provides functionality similar to that of the ICorRuntimeHost interface +// provided in the .NET Framework version 1, with the following changes +// https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/hosting/iclrruntimehost-interface type ICLRRuntimeHostVtbl struct { - QueryInterface uintptr - AddRef uintptr - Release uintptr - Start uintptr - Stop uintptr - SetHostControl uintptr - GetCLRControl uintptr - UnloadAppDomain uintptr - ExecuteInAppDomain uintptr - GetCurrentAppDomainId uintptr - ExecuteApplication uintptr + QueryInterface uintptr + AddRef uintptr + Release uintptr + // Start Initializes the CLR into a process. + Start uintptr + // Stop Stops the execution of code by the runtime. + Stop uintptr + // SetHostControl sets the host control interface. You must call SetHostControl before calling Start. + SetHostControl uintptr + // GetCLRControl gets an interface pointer of type ICLRControl that hosts can use to customize + // aspects of the common language runtime (CLR). + GetCLRControl uintptr + // UnloadAppDomain Unloads the AppDomain that corresponds to the specified numeric identifier. + UnloadAppDomain uintptr + // ExecuteInAppDomain Specifies the AppDomain in which to execute the specified managed code. + ExecuteInAppDomain uintptr + // GetCurrentAppDomainID gets the numeric identifier of the AppDomain that is currently executing. + GetCurrentAppDomainId uintptr + // ExecuteApplication used in manifest-based ClickOnce deployment scenarios to specify the application + // to be activated in a new domain. + ExecuteApplication uintptr + // ExecuteInDefaultAppDomain Invokes the specified method of the specified type in the specified assembly. ExecuteInDefaultAppDomain uintptr } @@ -82,8 +96,19 @@ func (obj *ICLRRuntimeHost) Start() error { return nil } -func (obj *ICLRRuntimeHost) ExecuteInDefaultAppDomain(pwzAssemblyPath, pwzTypeName, pwzMethodName, pwzArgument, pReturnValue *uint16) uintptr { - ret, _, _ := syscall.Syscall9( +// ExecuteInDefaultAppDomain Calls the specified method of the specified type in the specified managed assembly. +// HRESULT ExecuteInDefaultAppDomain ( +// [in] LPCWSTR pwzAssemblyPath, +// [in] LPCWSTR pwzTypeName, +// [in] LPCWSTR pwzMethodName, +// [in] LPCWSTR pwzArgument, +// [out] DWORD *pReturnValue +// ); +// An LPCWSTR is a 32-bit pointer to a constant string of 16-bit Unicode characters, which MAY be null-terminated. +// Use syscall.UTF16PtrFromString to turn a string into a LPCWSTR +// https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/hosting/iclrruntimehost-executeindefaultappdomain-method +func (obj *ICLRRuntimeHost) ExecuteInDefaultAppDomain(pwzAssemblyPath, pwzTypeName, pwzMethodName, pwzArgument *uint16) (pReturnValue *uint32, err error) { + hr, _, err := syscall.Syscall9( obj.vtbl.ExecuteInDefaultAppDomain, 6, uintptr(unsafe.Pointer(obj)), @@ -95,15 +120,39 @@ func (obj *ICLRRuntimeHost) ExecuteInDefaultAppDomain(pwzAssemblyPath, pwzTypeNa 0, 0, 0) - return ret + if err != syscall.Errno(0) { + err = fmt.Errorf("the ICLRRuntimeHost::ExecuteInDefaultAppDomain method returned an error:\r\n%s", err) + return + } + if hr != S_OK { + err = fmt.Errorf("the ICLRRuntimeHost::ExecuteInDefaultAppDomain method returned a non-zero HRESULT: 0x%x", hr) + return + } + err = nil + return } -func (obj *ICLRRuntimeHost) GetCurrentAppDomainID(pdwAppDomainId *uint16) uintptr { - ret, _, _ := syscall.Syscall( +// GetCurrentAppDomainID Gets the numeric identifier of the AppDomain that is currently executing. +// HRESULT GetCurrentAppDomainId( +// [out] DWORD* pdwAppDomainId +// ); +// https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/hosting/iclrruntimehost-getcurrentappdomainid-method +func (obj *ICLRRuntimeHost) GetCurrentAppDomainID() (pdwAppDomainId uint32, err error) { + hr, _, err := syscall.Syscall( obj.vtbl.GetCurrentAppDomainId, 2, uintptr(unsafe.Pointer(obj)), - uintptr(unsafe.Pointer(pdwAppDomainId)), - 0) - return ret + uintptr(unsafe.Pointer(&pdwAppDomainId)), + 0, + ) + if err != syscall.Errno(0) { + err = fmt.Errorf("the ICLRRuntimeHost::GetCurrentAppDomainID method returned an error:\r\n%s", err) + return + } + if hr != S_OK { + err = fmt.Errorf("the ICLRRuntimeHost::GetCurrentAppDomainID method returned a non-zero HRESULT: 0x%x", hr) + return + } + err = nil + return } diff --git a/iclrruntimeinfo.go b/iclrruntimeinfo.go index f812c83..0d35a2e 100644 --- a/iclrruntimeinfo.go +++ b/iclrruntimeinfo.go @@ -14,22 +14,46 @@ type ICLRRuntimeInfo struct { vtbl *ICLRRuntimeInfoVtbl } +// ICLRRuntimeInfoVtbl Provides methods that return information about a specific common language runtime (CLR), +// including version, directory, and load status. This interface also provides runtime-specific functionality +// without initializing the runtime. It includes the runtime-relative LoadLibrary method, the runtime +// module-specific GetProcAddress method, and runtime-provided interfaces through the GetInterface method. +// https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/hosting/iclrruntimeinfo-interface type ICLRRuntimeInfoVtbl struct { - QueryInterface uintptr - AddRef uintptr - Release uintptr - GetVersionString uintptr - GetRuntimeDirectory uintptr - IsLoaded uintptr - LoadErrorString uintptr - LoadLibrary uintptr - GetProcAddress uintptr - GetInterface uintptr - IsLoadable uintptr + QueryInterface uintptr + AddRef uintptr + Release uintptr + // GetVersionString Gets common language runtime (CLR) version information associated with a given + // ICLRRuntimeInfo interface. This method supersedes the GetRequestedRuntimeInfo and GetRequestedRuntimeVersion methods. + GetVersionString uintptr + // GetRuntimeDirectory Gets the installation directory of the CLR associated with this interface. + // This method supersedes the GetCORSystemDirectory method. + GetRuntimeDirectory uintptr + // IsLoaded Indicates whether the CLR associated with the ICLRRuntimeInfo interface is loaded into a process. + IsLoaded uintptr + // LoadErrorString Translates an HRESULT value into an appropriate error message for the specified culture. + // This method supersedes the LoadStringRC and LoadStringRCEx methods. + LoadErrorString uintptr + // LoadLibrary Loads a library from the framework directory of the CLR represented by an ICLRRuntimeInfo interface. + // This method supersedes the LoadLibraryShim method. + LoadLibrary uintptr + // GetProcAddress Gets the address of a specified function that was exported from the CLR associated with + // this interface. This method supersedes the GetRealProcAddress method. + GetProcAddress uintptr + // GetInterface Loads the CLR into the current process and returns runtime interface pointers, + // such as ICLRRuntimeHost, ICLRStrongName and IMetaDataDispenser. This method supersedes all the CorBindTo* functions. + GetInterface uintptr + // IsLoadable Indicates whether the runtime associated with this interface can be loaded into the current + // process, taking into account other runtimes that might already be loaded into the process. + IsLoadable uintptr + // SetDefaultStartupFlags Sets the CLR startup flags and host configuration file. SetDefaultStartupFlags uintptr + // GetDefaultStartupFlags Gets the CLR startup flags and host configuration file. GetDefaultStartupFlags uintptr - BindAsLegacyV2Runtime uintptr - IsStarted uintptr + // BindAsLegacyV2Runtime Binds this runtime for all legacy CLR version 2 activation policy decisions. + BindAsLegacyV2Runtime uintptr + // IsStarted Indicates whether the CLR that is associated with the ICLRRuntimeInfo interface has been started. + IsStarted uintptr } // GetRuntimeInfo is a wrapper function to return an ICLRRuntimeInfo from a standard version string @@ -166,19 +190,22 @@ func (obj *ICLRRuntimeInfo) BindAsLegacyV2Runtime() error { // HRESULT IsLoadable( // [out, retval] BOOL *pbLoadable); // https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/hosting/iclrruntimeinfo-isloadable-method -func (obj *ICLRRuntimeInfo) IsLoadable(pbLoadable *bool) error { +func (obj *ICLRRuntimeInfo) IsLoadable() (pbLoadable bool, err error) { debugPrint("Entering into iclrruntimeinfo.IsLoadable()...") hr, _, err := syscall.Syscall( obj.vtbl.IsLoadable, 2, uintptr(unsafe.Pointer(obj)), - uintptr(unsafe.Pointer(pbLoadable)), + uintptr(unsafe.Pointer(&pbLoadable)), 0) if err != syscall.Errno(0) { - return fmt.Errorf("the ICLRRuntimeInfo::IsLoadable method returned an error:\r\n%s", err) + err = fmt.Errorf("the ICLRRuntimeInfo::IsLoadable method returned an error:\r\n%s", err) + return } if hr != S_OK { - return fmt.Errorf("the ICLRRuntimeInfo::IsLoadable method returned a non-zero HRESULT: 0x%x", hr) + err = fmt.Errorf("the ICLRRuntimeInfo::IsLoadable method returned a non-zero HRESULT: 0x%x", hr) + return } - return nil + err = nil + return } diff --git a/icorruntimehost.go b/icorruntimehost.go index dc2c92d..cdd2631 100644 --- a/icorruntimehost.go +++ b/icorruntimehost.go @@ -12,29 +12,55 @@ type ICORRuntimeHost struct { vtbl *ICORRuntimeHostVtbl } +// ICORRuntimeHos Provides methods that enable the host to start and stop the common language runtime (CLR) +// explicitly, to create and configure application domains, to access the default domain, and to enumerate all +// domains running in the process. type ICORRuntimeHostVtbl struct { - QueryInterface uintptr - AddRef uintptr - Release uintptr - CreateLogicalThreadState uintptr - DeleteLogicalThreadState uintptr - SwitchInLogicalThreadState uintptr - SwitchOutLogicalThreadState uintptr + QueryInterface uintptr + AddRef uintptr + Release uintptr + // CreateLogicalThreadState Do not use. + CreateLogicalThreadState uintptr + // DeleteLogicalThreadSate Do not use. + DeleteLogicalThreadState uintptr + // SwitchInLogicalThreadState Do not use. + SwitchInLogicalThreadState uintptr + // SwitchOutLogicalThreadState Do not use. + SwitchOutLogicalThreadState uintptr + // LocksHeldByLogicalThreadState Do not use. LocksHeldByLogicalThreadState uintptr - MapFile uintptr - GetConfiguration uintptr - Start uintptr - Stop uintptr - CreateDomain uintptr - GetDefaultDomain uintptr - EnumDomains uintptr - NextDomain uintptr - CloseEnum uintptr - CreateDomainEx uintptr - CreateDomainSetup uintptr - CreateEvidence uintptr - UnloadDomain uintptr - CurrentDomain uintptr + // MapFile Maps the specified file into memory. This method is obsolete. + MapFile uintptr + // GetConfiguration Gets an object that allows the host to specify the callback configuration of the CLR. + GetConfiguration uintptr + // Start Starts the CLR. + Start uintptr + // Stop Stops the execution of code in the runtime for the current process. + Stop uintptr + // CreateDomain Creates an application domain. The caller receives an interface pointer of + // type _AppDomain to an instance of type System.AppDomain. + CreateDomain uintptr + // GetDefaultDomain Gets an interface pointer of type _AppDomain that represents the default domain for the current process. + GetDefaultDomain uintptr + // EnumDomains Gets an enumerator for the domains in the current process. + EnumDomains uintptr + // NextDomain Gets an interface pointer to the next domain in the enumeration. + NextDomain uintptr + // CloseEnum Resets a domain enumerator back to the beginning of the domain list. + CloseEnum uintptr + // CreateDomainEx Creates an application domain. This method allows the caller to pass an + // IAppDomainSetup instance to configure additional features of the returned _AppDomain instance. + CreateDomainEx uintptr + // CreateDomainSetup Gets an interface pointer of type IAppDomainSetup to an AppDomainSetup instance. + // IAppDomainSetup provides methods to configure aspects of an application domain before it is created. + CreateDomainSetup uintptr + // CreateEvidence Gets an interface pointer of type IIdentity, which allows the host to create security + // evidence to pass to CreateDomain or CreateDomainEx. + CreateEvidence uintptr + // UnloadDomain Unloads the specified application domain from the current process. + UnloadDomain uintptr + // CurrentDomain Gets an interface pointer of type _AppDomain that represents the domain loaded on the current thread. + CurrentDomain uintptr } // GetICORRuntimeHost is a wrapper function that takes in an ICLRRuntimeInfo and returns an ICORRuntimeHost object diff --git a/ienumunknown.go b/ienumunknown.go index c4b43dd..bd78c9e 100644 --- a/ienumunknown.go +++ b/ienumunknown.go @@ -12,14 +12,21 @@ type IEnumUnknown struct { vtbl *IEnumUnknownVtbl } +// IEnumUnknownVtbl Enumerates objects implementing the root COM interface, IUnknown. +// Commonly implemented by a component containing multiple objects. For more information, see IEnumUnknown. +// https://docs.microsoft.com/en-us/windows/win32/api/objidl/nn-objidl-ienumunknown type IEnumUnknownVtbl struct { QueryInterface uintptr AddRef uintptr Release uintptr - Next uintptr - Skip uintptr - Reset uintptr - Clone uintptr + // Next Retrieves the specified number of items in the enumeration sequence. + Next uintptr + // Skip Skips over the specified number of items in the enumeration sequence. + Skip uintptr + // Reset Resets the enumeration sequence to the beginning. + Reset uintptr + // Clone Creates a new enumerator that contains the same enumeration state as the current one. + Clone uintptr } func (obj *IEnumUnknown) AddRef() uintptr { diff --git a/iunknown.go b/iunknown.go index f82f582..d0e8adb 100644 --- a/iunknown.go +++ b/iunknown.go @@ -14,10 +14,19 @@ type IUnknown struct { vtbl *IUnknownVtbl } +// IUnknownVtbl Enables clients to get pointers to other interfaces on a given object through the +// QueryInterface method, and manage the existence of the object through the AddRef and Release methods. +// All other COM interfaces are inherited, directly or indirectly, from IUnknown. Therefore, the three +// methods in IUnknown are the first entries in the vtable for every interface. +// https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nn-unknwn-iunknown type IUnknownVtbl struct { + // QueryInterface Retrieves pointers to the supported interfaces on an object. QueryInterface uintptr - AddRef uintptr - Release uintptr + // AddRef Increments the reference count for an interface pointer to a COM object. + // You should call this method whenever you make a copy of an interface pointer. + AddRef uintptr + // Release Decrements the reference count for an interface on a COM object. + Release uintptr } // QueryInterface queries a COM object for a pointer to one of its interface; @@ -38,30 +47,53 @@ func (obj *IUnknown) QueryInterface(riid windows.GUID, ppvObject unsafe.Pointer) uintptr(ppvObject), ) if err != syscall.Errno(0) { - return fmt.Errorf("the ICORRuntimeHost::GetDefaultDomain method returned an error:\r\n%s", err) + return fmt.Errorf("the IUknown::QueryInterface method returned an error:\r\n%s", err) } if hr != S_OK { - return fmt.Errorf("the ICORRuntimeHost::GetDefaultDomain method method returned a non-zero HRESULT: 0x%x", hr) + return fmt.Errorf("the IUknown::QueryInterface method method returned a non-zero HRESULT: 0x%x", hr) } return nil } -func (obj *IUnknown) AddRef() uintptr { - ret, _, _ := syscall.Syscall( +// AddRef Increments the reference count for an interface pointer to a COM object. +// You should call this method whenever you make a copy of an interface pointer +// ULONG AddRef(); +// https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-addref +func (obj *IUnknown) AddRef() (count uint32, err error) { + debugPrint("Entering into iunknown.AddRef()...") + ret, _, err := syscall.Syscall( obj.vtbl.AddRef, 1, uintptr(unsafe.Pointer(obj)), 0, - 0) - return ret + 0, + ) + if err != syscall.Errno(0) { + return 0, fmt.Errorf("the IUnknown::AddRef method returned an error:\r\n%s", err) + } + err = nil + // Unable to avoid misuse of unsafe.Pointer because the Windows API call returns the safeArray pointer in the "ret" value. This is a go vet false positive + count = *(*uint32)(unsafe.Pointer(ret)) + return } -func (obj *IUnknown) Release() uintptr { - ret, _, _ := syscall.Syscall( +// Release Decrements the reference count for an interface on a COM object. +// ULONG Release(); +// https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-release +func (obj *IUnknown) Release() (count uint32, err error) { + debugPrint("Entering into iunknown.Release()...") + ret, _, err := syscall.Syscall( obj.vtbl.Release, 1, uintptr(unsafe.Pointer(obj)), 0, - 0) - return ret + 0, + ) + if err != syscall.Errno(0) { + return 0, fmt.Errorf("the IUnknown::Release method returned an error:\r\n%s", err) + } + err = nil + // Unable to avoid misuse of unsafe.Pointer because the Windows API call returns the safeArray pointer in the "ret" value. This is a go vet false positive + count = *(*uint32)(unsafe.Pointer(ret)) + return } diff --git a/methodinfo.go b/methodinfo.go index 34d35ca..696c799 100644 --- a/methodinfo.go +++ b/methodinfo.go @@ -16,6 +16,12 @@ type MethodInfo struct { vtbl *MethodInfoVtbl } +// MethodInfoVtbl Discovers the attributes of a method and provides access to method metadata. +// Inheritance: Object -> MemberInfo -> MethodBase -> MethodInfo +// MethodInfo Class: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.methodinfo?view=net-5.0 +// MethodBase Class: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.methodbase?view=net-5.0 +// MemberInfo Class: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.memberinfo?view=net-5.0 +// Object Class: https://docs.microsoft.com/en-us/dotnet/api/system.object?view=net-5.0 type MethodInfoVtbl struct { QueryInterface uintptr AddRef uintptr @@ -90,16 +96,6 @@ func (obj *MethodInfo) Release() uintptr { return ret } -func (obj *MethodInfo) GetType(pRetVal *uintptr) uintptr { - ret, _, _ := syscall.Syscall( - obj.vtbl.GetType, - 2, - uintptr(unsafe.Pointer(obj)), - uintptr(unsafe.Pointer(pRetVal)), - 0) - return ret -} - // Invoke_3 Invokes the method or constructor reflected by this MethodInfo instance. // virtual HRESULT __stdcall Invoke_3 ( // /*[in]*/ VARIANT obj, @@ -138,6 +134,7 @@ func (obj *MethodInfo) Invoke_3(variantObj Variant, parameters *SafeArray) (err // GetString returns a string that represents the current object // a string version of the method's signature // public virtual string ToString (); +// https://docs.microsoft.com/en-us/dotnet/api/system.object.tostring?view=net-5.0#System_Object_ToString func (obj *MethodInfo) GetString() (str string, err error) { debugPrint("Entering into methodinfo.GetString()...") var object *string diff --git a/utils.go b/utils.go index 79ff183..52e4300 100644 --- a/utils.go +++ b/utils.go @@ -16,6 +16,7 @@ import ( var Debug = false +// checkOK evaluates a HRESULT code for a caller and determines if there was an error func checkOK(hr uintptr, caller string) error { if hr != S_OK { return fmt.Errorf("%s returned 0x%08x", caller, hr) @@ -24,6 +25,7 @@ func checkOK(hr uintptr, caller string) error { } } +// must forces the program to exit if there is an error using the log.Fatal command func must(err error) { if err != nil { log.Fatal(err) diff --git a/variant.go b/variant.go index ea28616..8798013 100644 --- a/variant.go +++ b/variant.go @@ -3,8 +3,12 @@ package clr const ( + // VT_EMPTY No value was specified. If an optional argument to an Automation method is left blank, do not + // pass a VARIANT of type VT_EMPTY. Instead, pass a VARIANT of type VT_ERROR with a value of DISP_E_PARAMNOTFOUND. VT_EMPTY uint16 = 0x0000 - VT_NULL uint16 = 0x0001 + // VT_NULL A propagating null value was specified. (This should not be confused with the null pointer.) + // The null value is used for tri-state logic, as with SQL. + VT_NULL uint16 = 0x0001 // VT_UI1 is a Variant Type of Unsigned Integer of 1-byte VT_UI1 uint16 = 0x0011 // VT_UT4 is a Varriant Type of Unsigned Integer of 4-byte From 85f36cda8005cec6625b6e673b7bb9e81952b956 Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Thu, 25 Mar 2021 15:56:08 -0400 Subject: [PATCH 19/31] Added C2 Framework Support --- appdomain.go | 26 ++++ examples/C2Framework/C2Framework.go | 218 ++++++++++++++++++++++++++++ go-clr.go | 173 ++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + guids.go | 4 + hresult.go | 10 ++ iclrmetahost.go | 18 +++ iclrruntimeinfo.go | 2 +- icorruntimehost.go | 56 ++++++- ierrorinfo.go | 117 +++++++++++++++ io.go | 151 +++++++++++++++++++ isupporterrorinfo.go | 123 ++++++++++++++++ methodinfo.go | 58 +++++++- 14 files changed, 952 insertions(+), 7 deletions(-) create mode 100644 examples/C2Framework/C2Framework.go create mode 100644 ierrorinfo.go create mode 100644 io.go create mode 100644 isupporterrorinfo.go diff --git a/appdomain.go b/appdomain.go index 46b232a..1b17413 100644 --- a/appdomain.go +++ b/appdomain.go @@ -182,3 +182,29 @@ func (obj *AppDomain) Load_3(rawAssembly *SafeArray) (assembly *Assembly, err er return } + +// ToString Obtains a string representation that includes the friendly name of the application domain and any context policies. +// https://docs.microsoft.com/en-us/dotnet/api/system.appdomain.tostring?view=net-5.0#System_AppDomain_ToString +func (obj *AppDomain) ToString() (domain string, err error) { + debugPrint("Entering into appdomain.ToString()...") + var pDomain *string + hr, _, err := syscall.Syscall( + obj.vtbl.get_ToString, + 2, + uintptr(unsafe.Pointer(obj)), + uintptr(unsafe.Pointer(&pDomain)), + 0, + ) + + if err != syscall.Errno(0) { + err = fmt.Errorf("the AppDomain.ToString method retured an error:\r\n%s", err) + return + } + if hr != S_OK { + err = fmt.Errorf("the AppDomain.ToString method returned a non-zero HRESULT: 0x%x", hr) + return + } + err = nil + domain = ReadUnicodeStr(unsafe.Pointer(pDomain)) + return +} diff --git a/examples/C2Framework/C2Framework.go b/examples/C2Framework/C2Framework.go new file mode 100644 index 0000000..e860c8c --- /dev/null +++ b/examples/C2Framework/C2Framework.go @@ -0,0 +1,218 @@ +// +build windows + +// C2Framework is an example of how a Command & Control (C2) Framework could load the CLR, +// Load in an assembly, and execute it multiple time. This prevents the need to send the +// assembly down the wire multiple times. The examples also captures STDOUT/STDERR so they +// can be returned to the contorller. + +package main + +import ( + // Standard + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "syscall" + + // 3rd Party + clr "github.com/ropnop/go-clr" +) + +func main() { + verbose := flag.Bool("v", false, "Enable verbose output") + debug := flag.Bool("debug", false, "Enable debug output") + flag.Usage = func() { + flag.PrintDefaults() + os.Exit(0) + } + flag.Parse() + + rubeusPath := "C:\\Users\\bob\\Desktop\\Rubeus4.exe" + seatbeltPath := "C:\\Users\\bob\\Desktop\\Seatbelt4.exe" + sharpupPath := "C:\\Users\\bob\\Desktop\\SharpUp4.exe" + + if *debug { + clr.Debug = true + } + + // Redirect the program's STDOUT/STDERR to capture CLR assembly execution output + if *verbose { + fmt.Println("[-] Redirecting programs STDOUT/STDERR for CLR assembly exection...") + } + err := clr.RedirectStdoutStderr() + if err != nil { + log.Fatal(err) + } + + // Load the CLR and an ICORRuntimeHost instance + if *verbose { + fmt.Println("[-] Loading the CLR into this process...") + } + runtimeHost, err := clr.LoadCLR("v4") + if err != nil { + log.Fatal(err) + } + if *debug { + fmt.Printf("[DEBUG] Returned ICORRuntimeHost: %+v\n", runtimeHost) + } + + // Create a new AppDomain + if *verbose { + fmt.Println("[-] Creating a new AppDomain...") + } + domain, err := syscall.UTF16PtrFromString("rubeus") + if err != nil { + log.Fatal(err) + } + appDomain, err := runtimeHost.CreateDomain(domain) + if err != nil { + log.Fatal(err) + } + if *debug { + fmt.Printf("[DEBUG] Returned AppDomain: %v\n", appDomain) + } + + // Get Rubeus + rubeusBytes, err := ioutil.ReadFile(rubeusPath) + if err != nil { + log.Fatal(fmt.Sprintf("there was an error reading in the Rubeus file from %s:\n%s", rubeusPath, err)) + } + if *verbose { + fmt.Printf("[-] Ingested %d assembly bytes\n", len(rubeusBytes)) + } + + // Load assembly into default AppDomain + if *verbose { + fmt.Println("[-] Loading Rubeus into default AppDomain...") + } + methodInfo, err := clr.LoadAssembly(runtimeHost, rubeusBytes) + if err != nil { + log.Fatal(err) + } + if *debug { + fmt.Printf("[DEBUG] Returned MethodInfo: %+v\n", methodInfo) + } + + // Execute assembly from default AppDomain + if *verbose { + fmt.Println("[-] Executing Rubeus...") + } + stdout, stderr := clr.InvokeAssembly(methodInfo, []string{"klist"}) + if *debug { + fmt.Printf("[DEBUG] Returned STDOUT/STDERR\nSTDOUT: %s\nSTDERR: %s\n", stdout, stderr) + } + + // Print returned output + if stderr != "" { + fmt.Printf("[!] STDERR:\n%s\n", stderr) + } + if stdout != "" { + fmt.Printf("[+] STDOUT:\n%s\n", stdout) + } + + // Execute assembly from default AppDomain x2 + if *verbose { + fmt.Println("[-] Executing the Rubeus x2...") + } + stdout, stderr = clr.InvokeAssembly(methodInfo, []string{"triage"}) + if *debug { + fmt.Printf("[DEBUG] Returned STDOUT/STDERR\nSTDOUT: %s\nSTDERR: %s\n", stdout, stderr) + } + + // Print returned output + if stderr != "" { + fmt.Printf("[!] STDERR:\n%s\n", stderr) + } + if stdout != "" { + fmt.Printf("[+] STDOUT:\n%s\n", stdout) + } + + // Get Seatbelt + seatbeltBytes, err := ioutil.ReadFile(seatbeltPath) + if err != nil { + log.Fatal(fmt.Sprintf("there was an error reading in the Seatbelt file from %s:\n%s", seatbeltPath, err)) + } + + // Load assembly into default AppDomain + if *verbose { + fmt.Println("[-] Loading Seatbelt into default AppDomain...") + } + seatBelt, err := clr.LoadAssembly(runtimeHost, seatbeltBytes) + if err != nil { + log.Fatal(err) + } + if *debug { + fmt.Printf("[DEBUG] Returned MethodInfo: %+v\n", seatBelt) + } + + // Execute assembly from default AppDomain + if *verbose { + fmt.Println("[-] Executing Seatbelt...") + } + stdout, stderr = clr.InvokeAssembly(seatBelt, []string{"AntiVirus"}) + if *debug { + fmt.Printf("[DEBUG] Returned STDOUT/STDERR\nSTDOUT: %s\nSTDERR: %s\n", stdout, stderr) + } + + // Print returned output + if stderr != "" { + fmt.Printf("[!] STDERR:\n%s\n", stderr) + } + if stdout != "" { + fmt.Printf("[+] STDOUT:\n%s\n", stdout) + } + + // Execute assembly from default AppDomain x2 + if *verbose { + fmt.Println("[-] Executing Seatbelt x2...") + } + stdout, stderr = clr.InvokeAssembly(seatBelt, []string{"DotNet"}) + if *debug { + fmt.Printf("[DEBUG] Returned STDOUT/STDERR\nSTDOUT: %s\nSTDERR: %s\n", stdout, stderr) + } + + // Print returned output + if stderr != "" { + fmt.Printf("[!] STDERR:\n%s\n", stderr) + } + if stdout != "" { + fmt.Printf("[+] STDOUT:\n%s\n", stdout) + } + + // Get SharpUp + sharpUpBytes, err := ioutil.ReadFile(sharpupPath) + if err != nil { + log.Fatal(fmt.Sprintf("there was an error reading in the SharpUp file from %s:\n%s", sharpupPath, err)) + } + + // Load assembly into default AppDomain + if *verbose { + fmt.Println("[-] Loading SharpUp into default AppDomain...") + } + sharpUp, err := clr.LoadAssembly(runtimeHost, sharpUpBytes) + if err != nil { + log.Fatal(err) + } + if *debug { + fmt.Printf("[DEBUG] Returned MethodInfo: %+v\n", sharpUp) + } + + // Execute assembly from default AppDomain + if *verbose { + fmt.Println("[-] Executing SharpUp...") + } + stdout, stderr = clr.InvokeAssembly(sharpUp, []string{"-h"}) + if *debug { + fmt.Printf("[DEBUG] Returned STDOUT/STDERR\nSTDOUT: %s\nSTDERR: %s\n", stdout, stderr) + } + + // Print returned output + if stderr != "" { + fmt.Printf("[!] STDERR:\n%s\n", stderr) + } + if stdout != "" { + fmt.Printf("[+] STDOUT:\n%s\n", stdout) + } +} diff --git a/go-clr.go b/go-clr.go index 70eed3d..dc031ae 100644 --- a/go-clr.go +++ b/go-clr.go @@ -197,3 +197,176 @@ func ExecuteByteArray(targetRuntime string, rawBytes []byte, params []string) (r metahost.Release() return 0, nil } + +// LoadCLR loads the target runtime into the current process and returns the runtimehost +// The intended purpose is for the runtimehost to be reused for subsequent operations +// throught the duration of the program. Commonly used with C2 frameworks +func LoadCLR(targetRuntime string) (runtimeHost *ICORRuntimeHost, err error) { + if targetRuntime == "" { + targetRuntime = "v4" + } + + metahost, err := CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost) + if err != nil { + return runtimeHost, fmt.Errorf("there was an error enumerating the installed CLR runtimes:\n%s", err) + } + + runtimes, err := GetInstalledRuntimes(metahost) + if err != nil { + return + } + var latestRuntime string + for _, r := range runtimes { + if strings.Contains(r, targetRuntime) { + latestRuntime = r + break + } else { + latestRuntime = r + } + } + runtimeInfo, err := GetRuntimeInfo(metahost, latestRuntime) + if err != nil { + return + } + + isLoadable, err := runtimeInfo.IsLoadable() + if err != nil { + return + } + if !isLoadable { + err = fmt.Errorf("%s is not loadable for some reason", latestRuntime) + } + + return GetICORRuntimeHost(runtimeInfo) +} + +// ExecuteByteArrayDefaultDomain uses a previously instantiated runtimehost, gets the default AppDomain, +// loads the assembly into, executes the assembly, and then releases AppDomain +// Intended to be used by C2 frameworks to quickly execute an assembly one time +func ExecuteByteArrayDefaultDomain(runtimeHost *ICORRuntimeHost, rawBytes []byte, params []string) (stdout string, stderr string) { + appDomain, err := GetAppDomain(runtimeHost) + if err != nil { + stderr = err.Error() + return + } + safeArrayPtr, err := CreateSafeArray(rawBytes) + if err != nil { + stderr = err.Error() + return + } + + assembly, err := appDomain.Load_3(safeArrayPtr) + if err != nil { + stderr = err.Error() + return + } + + methodInfo, err := assembly.GetEntryPoint() + if err != nil { + stderr = err.Error() + return + } + + var paramSafeArray *SafeArray + methodSignature, err := methodInfo.GetString() + if err != nil { + stderr = err.Error() + return + } + + if expectsParams(methodSignature) { + if paramSafeArray, err = PrepareParameters(params); err != nil { + stderr = err.Error() + return + } + } + + nullVariant := Variant{ + VT: 1, + Val: uintptr(0), + } + + err = methodInfo.Invoke_3(nullVariant, paramSafeArray) + if err != nil { + stderr = err.Error() + return + } + + assembly.Release() + appDomain.Release() + return +} + +func LoadByteArrayInAppDomain(appDomain *AppDomain, rawBytes []byte) (methodInfo *MethodInfo, err error) { + safeArrayPtr, err := CreateSafeArray(rawBytes) + if err != nil { + return + } + + assembly, err := appDomain.Load_3(safeArrayPtr) + if err != nil { + return + } + + return assembly.GetEntryPoint() +} + +// LoadAssembly uses a previously instantiated runtimehost and loads an assembly into the default AppDomain +// and returns the assembly's methodInfo structure. The intended purpose is for the assembly to be loaded +// once but executed many times throught the duration of the program. Commonly used with C2 frameworks +func LoadAssembly(runtimeHost *ICORRuntimeHost, rawBytes []byte) (methodInfo *MethodInfo, err error) { + appDomain, err := GetAppDomain(runtimeHost) + if err != nil { + return + } + safeArrayPtr, err := CreateSafeArray(rawBytes) + if err != nil { + return + } + + assembly, err := appDomain.Load_3(safeArrayPtr) + if err != nil { + return + } + return assembly.GetEntryPoint() +} + +// InvokeAssembly uses the MethodInfo structure of a previously loaded assembly and executes it. +// The intended purpose is for the assembly to be executed many times throught the duration of the +// program. Commonly used with C2 frameworks +func InvokeAssembly(methodInfo *MethodInfo, params []string) (stdout string, stderr string) { + var paramSafeArray *SafeArray + methodSignature, err := methodInfo.GetString() + if err != nil { + stderr = err.Error() + return + } + + if expectsParams(methodSignature) { + if paramSafeArray, err = PrepareParameters(params); err != nil { + stderr = err.Error() + return + } + } + + nullVariant := Variant{ + VT: 1, + Val: uintptr(0), + } + + err = methodInfo.Invoke_3(nullVariant, paramSafeArray) + if err != nil { + stderr = err.Error() + return + } + + // Read data from previously redirected STDOUT/STDERR + if wSTDOUT != nil { + stdout, stderr, err = ReadStdoutStderr() + if err != nil { + stderr += err.Error() + } + } + + return +} diff --git a/go.mod b/go.mod index 942686c..0f6e079 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/ropnop/go-clr go 1.13 require ( + github.com/google/uuid v1.2.0 // indirect golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 golang.org/x/text v0.3.3 ) diff --git a/go.sum b/go.sum index a7ddc6a..bce9614 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= diff --git a/guids.go b/guids.go index 0b65cec..3727ea0 100644 --- a/guids.go +++ b/guids.go @@ -15,4 +15,8 @@ var ( IID_ICorRuntimeHost = windows.GUID{Data1: 0xcb2f6722, Data2: 0xab3a, Data3: 0x11d2, Data4: [8]byte{0x9c, 0x40, 0x00, 0xc0, 0x4f, 0xa3, 0x0a, 0x3e}} CLSID_CorRuntimeHost = windows.GUID{Data1: 0xcb2f6723, Data2: 0xab3a, Data3: 0x11d2, Data4: [8]byte{0x9c, 0x40, 0x00, 0xc0, 0x4f, 0xa3, 0x0a, 0x3e}} IID_AppDomain = windows.GUID{Data1: 0x05f696dc, Data2: 0x2b29, Data3: 0x3663, Data4: [8]byte{0xad, 0x8b, 0xc4, 0x38, 0x9c, 0xf2, 0xa7, 0x13}} + // IID_IErrorInfo is the interface ID for the Error interface 1CF2B120-547D-101B-8E65-08002B2BD119 + IID_IErrorInfo = windows.GUID{Data1: 0x1cf2b120, Data2: 0x547d, Data3: 0x101b, Data4: [8]byte{0x8e, 0x65, 0x08, 0x00, 0x2b, 0x2b, 0xd1, 0x19}} + // DF0B3D60-548F-101B-8E65-08002B2BD119 https://docs.microsoft.com/en-us/windows/win32/api/oaidl/nn-oaidl-isupporterrorinfo + IID_ISupportErrorInfo = windows.GUID{Data1: 0xDF0B3D60, Data2: 0x548F, Data3: 0x101B, Data4: [8]byte{0x8e, 0x65, 0x08, 0x00, 0x2b, 0x2b, 0xd1, 0x19}} ) diff --git a/hresult.go b/hresult.go index 7cf17cf..097be9d 100644 --- a/hresult.go +++ b/hresult.go @@ -1,12 +1,22 @@ package clr +// https://docs.microsoft.com/en-us/dotnet/framework/interop/how-to-map-hresults-and-exceptions +// https://docs.microsoft.com/en-us/windows/win32/seccrypto/common-hresult-values + const ( S_OK = 0x00 S_FALSE = 0x01 + // COR_E_TARGETINVOCATION is TargetInvocationException + // https://docs.microsoft.com/en-us/dotnet/api/system.reflection.targetinvocationexception?view=net-5.0 + COR_E_TARGETINVOCATION uint32 = 0x80131604 // COR_E_SAFEARRAYRANKMISMATCH is SafeArrayRankMismatchException COR_E_SAFEARRAYRANKMISMATCH uint32 = 0x80131538 // COR_E_BADIMAGEFORMAT is BadImageFormatException COR_E_BADIMAGEFORMAT uint32 = 0x8007000b // DISP_E_BADPARAMCOUNT is invalid number of parameters DISP_E_BADPARAMCOUNT uint32 = 0x8002000e + // E_POINTER Pointer that is not valid + E_POINTER uint32 = 0x80004003 + // E_NOINTERFACE No such interface supported + E_NOINTERFACE uint32 = 0x80004002 ) diff --git a/iclrmetahost.go b/iclrmetahost.go index 43a93a7..57f8e4f 100644 --- a/iclrmetahost.go +++ b/iclrmetahost.go @@ -88,6 +88,24 @@ func CLRCreateInstance(clsid, riid windows.GUID) (ppInterface *ICLRMetaHost, err return } +func (obj *ICLRMetaHost) QueryInterface(riid windows.GUID, ppvObject unsafe.Pointer) error { + debugPrint("Entering into icorruntimehost.QueryInterface()...") + hr, _, err := syscall.Syscall( + obj.vtbl.QueryInterface, + 3, + uintptr(unsafe.Pointer(obj)), + uintptr(unsafe.Pointer(&riid)), // A reference to the interface identifier (IID) of the interface being queried for. + uintptr(ppvObject), + ) + if err != syscall.Errno(0) { + return fmt.Errorf("the IUknown::QueryInterface method returned an error:\r\n%s", err) + } + if hr != S_OK { + return fmt.Errorf("the IUknown::QueryInterface method method returned a non-zero HRESULT: 0x%x", hr) + } + return nil +} + func (obj *ICLRMetaHost) AddRef() uintptr { ret, _, _ := syscall.Syscall( obj.vtbl.AddRef, diff --git a/iclrruntimeinfo.go b/iclrruntimeinfo.go index 0d35a2e..aba6adb 100644 --- a/iclrruntimeinfo.go +++ b/iclrruntimeinfo.go @@ -155,7 +155,7 @@ func (obj *ICLRRuntimeInfo) GetInterface(rclsid windows.GUID, riid windows.GUID, ) // The syscall returns "The requested lookup key was not found in any active activation context." in the error position // TODO Why is this error message returned? - if err.Error() != "The requested lookup key was not found in any active activation context." { + if err != syscall.Errno(0) && err.Error() != "The requested lookup key was not found in any active activation context." { return fmt.Errorf("the ICLRRuntimeInfo::GetInterface method returned an error:\r\n%s", err) } if hr != S_OK { diff --git a/icorruntimehost.go b/icorruntimehost.go index cdd2631..be86ce4 100644 --- a/icorruntimehost.go +++ b/icorruntimehost.go @@ -6,13 +6,15 @@ import ( "fmt" "syscall" "unsafe" + + "golang.org/x/sys/windows" ) type ICORRuntimeHost struct { vtbl *ICORRuntimeHostVtbl } -// ICORRuntimeHos Provides methods that enable the host to start and stop the common language runtime (CLR) +// ICORRuntimeHostVtbl Provides methods that enable the host to start and stop the common language runtime (CLR) // explicitly, to create and configure application domains, to access the default domain, and to enumerate all // domains running in the process. type ICORRuntimeHostVtbl struct { @@ -78,6 +80,26 @@ func GetICORRuntimeHost(runtimeInfo *ICLRRuntimeInfo) (*ICORRuntimeHost, error) return runtimeHost, err } +func (obj *ICORRuntimeHost) QueryInterface(riid windows.GUID, ppvObject unsafe.Pointer) error { + debugPrint("Entering into icorruntimehost.QueryInterface()...") + hr, _, err := syscall.Syscall( + obj.vtbl.QueryInterface, + 3, + uintptr(unsafe.Pointer(obj)), + uintptr(unsafe.Pointer(&riid)), // A reference to the interface identifier (IID) of the interface being queried for. + uintptr(ppvObject), + ) + if err != syscall.Errno(0) { + fmt.Println("1111111111111") + return fmt.Errorf("the IUknown::QueryInterface method returned an error:\r\n%s", err) + } + if hr != S_OK { + fmt.Println("222222222222222222") + return fmt.Errorf("the IUknown::QueryInterface method method returned a non-zero HRESULT: 0x%x", hr) + } + return nil +} + func (obj *ICORRuntimeHost) AddRef() uintptr { ret, _, _ := syscall.Syscall( obj.vtbl.AddRef, @@ -147,3 +169,35 @@ func (obj *ICORRuntimeHost) GetDefaultDomain() (IUnknown *IUnknown, err error) { err = nil return } + +// CreateDomain Creates an application domain. The caller receives an interface pointer of type _AppDomain to an instance of type System.AppDomain. +// HRESULT CreateDomain ( +// [in] LPWSTR pwzFriendlyName, +// [in] IUnknown* pIdentityArray, +// [out] void **pAppDomain +// ); +// https://docs.microsoft.com/en-us/previous-versions/dotnet/netframework-4.0/ms164322(v=vs.100) +func (obj *ICORRuntimeHost) CreateDomain(pwzFriendlyName *uint16) (pAppDomain *AppDomain, err error) { + debugPrint("Entering into icorruntimehost.CreateDomain()...") + hr, _, err := syscall.Syscall6( + obj.vtbl.CreateDomain, + 4, + uintptr(unsafe.Pointer(obj)), + uintptr(unsafe.Pointer(pwzFriendlyName)), // [in] LPWSTR pwzFriendlyName - An optional parameter used to give a friendly name to the domain + uintptr(unsafe.Pointer(nil)), // [in] IUnknown* pIdentityArray - An optional array of pointers to IIdentity instances that represent evidence mapped through security policy to establish a permission set + uintptr(unsafe.Pointer(&pAppDomain)), // [out] IUnknown** pAppDomain + 0, + 0, + ) + if err != syscall.Errno(0) { + // The specified procedure could not be found. + // TODO Why is this error message returned? + debugPrint(fmt.Sprintf("the ICORRuntimeHost::CreateDomain method returned an error:\r\n%s", err)) + } + if hr != S_OK { + err = fmt.Errorf("the ICORRuntimeHost::CreateDomain method method returned a non-zero HRESULT: 0x%x", hr) + return + } + err = nil + return +} diff --git a/ierrorinfo.go b/ierrorinfo.go new file mode 100644 index 0000000..880874c --- /dev/null +++ b/ierrorinfo.go @@ -0,0 +1,117 @@ +// +build windows + +package clr + +import ( + "fmt" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +type IErrorInfo struct { + vtbl *IErrorInfoVtbl +} + +// IErrorInfoVtbl returns information about an error in addition to the return code. +// It returns the error message, name of the component and GUID of the interface in +// which the error occurred, and the name and topic of the Help file that applies to the error. +// https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms723041(v=vs.85) +type IErrorInfoVtbl struct { + // QueryInterface Retrieves pointers to the supported interfaces on an object. + QueryInterface uintptr + // AddRef Increments the reference count for an interface pointer to a COM object. + // You should call this method whenever you make a copy of an interface pointer. + AddRef uintptr + // Release Decrements the reference count for an interface on a COM object. + Release uintptr + // GetDescription Returns a text description of the error + GetDescription uintptr + // GetGUID Returns the GUID of the interface that defined the error. + GetGUID uintptr + // GetHelpContext Returns the Help context ID for the error. + GetHelpContext uintptr + // GetHelpFile Returns the path of the Help file that describes the error. + GetHelpFile uintptr + // GetSource Returns the name of the component that generated the error, such as "ODBC driver-name". + GetSource uintptr +} + +// GetDescription Returns a text description of the error. +// HRESULT GetDescription ( +// BSTR *pbstrDescription); +// https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms714318(v=vs.85) +func (obj *IErrorInfo) GetDescription() (pbstrDescription *string, err error) { + debugPrint("Entering into ierrorinfo.GetDescription()...") + + hr, _, err := syscall.Syscall( + obj.vtbl.GetDescription, + 2, + uintptr(unsafe.Pointer(obj)), + uintptr(unsafe.Pointer(&pbstrDescription)), + 0, + ) + + if err != syscall.Errno(0) { + err = fmt.Errorf("the IErrorInfo::GetDescription method returned an error:\r\n%s", err) + return + } + if hr != S_OK { + err = fmt.Errorf("the IErrorInfo::GetDescription method method returned a non-zero HRESULT: 0x%x", hr) + return + } + err = nil + return +} + +// GetGUID Returns the globally unique identifier (GUID) of the interface that defined the error. +// HRESULT GetGUID( +// GUID *pGUID +// ); +// https://docs.microsoft.com/en-us/windows/win32/api/oaidl/nf-oaidl-ierrorinfo-getguid +func (obj *IErrorInfo) GetGUID() (pGUID *windows.GUID, err error) { + debugPrint("Entering into ierrorinfo.GetGUID()...") + + hr, _, err := syscall.Syscall( + obj.vtbl.GetGUID, + 2, + uintptr(unsafe.Pointer(obj)), + uintptr(unsafe.Pointer(pGUID)), + 0, + ) + + if err != syscall.Errno(0) { + err = fmt.Errorf("the IErrorInfo::GetGUID method returned an error:\r\n%s", err) + return + } + if hr != S_OK { + err = fmt.Errorf("the IErrorInfo::GetGUID method method returned a non-zero HRESULT: 0x%x", hr) + return + } + err = nil + return +} + +// GetErrorInfo Obtains the error information pointer set by the previous call to SetErrorInfo in the current logical thread. +// HRESULT GetErrorInfo( +// ULONG dwReserved, +// IErrorInfo **pperrinfo +// ); +// https://docs.microsoft.com/en-us/windows/win32/api/oleauto/nf-oleauto-geterrorinfo +func GetErrorInfo() (pperrinfo *IErrorInfo, err error) { + debugPrint("Entering into ierrorinfo.GetErrorInfo()...") + modOleAut32 := syscall.MustLoadDLL("OleAut32.dll") + procGetErrorInfo := modOleAut32.MustFindProc("GetErrorInfo") + hr, _, err := procGetErrorInfo.Call(0, uintptr(unsafe.Pointer(&pperrinfo))) + if err != syscall.Errno(0) { + err = fmt.Errorf("the OleAu32.GetErrorInfo procedure call returned an error:\n%s", err) + return + } + if hr != S_OK { + err = fmt.Errorf("the OleAu32.GetErrorInfo procedure call returned a non-zero HRESULT code: 0x%x", hr) + return + } + err = nil + return +} diff --git a/io.go b/io.go new file mode 100644 index 0000000..652749e --- /dev/null +++ b/io.go @@ -0,0 +1,151 @@ +// +build windows + +package clr + +import ( + "fmt" + "io" + "os" + + "golang.org/x/sys/windows" +) + +// origSTDOUT is a Windows Handle to the program's original STDOUT +var origSTDOUT = windows.Stdout + +// origSTDERR is a Windows Handle to the program's original STDERR +var origSTDERR = windows.Stderr + +// rSTDOUT is an io.Reader for STDOUT +var rSTDOUT *os.File + +// wSTDOUT is an io.Writer for STDOUT +var wSTDOUT *os.File + +// rSTDERR is an io.Reader for STDERR +var rSTDERR *os.File + +// wSTDERR is an io.Writer for STDERR +var wSTDERR *os.File + +// RedirectStdoutStderr redirects the program's STDOUT/STDERR to an *os.File that can be read from this Go program +// The CLR executes assemblies outside of Go and therefore STDOUT/STDERR can't be captured using normal functions +// Intended to be used with a Command & Control framework so STDOUT/STDERR can be captured and returned +func RedirectStdoutStderr() (err error) { + // Create a new reader and writer for STDOUT + rSTDOUT, wSTDOUT, err = os.Pipe() + if err != nil { + err = fmt.Errorf("there was an error calling the os.Pipe() function to create a new STDOUT:\n%s", err) + return + } + + // Createa new reader and writer for STDERR + rSTDERR, wSTDERR, err = os.Pipe() + if err != nil { + err = fmt.Errorf("there was an error calling the os.Pipe() function to create a new STDERR:\n%s", err) + return + } + + // Set STDOUT/STDERR to the new files from os.Pipe() + // https://docs.microsoft.com/en-us/windows/console/setstdhandle + if err = windows.SetStdHandle(windows.STD_OUTPUT_HANDLE, windows.Handle(wSTDOUT.Fd())); err != nil { + err = fmt.Errorf("there was an error calling the windows.SetStdHandle function for STDOUT:\n%s", err) + return + } + + if err = windows.SetStdHandle(windows.STD_ERROR_HANDLE, windows.Handle(wSTDERR.Fd())); err != nil { + err = fmt.Errorf("there was an error calling the windows.SetStdHandle function for STDERR:\n%s", err) + return + } + + return +} + +// RestoreStdoutStderr returns the program's original STDOUT/STDERR handles before they were redirected an *os.File +func RestoreStdoutStderr() error { + if err := windows.SetStdHandle(windows.STD_OUTPUT_HANDLE, origSTDOUT); err != nil { + return fmt.Errorf("there was an error calling the windows.SetStdHandle function to restore the original STDOUT handle:\n%s", err) + } + if err := windows.SetStdHandle(windows.STD_ERROR_HANDLE, origSTDERR); err != nil { + return fmt.Errorf("there was an error calling the windows.SetStdHandle function to restore the original STDERR handle:\n%s", err) + } + return nil +} + +// ReadStdoutStderr reads from the REDIRECTED STDOUT/STDERR +// Only use when RedirectStdoutStderr was previously called +func ReadStdoutStderr() (stdout string, stderr string, err error) { + debugPrint("Entering io.ReadStdoutStderr()...") + + /* + // Closing the STDOUT Writer will cause all subsequent calls to Invoke_3 + // to return HRESULT: 0x80131604, TargetInvocationException (COR_E_TARGETINVOCATION) + // Have not been able to read the inner error and determine the root cause + // Leaving wSTDOUT open and never closing is a temporary work around + err = wSTDOUT.Close() + if err != nil { + err = fmt.Errorf("there was an error closing the STDOUT Writer:\n%s", err) + return + } + */ + + // TODO Update to use io.ReadAll(), requires GO 1.16 + // https://golang.org/pkg/io/#ReadAll + // TODO return to io.Copy at a minimum + bStdout := make([]byte, 500000) + c, err := rSTDOUT.Read(bStdout) + // Expected rSTDOUT.Read() to return io.EOF, but it doesn't + if err != nil { + err = fmt.Errorf("there was an error reading from the STDOUT Reader:\n%s", err) + return + } + if c > 0 { + stdout = string(bStdout[:]) + } + + // Close the STDERR writer + // This is needed because there is no EOF if nothing was written STDERR and then the read call blocks + wSTDERR.Close() + bStderr := make([]byte, 500000) + c, err = rSTDERR.Read(bStderr) + // rSTDERR.Read() will return io.EOF when nothing was written to it + if err != nil && err != io.EOF { + err = fmt.Errorf("there was an error reading from the STDERR Reader:\n%s", err) + return + } + err = nil + if c > 0 { + stderr = string(bStderr[:]) + } + + return +} + +// CloseSTdoutStderr closes the Reader/Writer for the prviously redirected STDOUT/STDERR +// that was changed to an *os.File +func CloseStdoutStderr() (err error) { + err = rSTDOUT.Close() + if err != nil { + err = fmt.Errorf("there was an error closing the STDOUT Reader:\n%s", err) + return + } + + err = wSTDOUT.Close() + if err != nil { + err = fmt.Errorf("there was an error closing the STDOUT Writer:\n%s", err) + return + } + + err = rSTDERR.Close() + if err != nil { + err = fmt.Errorf("there was an error closing the STDERR Reader:\n%s", err) + return + } + + err = wSTDERR.Close() + if err != nil { + err = fmt.Errorf("there was an error closing the STDOUT Writer:\n%s", err) + return + } + return nil +} diff --git a/isupporterrorinfo.go b/isupporterrorinfo.go new file mode 100644 index 0000000..5b8af72 --- /dev/null +++ b/isupporterrorinfo.go @@ -0,0 +1,123 @@ +// +build windows + +package clr + +import ( + "fmt" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +// ISupportErrorInfo Ensures that error information can be propagated up the call chain correctly. +// Automation objects that use the error handling interfaces must implement ISupportErrorInfo +// https://docs.microsoft.com/en-us/windows/win32/api/oaidl/nn-oaidl-isupporterrorinfo +type ISupportErrorInfo struct { + vtbl *ISupportErrorInfoVtbl +} + +type ISupportErrorInfoVtbl struct { + // QueryInterface Retrieves pointers to the supported interfaces on an object. + QueryInterface uintptr + // AddRef Increments the reference count for an interface pointer to a COM object. + // You should call this method whenever you make a copy of an interface pointer. + AddRef uintptr + // Release Decrements the reference count for an interface on a COM object. + Release uintptr + // InterfaceSupportsErrorInfo Indicates whether an interface supports the IErrorInfo interface. + // https://docs.microsoft.com/en-us/windows/win32/api/oaidl/nf-oaidl-isupporterrorinfo-interfacesupportserrorinfo + InterfaceSupportsErrorInfo uintptr +} + +// QueryInterface queries a COM object for a pointer to one of its interface; +// identifying the interface by a reference to its interface identifier (IID). +// If the COM object implements the interface, then it returns a pointer to that interface after calling IUnknown::AddRef on it. +// HRESULT QueryInterface( +// REFIID riid, +// void **ppvObject +// ); +// https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-queryinterface(refiid_void) +func (obj *ISupportErrorInfo) QueryInterface(riid windows.GUID, ppvObject unsafe.Pointer) error { + debugPrint("Entering into iunknown.QueryInterface()...") + hr, _, err := syscall.Syscall( + obj.vtbl.QueryInterface, + 3, + uintptr(unsafe.Pointer(obj)), + uintptr(unsafe.Pointer(&riid)), // A reference to the interface identifier (IID) of the interface being queried for. + uintptr(ppvObject), + ) + if err != syscall.Errno(0) { + return fmt.Errorf("the IUknown::QueryInterface method returned an error:\r\n%s", err) + } + if hr != S_OK { + return fmt.Errorf("the IUknown::QueryInterface method method returned a non-zero HRESULT: 0x%x", hr) + } + return nil +} + +// AddRef Increments the reference count for an interface pointer to a COM object. +// You should call this method whenever you make a copy of an interface pointer +// ULONG AddRef(); +// https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-addref +func (obj *ISupportErrorInfo) AddRef() (count uint32, err error) { + debugPrint("Entering into iunknown.AddRef()...") + ret, _, err := syscall.Syscall( + obj.vtbl.AddRef, + 1, + uintptr(unsafe.Pointer(obj)), + 0, + 0, + ) + if err != syscall.Errno(0) { + return 0, fmt.Errorf("the IUnknown::AddRef method returned an error:\r\n%s", err) + } + err = nil + // Unable to avoid misuse of unsafe.Pointer because the Windows API call returns the safeArray pointer in the "ret" value. This is a go vet false positive + count = *(*uint32)(unsafe.Pointer(ret)) + return +} + +// Release Decrements the reference count for an interface on a COM object. +// ULONG Release(); +// https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-release +func (obj *ISupportErrorInfo) Release() (count uint32, err error) { + debugPrint("Entering into iunknown.Release()...") + ret, _, err := syscall.Syscall( + obj.vtbl.Release, + 1, + uintptr(unsafe.Pointer(obj)), + 0, + 0, + ) + if err != syscall.Errno(0) { + return 0, fmt.Errorf("the IUnknown::Release method returned an error:\r\n%s", err) + } + err = nil + // Unable to avoid misuse of unsafe.Pointer because the Windows API call returns the safeArray pointer in the "ret" value. This is a go vet false positive + count = *(*uint32)(unsafe.Pointer(ret)) + return +} + +// InterfaceSupportsErrorInfo +// HRESULT InterfaceSupportsErrorInfo( +// REFIID riid +// ); +// https://docs.microsoft.com/en-us/windows/win32/api/oaidl/nf-oaidl-isupporterrorinfo-interfacesupportserrorinfo +func (obj *ISupportErrorInfo) InterfaceSupportsErrorInfo(riid windows.GUID) error { + debugPrint("Entering into isupporterrorinfo.InterfaceSupportsErrorInfo()...") + hr, _, err := syscall.Syscall( + obj.vtbl.InterfaceSupportsErrorInfo, + 2, + uintptr(unsafe.Pointer(obj)), + uintptr(unsafe.Pointer(&riid)), + 0, + ) + if err != syscall.Errno(0) { + return fmt.Errorf("the ISupportErrorInfo::InterfaceSupportsErrorInfo method returned an error:\r\n%s", err) + } + if hr != S_OK { + return fmt.Errorf("the ISupportErrorInfo::InterfaceSupportsErrorInfo method method returned a non-zero HRESULT: 0x%x", hr) + } + return nil +} diff --git a/methodinfo.go b/methodinfo.go index 696c799..28e7b3b 100644 --- a/methodinfo.go +++ b/methodinfo.go @@ -66,14 +66,22 @@ type MethodInfoVtbl struct { GetBaseDefinition uintptr } -func (obj *MethodInfo) QueryInterface(riid *windows.GUID, ppvObject *uintptr) uintptr { - ret, _, _ := syscall.Syscall( +func (obj *MethodInfo) QueryInterface(riid windows.GUID, ppvObject unsafe.Pointer) error { + debugPrint("Entering into methodinfo.QueryInterface()...") + hr, _, err := syscall.Syscall( obj.vtbl.QueryInterface, 3, uintptr(unsafe.Pointer(obj)), - uintptr(unsafe.Pointer(riid)), - uintptr(unsafe.Pointer(ppvObject))) - return ret + uintptr(unsafe.Pointer(&riid)), // A reference to the interface identifier (IID) of the interface being queried for. + uintptr(ppvObject), + ) + if err != syscall.Errno(0) { + return fmt.Errorf("the IUknown::QueryInterface method returned an error:\r\n%s", err) + } + if hr != S_OK { + return fmt.Errorf("the IUknown::QueryInterface method method returned a non-zero HRESULT: 0x%x", hr) + } + return nil } func (obj *MethodInfo) AddRef() uintptr { @@ -119,10 +127,50 @@ func (obj *MethodInfo) Invoke_3(variantObj Variant, parameters *SafeArray) (err err = fmt.Errorf("the MethodInfo::Invoke_3 method returned an error:\r\n%s", err) return } + + // If the HRESULT is a TargetInvocationException, attempt to get the inner error + // This currentl doesn't work + if uint32(hr) == COR_E_TARGETINVOCATION { + var iSupportErrorInfo *ISupportErrorInfo + // See if MethodInfo supports the ISupportErrorInfo interface + err = obj.QueryInterface(IID_ISupportErrorInfo, unsafe.Pointer(&iSupportErrorInfo)) + if err != nil { + err = fmt.Errorf("the MethodInfo::QueryInterface method returned an error when looking for the ISupportErrorInfo interface:\r\n%s", err) + return + } + + // See if the ICorRuntimeHost interface supports the IErrorInfo interface + // Not sure if there is an Interface ID for MethodInfo + err = iSupportErrorInfo.InterfaceSupportsErrorInfo(IID_ICorRuntimeHost) + if err != nil { + err = fmt.Errorf("there was an error with the ISupportErrorInfo::InterfaceSupportsErrorInfo method:\r\n%s", err) + return + } + + // Get the IErrorInfo object + iErrorInfo, errG := GetErrorInfo() + if errG != nil { + err = fmt.Errorf("there was an error getting the IErrorInfo object:\r\n%s", errG) + return err + } + + // Read the IErrorInfo description + desc, errD := iErrorInfo.GetDescription() + if errD != nil { + err = fmt.Errorf("the IErrorInfo::GetDescription method returned an error:\r\n%s", errD) + return err + } + if desc == nil { + err = fmt.Errorf("the Assembly::Invoke_3 method returned a non-zero HRESULT: 0x%x", hr) + return + } + err = fmt.Errorf("the Assembly::Invoke_3 method returned a non-zero HRESULT: 0x%x with an IErrorInfo description of: %s", hr, *desc) + } if hr != S_OK { err = fmt.Errorf("the Assembly::Invoke_3 method returned a non-zero HRESULT: 0x%x", hr) return } + if pRetVal != nil { err = fmt.Errorf("the Assembly::Invoke_3 method returned a non-zero pRetVal: %+v", pRetVal) return From d791ef9395ea8779f90e2d20fc578c810dacfb9d Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Sat, 27 Mar 2021 09:22:04 -0400 Subject: [PATCH 20/31] Updated STDOUT/STDERR Read --- io.go | 47 ++++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/io.go b/io.go index 652749e..f0e2c75 100644 --- a/io.go +++ b/io.go @@ -62,6 +62,8 @@ func RedirectStdoutStderr() (err error) { } // RestoreStdoutStderr returns the program's original STDOUT/STDERR handles before they were redirected an *os.File +// Previously instantiated CLRs will continue to use the REDIRECTED STDOUT/STDERR handles and will not resume +// using the restored handles func RestoreStdoutStderr() error { if err := windows.SetStdHandle(windows.STD_OUTPUT_HANDLE, origSTDOUT); err != nil { return fmt.Errorf("there was an error calling the windows.SetStdHandle function to restore the original STDOUT handle:\n%s", err) @@ -77,44 +79,47 @@ func RestoreStdoutStderr() error { func ReadStdoutStderr() (stdout string, stderr string, err error) { debugPrint("Entering io.ReadStdoutStderr()...") - /* - // Closing the STDOUT Writer will cause all subsequent calls to Invoke_3 - // to return HRESULT: 0x80131604, TargetInvocationException (COR_E_TARGETINVOCATION) - // Have not been able to read the inner error and determine the root cause - // Leaving wSTDOUT open and never closing is a temporary work around - err = wSTDOUT.Close() - if err != nil { - err = fmt.Errorf("there was an error closing the STDOUT Writer:\n%s", err) - return - } - */ + // If nothing was written to STDOUT then Read() will block + // Can't call Close() because the pipe needs to remain open for the duration of the top-level program + // A "workaround" is to write in a null byte so that way it can be read and won't block + _, err = wSTDOUT.Write([]byte{0x00}) + if err != nil { + err = fmt.Errorf("there was an error writing a null-byte into STDOUT Writer:\n%s", err) + return + } // TODO Update to use io.ReadAll(), requires GO 1.16 // https://golang.org/pkg/io/#ReadAll - // TODO return to io.Copy at a minimum bStdout := make([]byte, 500000) c, err := rSTDOUT.Read(bStdout) - // Expected rSTDOUT.Read() to return io.EOF, but it doesn't - if err != nil { + // Will return EOF if there is no data to be read + if err != nil && err != io.EOF { err = fmt.Errorf("there was an error reading from the STDOUT Reader:\n%s", err) return } - if c > 0 { + // If STDOUT is contains more than the null byte we wrote into it, then capture it + if c > 1 && bStdout[1] != 0x00 { stdout = string(bStdout[:]) } - // Close the STDERR writer - // This is needed because there is no EOF if nothing was written STDERR and then the read call blocks - wSTDERR.Close() + // If nothing was written to STDERR then Read() will block + // Can't call Close() because the pipe needs to remain open for the duration of the top-level program + // A "workaround" is to write in a null byte so that way it can be read and won't block + _, err = wSTDERR.Write([]byte{0x00}) + if err != nil { + err = fmt.Errorf("there was an error writing a null-byte into STDERR Writer:\n%s", err) + return + } bStderr := make([]byte, 500000) c, err = rSTDERR.Read(bStderr) - // rSTDERR.Read() will return io.EOF when nothing was written to it + // Will return EOF when nothing was written to it if Close() was called first if err != nil && err != io.EOF { err = fmt.Errorf("there was an error reading from the STDERR Reader:\n%s", err) return } err = nil - if c > 0 { + // If STDERR is contains more than the null byte we wrote into it, then capture it + if c > 1 && bStderr[1] != 0x00 { stderr = string(bStderr[:]) } @@ -144,7 +149,7 @@ func CloseStdoutStderr() (err error) { err = wSTDERR.Close() if err != nil { - err = fmt.Errorf("there was an error closing the STDOUT Writer:\n%s", err) + err = fmt.Errorf("there was an error closing the STDERR Writer:\n%s", err) return } return nil From 46c71d5dde2c1e6273ea3401cc82752e7cd4171e Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Sat, 27 Mar 2021 09:34:07 -0400 Subject: [PATCH 21/31] Added SafeArrayDelete --- go-clr.go | 2 ++ safearray.go | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/go-clr.go b/go-clr.go index dc031ae..0eb44c9 100644 --- a/go-clr.go +++ b/go-clr.go @@ -354,6 +354,8 @@ func InvokeAssembly(methodInfo *MethodInfo, params []string) (stdout string, std Val: uintptr(0), } + defer SafeArrayDestroy(paramSafeArray) + err = methodInfo.Invoke_3(nullVariant, paramSafeArray) if err != nil { stderr = err.Error() diff --git a/safearray.go b/safearray.go index 4d55842..016b7b4 100644 --- a/safearray.go +++ b/safearray.go @@ -312,3 +312,29 @@ func SafeArrayGetUBound(psa *SafeArray, nDim uint32) (uint32, error) { } return plUbound, nil } + +// SafeArrayDestroy Destroys an existing array descriptor and all of the data in the array. +// If objects are stored in the array, Release is called on each object in the array. +// HRESULT SafeArrayDestroy( +// SAFEARRAY *psa +// ); +func SafeArrayDestroy(psa *SafeArray) error { + debugPrint("Entering into safearray.SafeArrayDestroy()...") + + modOleAuto := syscall.MustLoadDLL("OleAut32.dll") + safeArrayDestroy := modOleAuto.MustFindProc("SafeArrayDestroy") + + hr, _, err := safeArrayDestroy.Call( + uintptr(unsafe.Pointer(psa)), + 0, + 0, + ) + + if err != syscall.Errno(0) { + return fmt.Errorf("the oleaut32!SafeArrayDestroy function call returned an error:\n%s", err) + } + if hr != S_OK { + return fmt.Errorf("the oleaut32!SafeArrayDestroy function returned a non-zero HRESULT: 0x%x", hr) + } + return nil +} From b7ac0affbebdab0c70faa77d7a51f30ec28fa5d7 Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Sat, 27 Mar 2021 11:34:51 -0400 Subject: [PATCH 22/31] Removed unused code --- examples/C2Framework/C2Framework.go | 17 ---------------- go-clr.go | 14 -------------- icorruntimehost.go | 30 ++++++++++++++++++++++++++++- 3 files changed, 29 insertions(+), 32 deletions(-) diff --git a/examples/C2Framework/C2Framework.go b/examples/C2Framework/C2Framework.go index e860c8c..35e9a67 100644 --- a/examples/C2Framework/C2Framework.go +++ b/examples/C2Framework/C2Framework.go @@ -14,7 +14,6 @@ import ( "io/ioutil" "log" "os" - "syscall" // 3rd Party clr "github.com/ropnop/go-clr" @@ -58,22 +57,6 @@ func main() { fmt.Printf("[DEBUG] Returned ICORRuntimeHost: %+v\n", runtimeHost) } - // Create a new AppDomain - if *verbose { - fmt.Println("[-] Creating a new AppDomain...") - } - domain, err := syscall.UTF16PtrFromString("rubeus") - if err != nil { - log.Fatal(err) - } - appDomain, err := runtimeHost.CreateDomain(domain) - if err != nil { - log.Fatal(err) - } - if *debug { - fmt.Printf("[DEBUG] Returned AppDomain: %v\n", appDomain) - } - // Get Rubeus rubeusBytes, err := ioutil.ReadFile(rubeusPath) if err != nil { diff --git a/go-clr.go b/go-clr.go index 0eb44c9..6450992 100644 --- a/go-clr.go +++ b/go-clr.go @@ -297,20 +297,6 @@ func ExecuteByteArrayDefaultDomain(runtimeHost *ICORRuntimeHost, rawBytes []byte return } -func LoadByteArrayInAppDomain(appDomain *AppDomain, rawBytes []byte) (methodInfo *MethodInfo, err error) { - safeArrayPtr, err := CreateSafeArray(rawBytes) - if err != nil { - return - } - - assembly, err := appDomain.Load_3(safeArrayPtr) - if err != nil { - return - } - - return assembly.GetEntryPoint() -} - // LoadAssembly uses a previously instantiated runtimehost and loads an assembly into the default AppDomain // and returns the assembly's methodInfo structure. The intended purpose is for the assembly to be loaded // once but executed many times throught the duration of the program. Commonly used with C2 frameworks diff --git a/icorruntimehost.go b/icorruntimehost.go index be86ce4..c8e77a8 100644 --- a/icorruntimehost.go +++ b/icorruntimehost.go @@ -17,6 +17,7 @@ type ICORRuntimeHost struct { // ICORRuntimeHostVtbl Provides methods that enable the host to start and stop the common language runtime (CLR) // explicitly, to create and configure application domains, to access the default domain, and to enumerate all // domains running in the process. +// https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/hosting/icorruntimehost-interface type ICORRuntimeHostVtbl struct { QueryInterface uintptr AddRef uintptr @@ -195,7 +196,34 @@ func (obj *ICORRuntimeHost) CreateDomain(pwzFriendlyName *uint16) (pAppDomain *A debugPrint(fmt.Sprintf("the ICORRuntimeHost::CreateDomain method returned an error:\r\n%s", err)) } if hr != S_OK { - err = fmt.Errorf("the ICORRuntimeHost::CreateDomain method method returned a non-zero HRESULT: 0x%x", hr) + err = fmt.Errorf("the ICORRuntimeHost::CreateDomain method returned a non-zero HRESULT: 0x%x", hr) + return + } + err = nil + return +} + +// EnumDomains Gets an enumerator for the domains in the current process. +// HRESULT EnumDomains ( +// [out] HCORENUM *hEnum +// ); +func (obj *ICORRuntimeHost) EnumDomains() (hEnum *uintptr, err error) { + debugPrint("Enterin into icorruntimehost.EnumDomains()...") + + hr, _, err := syscall.Syscall( + obj.vtbl.EnumDomains, + (uintptr(unsafe.Pointer(hEnum))), + 0, + 0, + 0, + ) + + if err != syscall.Errno(0) { + err = fmt.Errorf("the ICORRuntimeHost::EnumDomains method returned an error:\n%s", err) + return + } + if hr != S_OK { + err = fmt.Errorf("the ICORRuntimeHost::EnumDomains method returned a non-zero HRESULT: 0x%x", hr) return } err = nil From a2bef244455b0576bf12a5ef03b45eba3f02560b Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Sun, 28 Mar 2021 08:38:35 -0400 Subject: [PATCH 23/31] Update go.mod --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 0f6e079..8fe6d68 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/ropnop/go-clr +module github.com/Ne0nd0g/go-clr go 1.13 From 875ea40cb65e97a4b4d33d4a5bfdcbd550c20a04 Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Thu, 8 Apr 2021 15:28:33 -0400 Subject: [PATCH 24/31] Updated STDOUT/STDERR to use a buffer --- examples/C2Framework/C2Framework.go | 13 ++- examples/CLRWrapper/CLRWrapper.go | 2 +- examples/DLLfromDisk/DLLfromDisk.go | 2 +- examples/EXEfromMemory/EXEfromMemory.go | 2 +- go-clr.go | 11 ++- go.mod | 2 +- go.sum | 2 + io.go | 126 ++++++++++++++++-------- 8 files changed, 106 insertions(+), 54 deletions(-) diff --git a/examples/C2Framework/C2Framework.go b/examples/C2Framework/C2Framework.go index 35e9a67..a9de2f2 100644 --- a/examples/C2Framework/C2Framework.go +++ b/examples/C2Framework/C2Framework.go @@ -15,8 +15,7 @@ import ( "log" "os" - // 3rd Party - clr "github.com/ropnop/go-clr" + clr "github.com/Ne0nd0g/go-clr/v1" ) func main() { @@ -28,9 +27,9 @@ func main() { } flag.Parse() - rubeusPath := "C:\\Users\\bob\\Desktop\\Rubeus4.exe" - seatbeltPath := "C:\\Users\\bob\\Desktop\\Seatbelt4.exe" - sharpupPath := "C:\\Users\\bob\\Desktop\\SharpUp4.exe" + rubeusPath := `C:\Users\bob\Desktop\Rubeus4.exe` + seatbeltPath := `C:\Users\bob\Desktop\Seatbelt4.exe` + sharpupPath := `C:\Users\bob\Desktop\SharpUp4.exe` if *debug { clr.Debug = true @@ -99,7 +98,7 @@ func main() { if *verbose { fmt.Println("[-] Executing the Rubeus x2...") } - stdout, stderr = clr.InvokeAssembly(methodInfo, []string{"triage"}) + stdout, stderr = clr.InvokeAssembly(methodInfo, []string{"triage", "/service:KRBTGT"}) if *debug { fmt.Printf("[DEBUG] Returned STDOUT/STDERR\nSTDOUT: %s\nSTDERR: %s\n", stdout, stderr) } @@ -186,7 +185,7 @@ func main() { if *verbose { fmt.Println("[-] Executing SharpUp...") } - stdout, stderr = clr.InvokeAssembly(sharpUp, []string{"-h"}) + stdout, stderr = clr.InvokeAssembly(sharpUp, []string{"audit"}) if *debug { fmt.Printf("[DEBUG] Returned STDOUT/STDERR\nSTDOUT: %s\nSTDERR: %s\n", stdout, stderr) } diff --git a/examples/CLRWrapper/CLRWrapper.go b/examples/CLRWrapper/CLRWrapper.go index 9e5b91b..23f0652 100644 --- a/examples/CLRWrapper/CLRWrapper.go +++ b/examples/CLRWrapper/CLRWrapper.go @@ -8,7 +8,7 @@ import ( "log" "runtime" - clr "github.com/ropnop/go-clr" + clr "github.com/Ne0nd0g/go-clr/v1" ) func main() { diff --git a/examples/DLLfromDisk/DLLfromDisk.go b/examples/DLLfromDisk/DLLfromDisk.go index c9f7a06..027241a 100644 --- a/examples/DLLfromDisk/DLLfromDisk.go +++ b/examples/DLLfromDisk/DLLfromDisk.go @@ -8,7 +8,7 @@ import ( "syscall" "unsafe" - clr "github.com/ropnop/go-clr" + clr "github.com/Ne0nd0g/go-clr/v1" ) func must(err error) { diff --git a/examples/EXEfromMemory/EXEfromMemory.go b/examples/EXEfromMemory/EXEfromMemory.go index 04da98f..8e50449 100644 --- a/examples/EXEfromMemory/EXEfromMemory.go +++ b/examples/EXEfromMemory/EXEfromMemory.go @@ -12,7 +12,7 @@ import ( "syscall" "unsafe" - clr "github.com/ropnop/go-clr" + clr "github.com/Ne0nd0g/go-clr/v1" ) func must(err error) { diff --git a/go-clr.go b/go-clr.go index 6450992..05e3ee0 100644 --- a/go-clr.go +++ b/go-clr.go @@ -212,6 +212,7 @@ func LoadCLR(targetRuntime string) (runtimeHost *ICORRuntimeHost, err error) { } runtimes, err := GetInstalledRuntimes(metahost) + debugPrint(fmt.Sprintf("Installed Runtimes: %v", runtimes)) if err != nil { return } @@ -342,15 +343,21 @@ func InvokeAssembly(methodInfo *MethodInfo, params []string) (stdout string, std defer SafeArrayDestroy(paramSafeArray) + // Ensure exclusive access to read/write STDOUT/STDERR + mutex.Lock() + defer mutex.Unlock() + err = methodInfo.Invoke_3(nullVariant, paramSafeArray) if err != nil { stderr = err.Error() - return + // Don't return because there could be data on STDOUT/STDERR } // Read data from previously redirected STDOUT/STDERR if wSTDOUT != nil { - stdout, stderr, err = ReadStdoutStderr() + var e string + stdout, e, err = ReadStdoutStderr() + stderr += e if err != nil { stderr += err.Error() } diff --git a/go.mod b/go.mod index 8fe6d68..9053dde 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/Ne0nd0g/go-clr +module github.com/Ne0nd0g/go-clr/v1 go 1.13 diff --git a/go.sum b/go.sum index bce9614..5af9418 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/ropnop/go-clr v0.0.0-20200805161622-a9381fbe4fcd h1:dV3XttZUwgC5hx2aKI+UrLORj8D2B2Mjn8SqOd7OfLg= +github.com/ropnop/go-clr v0.0.0-20200805161622-a9381fbe4fcd/go.mod h1:nmfSwDnx0YNeDE16h8+SlEwL0djctDQ8RSer/35eoo4= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= diff --git a/io.go b/io.go index f0e2c75..a42d509 100644 --- a/io.go +++ b/io.go @@ -3,9 +3,12 @@ package clr import ( + "bufio" + "bytes" "fmt" - "io" "os" + "sync" + "time" "golang.org/x/sys/windows" ) @@ -28,6 +31,22 @@ var rSTDERR *os.File // wSTDERR is an io.Writer for STDERR var wSTDERR *os.File +// Stdout is a buffer to collect anything written to STDOUT +// The CLR will return the COR_E_TARGETINVOCATION error on subsequent Invoke_3 calls if the +// redirected STDOUT writer is EVER closed while the parent process is running (e.g., a C2 Agent) +// The redirected STDOUT reader will never recieve EOF and therefore reads will block and that is +// why a buffer is used to stored anything that has been written to STDOUT while subsequent calls block +var Stdout bytes.Buffer + +// Stderr is a buffer to collect anything written to STDERR +var Stderr bytes.Buffer + +// errors is used to capture an errors from a goroutine +var errors = make(chan error) + +// mutex ensures exclusive access to read/write on STDOUT/STDERR by one routine at a time +var mutex = &sync.Mutex{} + // RedirectStdoutStderr redirects the program's STDOUT/STDERR to an *os.File that can be read from this Go program // The CLR executes assemblies outside of Go and therefore STDOUT/STDERR can't be captured using normal functions // Intended to be used with a Command & Control framework so STDOUT/STDERR can be captured and returned @@ -58,6 +77,10 @@ func RedirectStdoutStderr() (err error) { return } + // Start STDOUT/STDERR buffer and collection + go BufferStdout() + go BufferStderr() + return } @@ -77,52 +100,35 @@ func RestoreStdoutStderr() error { // ReadStdoutStderr reads from the REDIRECTED STDOUT/STDERR // Only use when RedirectStdoutStderr was previously called func ReadStdoutStderr() (stdout string, stderr string, err error) { - debugPrint("Entering io.ReadStdoutStderr()...") - - // If nothing was written to STDOUT then Read() will block - // Can't call Close() because the pipe needs to remain open for the duration of the top-level program - // A "workaround" is to write in a null byte so that way it can be read and won't block - _, err = wSTDOUT.Write([]byte{0x00}) - if err != nil { - err = fmt.Errorf("there was an error writing a null-byte into STDOUT Writer:\n%s", err) + debugPrint("Entering into io.ReadStdoutStderr()...") + + // Sleep for one Microsecond to wait for STDOUT/STDERR goroutines to finish reading + // Race condition between reading the buffers and reading STDOUT/STDERR to the buffers + // Can't close STDOUT/STDERR writers once the CLR invokes on assembly and EOF is not + // returned because parent program is perpetually running + time.Sleep(1 * time.Microsecond) + + // Check the error channel to see if any of the goroutines generated an error + if len(errors) > 0 { + var totalErrors string + for e := range errors { + totalErrors += e.Error() + } + err = fmt.Errorf(totalErrors) return } - // TODO Update to use io.ReadAll(), requires GO 1.16 - // https://golang.org/pkg/io/#ReadAll - bStdout := make([]byte, 500000) - c, err := rSTDOUT.Read(bStdout) - // Will return EOF if there is no data to be read - if err != nil && err != io.EOF { - err = fmt.Errorf("there was an error reading from the STDOUT Reader:\n%s", err) - return - } - // If STDOUT is contains more than the null byte we wrote into it, then capture it - if c > 1 && bStdout[1] != 0x00 { - stdout = string(bStdout[:]) + // Read STDOUT Buffer + if Stdout.Len() > 0 { + stdout = Stdout.String() + Stdout.Reset() } - // If nothing was written to STDERR then Read() will block - // Can't call Close() because the pipe needs to remain open for the duration of the top-level program - // A "workaround" is to write in a null byte so that way it can be read and won't block - _, err = wSTDERR.Write([]byte{0x00}) - if err != nil { - err = fmt.Errorf("there was an error writing a null-byte into STDERR Writer:\n%s", err) - return + // Read STDERR Buffer + if Stderr.Len() > 0 { + stderr = Stderr.String() + Stderr.Reset() } - bStderr := make([]byte, 500000) - c, err = rSTDERR.Read(bStderr) - // Will return EOF when nothing was written to it if Close() was called first - if err != nil && err != io.EOF { - err = fmt.Errorf("there was an error reading from the STDERR Reader:\n%s", err) - return - } - err = nil - // If STDERR is contains more than the null byte we wrote into it, then capture it - if c > 1 && bStderr[1] != 0x00 { - stderr = string(bStderr[:]) - } - return } @@ -154,3 +160,41 @@ func CloseStdoutStderr() (err error) { } return nil } + +// BufferStdout is designed to be used as a go routine to monitor for data written to the REDIRECTED STDOUT +// and collect it into a buffer so that it can be collected and sent back to a server +func BufferStdout() { + debugPrint("Entering into io.BufferStdout()...") + stdoutReader := bufio.NewReader(rSTDOUT) + for { + // Standard STDOUT buffer size is 4k + buf := make([]byte, 4096) + line, err := stdoutReader.Read(buf) + if err != nil { + errors <- fmt.Errorf("there was an error reading from STDOUT in io.BufferStdout:\n%s", err) + } + if line > 0 { + // Remove null bytes and add contents to the buffer + Stdout.Write(bytes.TrimRight(buf, "\x00")) + } + } +} + +// BufferStderr is designed to be used as a go routine to monitor for data written to the REDIRECTED STDERR +// and collect it into a buffer so that it can be collected and sent back to a server +func BufferStderr() { + debugPrint("Entering into io.BufferStderr()...") + stderrReader := bufio.NewReader(rSTDERR) + for { + // Standard STDOUT buffer size is 4k + buf := make([]byte, 4096) + line, err := stderrReader.Read(buf) + if err != nil { + errors <- fmt.Errorf("there was an error reading from STDOUT in io.BufferStdout:\n%s", err) + } + if line > 0 { + // Remove null bytes and add contents to the buffer + Stderr.Write(bytes.TrimRight(buf, "\x00")) + } + } +} From 37fb9d6e024604b61709fa20410e00df7e94968e Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Thu, 8 Apr 2021 17:06:43 -0400 Subject: [PATCH 25/31] Modules + Tags Hard... --- examples/C2Framework/C2Framework.go | 2 +- examples/CLRWrapper/CLRWrapper.go | 2 +- examples/DLLfromDisk/DLLfromDisk.go | 2 +- examples/EXEfromMemory/EXEfromMemory.go | 2 +- go.mod | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/C2Framework/C2Framework.go b/examples/C2Framework/C2Framework.go index a9de2f2..167f803 100644 --- a/examples/C2Framework/C2Framework.go +++ b/examples/C2Framework/C2Framework.go @@ -15,7 +15,7 @@ import ( "log" "os" - clr "github.com/Ne0nd0g/go-clr/v1" + clr "github.com/Ne0nd0g/go-clr" ) func main() { diff --git a/examples/CLRWrapper/CLRWrapper.go b/examples/CLRWrapper/CLRWrapper.go index 23f0652..8ef8266 100644 --- a/examples/CLRWrapper/CLRWrapper.go +++ b/examples/CLRWrapper/CLRWrapper.go @@ -8,7 +8,7 @@ import ( "log" "runtime" - clr "github.com/Ne0nd0g/go-clr/v1" + clr "github.com/Ne0nd0g/go-clr" ) func main() { diff --git a/examples/DLLfromDisk/DLLfromDisk.go b/examples/DLLfromDisk/DLLfromDisk.go index 027241a..787d694 100644 --- a/examples/DLLfromDisk/DLLfromDisk.go +++ b/examples/DLLfromDisk/DLLfromDisk.go @@ -8,7 +8,7 @@ import ( "syscall" "unsafe" - clr "github.com/Ne0nd0g/go-clr/v1" + clr "github.com/Ne0nd0g/go-clr" ) func must(err error) { diff --git a/examples/EXEfromMemory/EXEfromMemory.go b/examples/EXEfromMemory/EXEfromMemory.go index 8e50449..602c603 100644 --- a/examples/EXEfromMemory/EXEfromMemory.go +++ b/examples/EXEfromMemory/EXEfromMemory.go @@ -12,7 +12,7 @@ import ( "syscall" "unsafe" - clr "github.com/Ne0nd0g/go-clr/v1" + clr "github.com/Ne0nd0g/go-clr" ) func must(err error) { diff --git a/go.mod b/go.mod index 9053dde..8fe6d68 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/Ne0nd0g/go-clr/v1 +module github.com/Ne0nd0g/go-clr go 1.13 From b14c9eac804e865e551d071b329b69932176d630 Mon Sep 17 00:00:00 2001 From: Alex Flores Date: Wed, 9 Feb 2022 20:59:54 -0500 Subject: [PATCH 26/31] ignore windows error that doesn't impact execution - windows error - ERROR_OLD_WIN_VERSION 1150 (0x47E) --- appdomain.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/appdomain.go b/appdomain.go index 1b17413..3a27058 100644 --- a/appdomain.go +++ b/appdomain.go @@ -1,4 +1,4 @@ -// +build windows +//go:build windows package clr @@ -171,7 +171,9 @@ func (obj *AppDomain) Load_3(rawAssembly *SafeArray) (assembly *Assembly, err er ) if err != syscall.Errno(0) { - return + if err != syscall.Errno(1150) { + return + } } if hr != S_OK { From b96d0f1a991c038adac657179062ec39b1f226ed Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Sat, 2 Apr 2022 07:59:36 -0400 Subject: [PATCH 27/31] Updated supporting packages --- go.mod | 5 ++--- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 8fe6d68..c52a0d6 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/Ne0nd0g/go-clr go 1.13 require ( - github.com/google/uuid v1.2.0 // indirect - golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 - golang.org/x/text v0.3.3 + golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f + golang.org/x/text v0.3.7 ) diff --git a/go.sum b/go.sum index 5af9418..b85a3c2 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,9 @@ -github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= -github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/ropnop/go-clr v0.0.0-20200805161622-a9381fbe4fcd h1:dV3XttZUwgC5hx2aKI+UrLORj8D2B2Mjn8SqOd7OfLg= -github.com/ropnop/go-clr v0.0.0-20200805161622-a9381fbe4fcd/go.mod h1:nmfSwDnx0YNeDE16h8+SlEwL0djctDQ8RSer/35eoo4= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f h1:rlezHXNlxYWvBCzNses9Dlc7nGFaNMJeqLolcmQSSZY= +golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From d57278d3ac46ff5c2078042c441f577c07660de8 Mon Sep 17 00:00:00 2001 From: Marc Coury Date: Tue, 25 Oct 2022 20:51:57 +0100 Subject: [PATCH 28/31] Remove must from go-clr.go --- go-clr.go | 20 +++++++++++++++----- utils.go | 8 -------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/go-clr.go b/go-clr.go index 05e3ee0..615c99c 100644 --- a/go-clr.go +++ b/go-clr.go @@ -89,16 +89,26 @@ func ExecuteDLLFromDisk(targetRuntime, dllpath, typeName, methodName, argument s } pDLLPath, err := syscall.UTF16PtrFromString(dllpath) - must(err) + if err != nil { + return + } pTypeName, err := syscall.UTF16PtrFromString(typeName) - must(err) + if err != nil { + return + } pMethodName, err := syscall.UTF16PtrFromString(methodName) - must(err) + if err != nil { + return + } pArgument, err := syscall.UTF16PtrFromString(argument) - must(err) + if err != nil { + return + } ret, err := runtimeHost.ExecuteInDefaultAppDomain(pDLLPath, pTypeName, pMethodName, pArgument) - must(err) + if err != nil { + return + } if *ret != 0 { return int16(*ret), fmt.Errorf("the ICLRRuntimeHost::ExecuteInDefaultAppDomain method returned a non-zero return value: %d", *ret) } diff --git a/utils.go b/utils.go index 52e4300..08641ff 100644 --- a/utils.go +++ b/utils.go @@ -5,7 +5,6 @@ package clr import ( "bytes" "fmt" - "log" "strings" "unicode/utf16" "unsafe" @@ -25,13 +24,6 @@ func checkOK(hr uintptr, caller string) error { } } -// must forces the program to exit if there is an error using the log.Fatal command -func must(err error) { - if err != nil { - log.Fatal(err) - } -} - func utf16Le(s string) []byte { enc := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder() var buf bytes.Buffer From cb412660836fe8e684e7352db2e20185ab36be5e Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Thu, 10 Nov 2022 18:12:44 -0500 Subject: [PATCH 29/31] Fixed #4 --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ io.go | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..74866ec --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,33 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## 1.3.0 2022-XX-XX + +### Fixed + +- [Issue 4](https://github.com/Ne0nd0g/go-clr/issues/4) - Application's Without A Console Will Not Return Output + +## 1.2.0 2022-04-02 + +### Fixed + +- Fixed an error when attempting to load correctly targeted assemblies through https://github.com/Ne0nd0g/go-clr/pull/2 + +### Changed + +- Upgraded supporting Go modules + +## 1.1.0 2021-04-08 + +- Learned how to correctly use tags and updated import + +## 1.0.0 2021-04-08 + +- Initial tagged version from forked + +### Added + +- Added buffer to collect redirected STDOUT/STDERR \ No newline at end of file diff --git a/io.go b/io.go index a42d509..e9bbce1 100644 --- a/io.go +++ b/io.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows package clr @@ -8,6 +9,7 @@ import ( "fmt" "os" "sync" + "syscall" "time" "golang.org/x/sys/windows" @@ -58,13 +60,41 @@ func RedirectStdoutStderr() (err error) { return } - // Createa new reader and writer for STDERR + // Create a new reader and writer for STDERR rSTDERR, wSTDERR, err = os.Pipe() if err != nil { err = fmt.Errorf("there was an error calling the os.Pipe() function to create a new STDERR:\n%s", err) return } + kernel32 := windows.NewLazySystemDLL("kernel32.dll") + getConsoleWindow := kernel32.NewProc("GetConsoleWindow") + + // Ensure the process has a console because if it doesn't there will be no output to capture + hConsole, _, _ := getConsoleWindow.Call() + if hConsole == 0 { + // https://learn.microsoft.com/en-us/windows/console/allocconsole + allocConsole := kernel32.NewProc("AllocConsole") + // BOOL WINAPI AllocConsole(void); + ret, _, err := allocConsole.Call() + // A process can be associated with only one console, so the AllocConsole function fails if the calling process + // already has a console. So long as any console exists we are good to go and therefore don't care about errors + if ret == 0 { + return fmt.Errorf("there was an error calling kernel32!AllocConsole with return code %d: %s", ret, err) + } + + // Get a handle to the newly created/allocated console + hConsole, _, _ = getConsoleWindow.Call() + + user32 := windows.NewLazySystemDLL("user32.dll") + showWindow := user32.NewProc("ShowWindow") + // Hide the console window + ret, _, err = showWindow.Call(hConsole, windows.SW_HIDE) + if err != syscall.Errno(0) { + return fmt.Errorf("there was an error calling user32!ShowWindow with return %+v: %s", ret, err) + } + } + // Set STDOUT/STDERR to the new files from os.Pipe() // https://docs.microsoft.com/en-us/windows/console/setstdhandle if err = windows.SetStdHandle(windows.STD_OUTPUT_HANDLE, windows.Handle(wSTDOUT.Fd())); err != nil { @@ -132,7 +162,7 @@ func ReadStdoutStderr() (stdout string, stderr string, err error) { return } -// CloseSTdoutStderr closes the Reader/Writer for the prviously redirected STDOUT/STDERR +// CloseStdoutStderr closes the Reader/Writer for the previously redirected STDOUT/STDERR // that was changed to an *os.File func CloseStdoutStderr() (err error) { err = rSTDOUT.Close() From 7aa95c6ac17cb98d71e83678b22aff05f80685df Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Thu, 10 Nov 2022 18:20:22 -0500 Subject: [PATCH 30/31] v1.3.0 --- CHANGELOG.md | 9 +++++++-- go-clr.go | 11 ++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74866ec..c259483 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## 1.3.0 2022-XX-XX +## 1.3.0 2022-11-10 + +## Changed + +- Merged [Pull 3](https://github.com/Ne0nd0g/go-clr/pull/3) from @mec07 to return errors instead of exiting the program ### Fixed @@ -14,7 +18,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Fixed -- Fixed an error when attempting to load correctly targeted assemblies through https://github.com/Ne0nd0g/go-clr/pull/2 +- Merged [Pull 2](https://github.com/Ne0nd0g/go-clr/pull/2) from @audibleblink that fixed an error when attempting to +load correctly targeted assemblies through ### Changed diff --git a/go-clr.go b/go-clr.go index 615c99c..cc5f323 100644 --- a/go-clr.go +++ b/go-clr.go @@ -1,6 +1,7 @@ +//go:build windows // +build windows -// go-clr is a PoC package that wraps Windows syscalls necessary to load and the CLR into the current process and +// Package clr is a PoC package that wraps Windows syscalls necessary to load and the CLR into the current process and // execute a managed DLL from disk or a managed EXE from memory package clr @@ -11,7 +12,7 @@ import ( "unsafe" ) -// GetInstallRuntimes is a wrapper function that returns an array of installed runtimes. Requires an existing ICLRMetaHost +// GetInstalledRuntimes is a wrapper function that returns an array of installed runtimes. Requires an existing ICLRMetaHost func GetInstalledRuntimes(metahost *ICLRMetaHost) ([]string, error) { var runtimes []string enumICLRRuntimeInfo, err := metahost.EnumerateInstalledRuntimes() @@ -210,7 +211,7 @@ func ExecuteByteArray(targetRuntime string, rawBytes []byte, params []string) (r // LoadCLR loads the target runtime into the current process and returns the runtimehost // The intended purpose is for the runtimehost to be reused for subsequent operations -// throught the duration of the program. Commonly used with C2 frameworks +// throughout the duration of the program. Commonly used with C2 frameworks func LoadCLR(targetRuntime string) (runtimeHost *ICORRuntimeHost, err error) { if targetRuntime == "" { targetRuntime = "v4" @@ -310,7 +311,7 @@ func ExecuteByteArrayDefaultDomain(runtimeHost *ICORRuntimeHost, rawBytes []byte // LoadAssembly uses a previously instantiated runtimehost and loads an assembly into the default AppDomain // and returns the assembly's methodInfo structure. The intended purpose is for the assembly to be loaded -// once but executed many times throught the duration of the program. Commonly used with C2 frameworks +// once but executed many times throughout the duration of the program. Commonly used with C2 frameworks func LoadAssembly(runtimeHost *ICORRuntimeHost, rawBytes []byte) (methodInfo *MethodInfo, err error) { appDomain, err := GetAppDomain(runtimeHost) if err != nil { @@ -329,7 +330,7 @@ func LoadAssembly(runtimeHost *ICORRuntimeHost, rawBytes []byte) (methodInfo *Me } // InvokeAssembly uses the MethodInfo structure of a previously loaded assembly and executes it. -// The intended purpose is for the assembly to be executed many times throught the duration of the +// The intended purpose is for the assembly to be executed many times throughout the duration of the // program. Commonly used with C2 frameworks func InvokeAssembly(methodInfo *MethodInfo, params []string) (stdout string, stderr string) { var paramSafeArray *SafeArray From 6176f4aed1d8102c629b863308f2d8d951f4f6b8 Mon Sep 17 00:00:00 2001 From: Ne0nd0g Date: Thu, 10 Nov 2022 18:23:43 -0500 Subject: [PATCH 31/31] Fixed CHANGELOG version numbers --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c259483..ff1d045 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## 1.3.0 2022-11-10 +## 1.0.3 2022-11-10 ## Changed @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - [Issue 4](https://github.com/Ne0nd0g/go-clr/issues/4) - Application's Without A Console Will Not Return Output -## 1.2.0 2022-04-02 +## 1.0.2 2022-04-02 ### Fixed @@ -25,7 +25,7 @@ load correctly targeted assemblies through - Upgraded supporting Go modules -## 1.1.0 2021-04-08 +## 1.0.1 2021-04-08 - Learned how to correctly use tags and updated import