The promised reverse engineering writeup is finally here. This post will feature 3 challenges: random security, maze, and image.
While image is categorized as crypto, I feel like that’s a gross misrepresentation of the challenge, so I’m putting all 3 as rev.
Let’s start out with the most basic challenge: random security.
Random Security - 452 - Medium
One of my friends recently learned Java and started teasing all of us for not knowing anything about programming. He made what he called a secure program and challenged us to steal some flag from it. I have no idea where to even start, could you help out?
We’re not given any source, so let’s just connect to the server and see what we get.
Seems like we have to generate another random number and send it back. Given the flavortext of the challenge and the server messages, we probably have the generate the result of the nextDouble method of the Random class.
The server seems to be giving us a single output of nextDouble as well, so somehow we need to recover the random seed from that, and then generate the next double for the server.
Let’s take a look at the relevant source code for Random:
The next method is the core of the RNG, and it’s pretty simple. It’s just a linear congruential generator (LCG) with a 48-bit modulus.
The values of multiplier, addend, and mask are all constants, so we can just hardcode them if needed.
To generate a double like the server is doing, there are 2 calls being made to next. This means that we are able to extract the top 26 bits from the first next call, and the top 27 bits from the second next call.
However, this still isn’t enough to completely predict all the outputs. We still don’t know the full value of seed.
Thankfully, because we’re given the top 26 bits of the state before the second next call, we can easily brute force the remaining 48 - 26 = 22 bits.
To verify, 2 ^ 22 = 4194304, which is tiny in terms of brute forcing.
To save time, I just directly ported over the algorithm listed here into Python, and it worked perfectly.
Now all we have to do is reimplement the next and nextDouble methods.
Finally, we can send the next generated double back to the server and finally get the flag. Before we generate the next double though, we do have to make
a single next call, as the seed we recovered is the seed after the first next call in the nextDouble method, so we still need to go through the second one.
Here’s the full script:
After running for just a few seconds, we get our flag:
0.19317240683223058
1739942358855791
00110001011100111011111100 110001110101011000001101111
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 4194304/4194304 [00:03<00:00, 1110484.95it/s]
[54373198860349]
Correct! Here is your flag: bucket{RaNd0m_nUmb3r5_53cur3_d24d8c961}
Now let’s see how we can take this a step further with our next challenge, maze.
Maze - 478 - Hard
After you solved my last challenge I have upped my game. This time there is no way you will find the flag. Im so confident you will lose that I will give you the file to the game. Good luck! You’re gonna need it.
This time we’re given a .class file, so let’s decompile it here again to get the source code.
Thankfully, it’s not obfuscated like Troll was, so we can actually read it.
It’s a bit long, so let’s start decomposing it. Let’s start from our goal, the getFlag function.
The only place this is called is at the end of the main method:
Which means to get the flag, we somehow need to throw an ArrayIndexOutOfBoundsException.
Looking at the rest of the main method, we can clearly see the intended way to do this: We just need to somehow escape out of maze and reach the border.
This while true loop implements a basic movement system that looks like this:
QWE
A D
ZSC
Where Q moves up and to the left, W just moves up, and so on. If we ever move onto a #, the loop ends and the program exits. This means we have to somehow escape the maze without ever touching a #.
We also have a special input, R, that gives us a randomly generated double. Hmm, I wonder how this could help us…
Let’s look at the final part of the code, initMap:
First, we have the actual maze generation:
Then, the maze gets printed out. This should be the first thing we see when we connect to the server, so let’s do that as well as see what we get.
Note the extra wrapper of spaces on all 4 sides of the grid. That should be the area we are trying to reach, as any outward movement in that wrapper should move us out of bounds and throw the necessary error needed to get the flag.
But how do we get out of these concentric walls?
Well, there’s one last part to the challenge. Remember how we’re given random.nextDouble outputs? This is where that finally comes into play.
(Oh, and the last line just sets maze[20][20] to the player)
Anyways, back to the random part. We loop 10 times, and each time, we generate a random number from a calculated bound. We also generate another rounded number bounded by 4, and from that number, we set a different part of the maze matrix to an empty ' '.
Since I didn’t want to go through the effort of reversing exacty that the calculations of bound meant, I just moved the maze printing statement to be right after this loop instead. (Remember to leave the if statement behind, as it’s used to initialize the empty squares).
Running the file locally, we can now see exactly what’s going on:
This loop actually randomly generates holes into the walls! This means we can actually escape the maze once we’re able to recover where the holes are.
Because the maze is only printed once at the beginning without the holes, we need to completely recover these holes through their random generation, and then move through each hole completely blind.
But there’s just one problem: we can recover the random seed from the nextDouble, but how do we loop backwards to get the previously generated nextInts?
m=0x5DEECE66Db=0xBs=initial seedFor every call to next(bits)s=ms+bmod248return s>>(48−bits)
Because LCGs are defined as just affine (mx+b) functions modulo some fixed number, they are actually reversible if you know their parameters. In this case, Java’s Random parameters are all publicly known, so we can easily use this to convert a reversed form of our Java Random LCG.
Mathematically, this can be done as long as we can find m−1 in the modulus (which happens to work for Java’s value of m):
Let’s fix up our previous solve script for random security. At the same time, let’s also ensure it returns the value that next would have returned if it were called from the previous seed state (You’ll see why we want this later).
First let’s wrap the random seed cracking into its own function, as we know it already works. Then, from the seed recover, we can slowly start working backwards.
We need our next function again:
Let’s see how we could create a prev function now. We know that we have to step back using the inverse of the LCG, but we also want it to return what next would have returned from that random state. An easy way to do this is just to reverse once, call next, and then reverse again. No extra work required!
Now we can remake nextDouble and prevDouble to check that we’re reversing correctly:
Just to test:
But remember, the random holes were made with nextInt, so we need to reverse that as well! Here’s the normal nextInt ported to Python:
While the loop may look hard to reverse, a dive into Java docs actually reveals that this loop rarely ever rejects in the first place, so let’s just replace all the next calls with prev and call it a day.
Finally, we can start reversing the holes. Remember that since the holes are generated by a for loop, we need to iterate backwards to get the correct values. On
each loop iteration, we also need to generate the nextInt(4) first, and then the nextInt(bound).
Finally, we need some way to generate the correct path to get out. Since the holes are completely hidden on remote, I just chose to do it locally with BFS and then copy the path
over to remote.
Now all we have to do it get a random number, pass it into our script, and submit the path to win!
Full script below: