UMassCTF 2023: java_jitters & java_jitters_2 Writeups
Posted 2023/03/26
Over the weekend, I competed in UMassCTF 2023, the capture-the-flag contest hosted by UMass Amherst’s Cybersecurity Club. I competed in the CTF with team CVE2K9, a group of CTF loving friends I found on the fediverse.
This is my writeup for the two Java reverse engineering problems in the CTF: java_jitters
and java_jitters_2
(this writeup has also been crossposted to the team website here)
java_jitters
Description
sips coffee o vault of secrets, teller of wisdom and UMastery, tell me the secret phrase and I shall share my wisdom
File: javajitters.jar
The program provided is run in the command line and asks the user to supply a password as an argument, if the password is correct, the program prints out the flag.
Unpacking the JAR
This is pretty simple, JAR files are just fancy ZIP files with a set directory structure so once you unzip it you get this:
The first thing that you should look at is the META-INF/MANIFEST.MF
file which defines what class is run by Java when executing the JAR:
1 2 |
|
This tells us to look at the main
function within the Main
class in edu/umass/javajitters
Decompiling the Main
class
It turns out that the Java code for this application has been obfuscated using Skidfuscator, so most Java decompilers fail to decompile the main
function within this class. For example, the version of Fernflower packaged with IntelliJ decompiled every other function but did this on the main
function:
1 2 3 |
|
This isn’t great as reading through the Java bytecode, you can tell most of the program logic falls within this main
function. All is not lost though, after trying many other decompilers like Procyon, CFR, and JADX, I found that the version of Fernflower packaged with Recaf gives us enough code for us to figure out the rest1. The code Recaf gave us isn’t perfect and failed to run no matter how many tweaks I made but we can still figure out the code’s logic from it.
I also decompiled skid/Factory.class
and skid/nerzotvnwdsdpsre.class
using IntelliJ’s version of Fernflower2.
Refactoring the code for my sanity
After painstakingly reading through the whole code, I made a refactored/annotated version of the source code.
The first problem when refactoring is that a lot of the Java bytecode uses the invokedynamic
instruction for many function calls which Fernflower hates, decompiling the instruction to something like this:
1 |
|
You can sometimes guess what function is being used based on the context and datatypes of the variables but you can look at the bytecode (using IntelliJ or Recaf) to find what function is being called:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
This can by translated by hand into this: (After consulting Java docs to figure out how StringBuilder worked)
1 |
|
After tediously doing this for every one of the 66 invokedynamic
calls you get proper Java code!!!
I also renamed the 0o$Oo$K
, K0o$KOo$KK
, and Oo0o$OoOo$OoK
functions into crypto_func1
, crypto_func2
, and xor
respectively.
Tracing crypto_func1
and crypto_func2
I looked at crypto_func1
first after realizing it was called very early on in the main
function. From reading the code and running through it line by line in JShell3 with the args crypto_func1("password", 58777307);
4, I figured out that the function would SHA-256 hash whatever string was passed to it and then call crypto_func2
. There is one common code pattern to look at here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
A similar code block to this with different variables pops up once within crypto_func1
and very often in main
. Running through it line by line you will see that it initializes a StringBuilder
object with ciphertext, does some sort of decryption character by character with var9
being used as a key, and then converts the decrypted StringBuilder
to a String
. This is Skidfuscator’s string encryption, obfuscation which we can bypass by keeping track of the key variable’s value as the program runs.
Tracing through crypto_func2
in a similar manner, you will find that it just takes the hash that crypto_func1
generated in byte[]
form and converts it to a hexadecimal string.
One thing to note is that as another obfuscation method, you won’t find return
statements within these two functions. Instead, you will find nerzotvnwdsdpsre
exceptions being thrown. This is a custom exception created by the obfuscator that looks like this:
1 2 3 4 5 6 7 8 9 10 11 |
|
This exception gets used like this to simulate return
statements:
1 2 3 4 5 6 7 8 |
|
Analyzing main
With those two functions out of the way, let’s start analyzing the main program code. First we have to find the key which I’ve called int1
:
1 2 |
|
This key is initialized with a seeded random number generator:
1 2 |
|
This means our key starts with the value 849836953
.
The program starts by checking that a password is supplied, then over a series of calculations (traced through JShell), the key changes to 2024113895
. Throughout these calculations, functions in the Factory
helper class are called which appear to be a checksum function to make sure that the key is correct throughout the program’s execution. The program then calls crypto_func1(str2, 58777307)
to SHA-256 hash our password.
The rest of the program is a series of nested if-else blocks which do the following:
- Performs a XOR calculation against some integer literal to calculate a new key
- Decrypts some ciphertext to a known SHA-256 hash (using the method outlined above)
- Compares that hash against the user’s password hash
- If the hashes match:
- Performs a XOR calculation against some integer literal to calculate a new key again
- Decrypts a message to output to the user (using the method outlined above)
- Prints out the message
- Exits the program
- If the hashes don’t match, do more XOR calculations and
Factory
checksums to change up the key and repeat
There are many known passwords as some output easter egg messages like Congratulations, you've unlocked the secret to a perfect cup of Java!
. If none of the passwords match, the last else block prints a password not found message.
Tracing the program in JShell, halfway into the code we get to a block where the key is 1140202191
and has a SHA-256 hash of 8900fbb69012f45062aa6802718ad464eaea0854b66fe8916b3b38e775c296a8
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
|
Decrypting the message of this block, we get our flag: UMASS{C0ff33_m@k3s_m3_J@v@_crazy}
java_jitters_2
Please please please don’t look down here until you’ve understood the java_jitters solution above. It builds on the previous solution significantly.
Description
sips coffee if only i remembered the password to my amazing app… maybe i could get those java beans coins…
File: javajitters_v2.jar
Assumptions from java_jitters
Like the original java_jitters challenge, we can unpack the JAR and decompile it with Recaf’s version of Fernflower. The code is also obfuscated using the same Skidfuscator tool, so the string literals encryption algorithm is the same (just with a new key that we need to trace). Like the original problem, there are a lot of invokedynamic
instructions that you will need to convert to Java code by looking at the bytecode.
I also assumed that the 0o$Oo$K
and K0o$KOo$KK
functions were the same SHA-256 password hashing functions from the original challenge and did not analyze those.
Key differences from java_jitters outside of main program logic
Like the original challenge, java_jitters_2 also uses exceptions to replace return statements, but this time there are two exception classes. xpbyayedzpfnsdwh
replaces string return statements and edyxsdbugbromxsl
replaces byte array return statements.
This byte array return exception gets used in a new function called Oo0o$OoOo$OoK(String var0, int var1, int var2)
which I renamed to decode_data(String ciphertext, int len, int seed_arg)
for reasons I will discuss later.
Somewhat annotated/refactored version of the code available here
Analyzing the main
function
Like the original problem, we can trace the program flow and the encryption key using JShell. The key is initialized in the same way as the last problem:
1 2 3 4 |
|
The program then proceeds to check the number of arguments supplied and then perform XOR and Factory
checksum operations to change the key and then hashes the user’s password.
The big difference between this program and the last is that instead of constantly checking hashes against known hashes in if-else blocks, we start by building a dictionary of known hashes and their respective message data (encoded in Base64):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
The program does this with 11 different sets of hashes and messages, changing the key every time. This gives us a dictionary that looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
The program then checks if the hash from the user’s password is found in the dictionary (which we can ignore when tracing in JShell) and then moves on to a message decoding and printing code block:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
It starts by grabbing the message data for the hash provided, decoding the data with Base64, and then runs the decode_data
function on the decoded data with the user’s password length and 133764025
as arguments. From this, we can deduce that decode_data
is a decryption function for the message that takes password length as a key.
This is a pretty simple key to brute force, which we can do for every message in JShell:
1 2 3 4 5 6 7 8 9 10 |
|
This gives us all the messages in the program like You must have had a triple-shot espresso to crack this password!
and *sips coffee*
, but most importantly it also gives us the message that contains our flag: UMASS{r3v3rsing_j4v4_1s_4_j1tt3ry_j0b}
All the source code that was relevant for both java_jitters and java_jitters_v2 in decompiled and annotated/refactored forms can be found here
-
Recaf being able to do it but not IntelliJ is interesting since JetBrains is the creator of Fernflower so you would think the IntelliJ version is the best/latest version ↩
-
Turns out these two classes and everything under
skid/
is Skidfuscator’s helper classes ↩ -
JShell for the uninitiated, is Java’s secret REPL shell which allows you to test Java code in an interpreter like Python’s interpreter ↩
-
password
was just a test,58777307
was a constant from how it was called inmain
↩