Solving Java Reversing Challenges - Noverify's Crackme 3
TLDR
This time, we solve a Java crackme which focuses on InvokeDynamic instruction and has some basic obfuscation. We analyse the java bytecode instructions and use regex to bypass obfuscation.
And then, experiment with dynamic instrumentation to later debug and understand it. Also we talk about the tooling process often required to solve java reversing challenges.
Challenge
Download noverify’s crackme 3
Introduction
Ahh, So It’s been a while I’ve written anything!
The last two weeks I’ve been fiddling around with reversing java apps and got to try these cool crackmes by @graxcoding.
So the third one checks our input of a 64 bit long integer to validate it.
Jadx fails to extract any classes, not cool!
Also I tried using the jar tool’s xf
option and other tools like Bytecode Viewer but got the same result ¯\(ツ)/¯
Then I remembered a video from MalwareAnalysisForHedgehogs where he used this dumper as a java agent to dump the classes from an obfuscated jar file.
Basically our agent class must implement a public static premain method similar in principle to main method. After the JVM has initialized, the premain method will be called, then the real application main method.
It worked perfectly and we have the me_nov_crackme_CrackMe.class
file and can work on it.
Static Analysis
At first I tried using JADX again but failed…
Then I used Bytecode Viewer and some of the decompilers it comes with but sadly those didn’t work too and I’d had to go with the bytecode.
I’ll only dig up into some of the bytecode instructions, so if you are not quite experienced with JVM I’d recommend the following tutorials :
JavaCode To ByteCode - James D Bloom
Java Bytecode Crash Course - David Buck
Also, And as a reference for the JVM instruction set we have the following:
Oracle Java SE 7 Doc - The JVM Instruction Set
Wikipedia - Java Bytecode instruction Listings
I just copied the bytecode from Bytecode Viewer and started analysing it.
It looks obfuscated but we can easily notice what is it doing …
For instance look at the following bytecode:
1 | L1 { |
This can be interpreted as the following pseudocode :
1 | if(args.length != 1) { |
And just after this we have what looks like a try-catch block.
1 | L34 { // try block |
Could easily be converted to the following:
1 | try { |
Actually I noticed quite a pattern with the xor instructions and wrote a short python script to just comment the result for it.
1 | import re |
The first regex searches for two consecutive ldc
instruction on Integers.
And the latter one just checks for one ldc
and other dup
which always results in 0.
The result of these xors is used as predefined integers in the program.
And after some such xors we find some Congratulations and sorry strings which leads us to the checking instruction that makes use of one of the methods ie. method 0
and passes an array to it as an argument.
Tracing back to the array initialisation I found that there were actually two arrays :
1 | L5 { |
Taking help from the instruction set manual we see that they are both byte arrays.
Subsequently we observe bastore
in every label below which does some calculations and store the result in these bytearrays.
So only the array with length as 3 is passed and which depends on our input.
Then we can see that there are several methods named numerically other than 0 and I explored them the other day.
Indy in Action
Continuing further it is important we understand about invokedynamic.
We can easily observe the InvokeDynamic instruction at the return of every other method other than main.
1 | invokedynamic me/nov/crackme/CrackMe.127([Ljava/lang/Object;)Ljava/lang/Object; : 64([Ljava/lang/Object;)Ljava/lang/Object; ([Ljava/lang/Object;)Ljava/lang/Object; |
The invokedynamic (or simply Indy) is used for optimization and creating efficient java programs and implements a runtime system that can choose the most appropriate implementation of a method or function after the program has been compiled.
InvokeDynamic 101
InvokeDynamic for Mere Mortals
Also checkout how are lambdas in java implemented with making use of invokedynamic here
For examples we have the following implementations :
- Lambda Expressions in Java 8+:
LambdaMetafactory
- String Concatenation in Java 9+:
StringConcatFactory
For instance, in newer versions of Java, String Concatenation is not done by appending the string elements multiple times using StringBuilder append function, instead it places those in an array and makes use of invokedynamic and StringConcatFactory to have a single method call.
Furthering researching about the topic I came across this post by the official JEB blog.
https://www.pnfsoftware.com/blog/android-o-and-dex-version-38-new-dalvik-opcodes-to-support-dynamic-invocation/
And I thought to give JEB Pro a try to see how well it handles it. Apart from some ambiguous variable names all is fine.
So basically for indy there is a bootStrap method that creates a callsite which points to a handle to a predefined method.
Here 127
looks like the bootstrap method!
1 | public static Object 127(Object[] arg8) { |
Basically, it returns a callsite which points to a method handle it finds when searching for a static method with the value of fun in the class Crackme.
Reference - MutableCallSite
Algorithm
1 | public static Object 0(Object[] arg5) { |
It converts the string passed as an array to a 3 digit integer and does the following operation on it.
1 | (( get_hashcode(callee_fcn) * arg) * 0x7FFF << 3 | 127) & 0xFFFF |
So, It monitors the execution flow and depends on the callee function. And later converts it to a utf-8 string and stores it into a static variable fun
.
It passes the integer result xored with a specific value as an argument to the method called as a result of invokedynamic.
All the other methods are mostly similar to this except that they differ in this xor operand value.
For extracting those, we can just use a simple regex. Also for 127 we can just fake it with a 0.
1 | import re |
1 | (Thread.currentThread().getStackTrace().length > 127) |
We see that it checks if the no. of functions executed in a run is greater than 127 it returns true which should then validate out input.
Perfect this is all we need to know to write a short python script.
1 | nice_fcns = [0,63,127,255,462,740,896,1217,1721,1914,2457,2697,3300,3994,4535,4647,5078,5256,5311,5594,5680,5751,6139,6273,6544,6640,6740,6866,6956,7039,7127,7205,7334,8392,8489,8591,8963,9828,10245,10310,10359,11024,11208,11394,11775,12009,12071,12505,12531,12689,13158,13529,13910,13946,14071,14256,14643,14981,15004,15021,15170,15725,15889,15903,15999,16632,17038,17270,17362,17470,17594,18318,18415,18642,18809,18943,19300,19493,19699,19813,20069,20753,21238,21380,21522,21855,21867,22095,22909,22966,23046,23167,23469,23651,23762,23979,24112,24303,24352,24395,24440,25087,25167,25366,25592,25895,26142,26206,26442,26746,27153,27647,27728,27903,28356,28537,29070,29177,29329,29648,29760,30695,30827,31246,31314,31533,31709,31797,32014,32263,32384,32681,32815,32896,32937,33201,33323,33467,33511,34315,34374,35511,35812,36141,36234,36256,36382,36556,37137,37298,37790,37918,37995,38143,38435,38867,38940,38974,39041,39108,39166,39309,40355,40726,40791,40986,41166,41503,42447,42534,42704,43135,43263,43521,43557,43730,43779,44178,44677,44734,44797,44943,45007,45966,46337,46396,46528,46979,47000,47533,47846,47921,47948,48207,48557,48726,48975,48996,49636,49667,50383,51030,51358,51578,51583,51692,51702,52032,52101,52184,52396,52500,52530,52538,52556,52931,53184,53275,53324,53564,53774,53887,54212,54764,55039,55295,55342,55359,55375,55774,55938,56138,56452,56564,56816,57016,57107,57508,57569,57611,57779,57796,58153,59099,59380,59406,59575,59829,59875,60282,60756,61041,61087,61428,61495,61565,61664,61703,61839,62288,62561,62614,62719,62991,63231,63256,63670,63882,64009,64051,64127,64191,64625,64934,65018,65155,] |
Here we check for maximum length of stacktrace which can be achieved and to my surprise it was 22!
Also there are 3 results(191,465,739) with the same stacktrace which ends at 7039.
1 | ('len(stackTrace) = 22', [0, 55295L, 25087L, 255L, 15999L, 64127L, 63231L, 11775L, 23167L, 62719L, 43135L, 27647L, 18943L, 38143L, 51583L, 53887L, 27903L, 55039L, 43263L, 7039L]) |
Then I moved on and wrote used z3 to find the exact 64 bit integer which results in those 3 digit values. Note the operations on the input.
1 | from z3 import * |
But indeed it validated successfully !!
Dynamic Analysis
So, what did I miss?
I wanted to check what was going under the hood and tried some debuggers like jdb(didn’t work).
Also I came across Dr Garbage Tool’s Bytecode Visualizer.
This is an old eclipse plugin set and doesn’t work with newer versions of eclipse.
I was able to install it but alas I don’t know why it wasn’t able to identify the main method.
At last, I reached out for some java bytecode editors to add debug print statements.
Recaf is very easy to use and got a nice UI as well so I went with it.
Also I asked Col-E(Recaf’s Developer) about any good bytecode debuggers and unfortunately, it turns out there aren’t any as of now!
I also shared the weirdness of this jar in extracting the class.
And, Then I got to know about the forward slash trick which was pretty obvious from the jar verbose extraction that I didn’t observe carefully before.
Obviously the decompiler view doesn’t work so we’d have to switch to the class table mode.
Select method_0 and edit with assembler.
We can just add a System.out.println()
for variable null1 shown.
If everything goes well, we should see this output.
We can do the same for the fun variable.
But we need some automation for adding these instructions in every method.
And currently Recaf doesn’t have any Automation API.
So to resolve this problem I turned to dynamic instrumentation.
Dynamic Instrumentation using ASM
Just FYI I’m new to the instrumentation part so I checked out some frameworks/libraries which could help me with it.
As it turns out there are several options and I tried some of them such as JavaAssist, ByteBuddy and ASM.
But ASM is at the lowest level and is the base for Bytebuddy and cglib as well, so I went with it!
Checkout this stackoverflow answer for more on Analysis of bytecode libraries
ASM User Guide and Tutorials
- https://asm.ow2.io/asm4-guide.pdf
- http://www.egtry.com/java/bytecode/asm/
- https://www.tomsquest.com/blog/2014/01/intro-java-agent-and-bytecode-manipulation/
- https://stackoverflow.com/questions/tagged/java-bytecode-asm
For verifying your ASM Implementation and how ASM reads your class you can checkout ASMifier.
Here we have v5
as var_1
and the following condition adds these three lines of code after it encounters any (ISTORE, 1)
instruction.
1 | if (opcode == ISTORE && var == 1) { |
Same goes for logging the static variable ie. fun
.
1 | if(opcode == PUTSTATIC && name.equals("fun")) { |
I added a string ie. CrackMe.fun =
to differentiate both of them.
Cool lets see how it ends.
Weirdness of method_63
Ahh as you can see, I missed the most important part of this crackme, ie. UTF-8 LOL !
So single surrogates are illegal and are converted to ‘ ? ‘ character (ie. 63).
FYI Surrogates are characters in the Unicode range U+D800 - U+DFFF.
And Here we notice 56575 which is then converted to 63.
Here it inverts the comparision sign and validates if the length of stacktrace is less than 127.
1 | (Thread.currentThread().getStackTrace().length < 0x7F) |
So the stacktrace length check for other methods is bogus and is only used for deception.
Also the v5 in method_63 always results in 127 which returns true halting the program.
1 | (( get_hashcode(callee_fcn) * arg) * 0x7FFF << 3 | 127) & 127 |
So now we know what were we missing.
Solution and Source files
I’ve uploaded all of my solution files along with the ASM Agent project for this crackme on my github.
mrT4ntr4/Challenge-Solution-Files/noverify_crackme_3
Also krakatau does a good job that I got to know about from the author.
Also some of the source files were disclosed by him:
- Original Driver Source code
- Keygen for methods
- Final Keygen
- Original ASMifier source code for final crackme
And, some manual obfuscation was done afterwards.
I enjoyed this challenge, all thanks to @graxcoding for making it!