Welcome to another vulnerability report, this time for version 3.4 of the software. This won’t make much sense on its own so if you’re unfamiliar with TimeLock I suggest you read previous reports (particulary for V3.1 and V3.2) from here: https://www.algomachines.com/people
Challenge lockbox details:
Start Time: 17/02/2020 00:00 UTC
End Time: 17/03/2020 00:00 UTC
The current version of TimeLock is similar to 3.2 and 3.3 in that it employs a runtime interpreter to protect the sensitive parts of the code that a cracker could otherwise abuse. It does so quite successfully in my opinion, but deeper analysis of the decryption chain after the interpreter is finished gives away something else that’s quite easy to take advantage of.
The addresses referenced in this report are Virtual Addresses (VA), rebased at 0x0, or File Offsets(FO).
The same time-based antidebug is still there, although one of the functions was changed very slightly. Not going into it, but still the approach is to NOP both checks. The addresses for the checks are: FO 2FD8D (replace with 4 NOP bytes — NOP=0x90) and FO 2043C8 (replace with 4 NOP bytes).
This version of the tool proved to be a much bigger challenge than the previous ones. One of the main reasons for this is the extra obfuscation layer that seems to be present everywhere in the encryption/decryption chain. I was happy to see that, and I personally think it adds a very strong layer of protection against someone who would try to explore the application for the first time (aka without any binaries of the previous versions and no reports). Since the vulnerability in V3.3 was related to the Hashing function at the end of the interpreter script, I was expecting this version to have that changed, which turned out to be correct. The hashing function now seems to be agnostic to the time difference, meaning it can no longer be abused in that direction (even though the function itself is not compiled at runtime).
After spending quite a bit of time on the interpreter and failing to decrypt the lockbox, I figured I might as well go further down the decryption chain and see what else lies there. For this purpose, I created 2 test lockboxes of my own, one that was openable straight away and one that was locked. The aim was to see where and how the output hash from the interpreter was used.
Fair bit of warning, I have no idea how to properly explain this vulnerability in a way that anyone except the author or someone who goes hands on with the debugger will understand. Sorry.
I will *not* go into details on how I traced this, because it was mostly just manual labor, going step-by-step through code until finally I saw a place where the resulting hash was used, in the function at VA 39D20. Now, if we look at the function in IDA,
it honestly looks quite ugly. But a very close exploration of it using the debugger (using both openable and locked test boxes) will reveal one essential fact:
&v42 (the red square in the image above) is a pointer to the hash that was obtained (much) earlier in the interpreter. It is the only variable in there that is dependent on the time, and so it is the only barrier between us and an openable lockbox. But it’s a (pseudo-)random 64 bit number, so it’s quite unlikely we’ll be able to guess it.
Now, tracing through the steps straight past this point (again, with a debugger), we can note something crucial:
v19 (the orange square in the image above) is a value that is the only “inheritor” of time dependence given by the hash. Basically, some mathematical operations take place that use the hash as input and yield an output of a suitable form for what comes next.
Following that, a loop is entered in which a stream-style operation on a buffer of data is done, with v20 as the initial key (the key itself is updated after every decrypted block, but v20 acts as the IV).
Finally, there’s this check, right after the loop:
We can see that v34 is the return of the function, so this part checks if the stream decryption was correct or not, and returns 1 if it is. This is the crucial point where the decryption diverges between an openable and a locked box. Now, v3 is defined earlier as v3 = a3, meaning that it’s an input argument to the function. In other words, this is the “reference” value against which the stream decryption is compared against, and we already have access to it. And v21 is just a pointer to the start of the decrypted buffer.
So we can now make out a story of what’s going on:
- The hash (v42) gets crunched into v19
- v19 is fully used to determine v20, which is a sort of an IV for a stream cipher that gets applied to a buffer
- The stream cipher runs all the way, and finally, the first 2 decrypted uint64 values of the cipher are compared against the reference a3 which we have access to beforehand
- If the stream cipher decrypt matches the reference, the algorithm has “validated” the date, and lets the execution proceed. Otherwise, it would seem the date isn’t in the correct interval.
The “breaking point” of this whole algorithm can only be discovered empirically (by debugging several lockboxes) and it’s this:
The v19 variable has a single value that will unlock a box (since it’s derived from the hash which also has a single good value). Furthermore, it will always be a small number! From my tests, it was never a number higher than 0x400, regardless if the box is locked or openable. That means that unlike the hash itself, this number is entirely bruteforceable! I liked this theory but on the other hand, running the program 1000 times and changing everything manually would be extremely tedious and time-consuming. And very frustrating if it would fail to give results after many hours of mindless runs. So let’s find a nicer approach.
Opening the lockbox
Okay, this part *might* get confusing if you don’t have a decent background in debugging/assembly.
First, let’s open TimeLock in a debugger. Inside the TimeLock.exe module, place a breakpoint at VA 39EA5. Open the challenge lockbox and start the decryption process (with password ED44B5D2–3421–4383-A934-CAB0523A2D25). Once the breakpoint hits, there’s 2 things to note. The v19 value is in the RCX register (and should be 0x280 as of right now, but it will change at least once before the lockbox can be open). The register R15 contains a pointer to the reference value for validation of the decryption (fun note, the reference is a constant built in the code, it doesn’t change between lockboxes — not that we’d care about that anyway). The reference is a 128 bit number, 0x54FB9BB2983CCE864172E15910AA8655. This is the number that we need to match after the decryption is done.
Now it’s time to creatively repurpose the contents of the function to bruteforce for the correct decryption. The hypothesis is that for exactly one of the numbers that v19 can be, the first 2 blocks of the decryption will be equal to our reference. And that the number itself will be low enough to bruteforce.
We need a counter variable that can hold our current v19 value and increment it after every check. We can use R12 for this, as it isn’t used anywhere else, so there’s no fear of the code messing with it.
A pseudo algorithm of what we want to do looks like this (note that R12 is already 0 so there’s no need to re-initialize it):
Below you can find an image of the modifications that have to be done to the asm code (in red). Another breakpoint is placed at VA 39F5E in order to intercept the code once it found the correct solution, and the previous one at VA 39EA5 is removed because that instruction will be hit over and over again and we don’t want that.
Aaaaaand, run it! In just a few miliseconds you should end up at the breakpoint. Checking the result, R12=0x11 at the time of writing. Write your own result down. All right, now let’s test it out.
Open another instance of the debugger, with a new instance of TimeLock. This time, only place a breakpoint at VA 39EA5, just like the first time. Start the decryption and wait for it to hit. Once it does, overwrite the RCX pointer with the new result (0x11 for me) and let resume execution. Voilà!
I really like the evolution of TimeLock. It seems quite safe already and I believe that without access to the previous versions and these write-ups, a cracker would have quite a hard time to navigate the code, especially with the obfuscation included in the recent version. There are probably still some minor things to polish, like the above function. It’s obviously a very big flaw to count on small, bruteforceable numbers for anything. But I feel this weak spot can be patched fairly easily.
I would like to thank the author for this extremely interesting challenge. Hopefully this report will help in making TimeLock secure and unbreakable, and, of course, I’ll be eagerly waiting for future challenges.