TimeLock (https://www.algomachines.com/) is a freeware program being developed in order to facilitate secure temporary locking of files up to 10KB in size, with the unlocking being possible only during a time window specified by the user during initial locking.
In it’s current iteration (V3.1) locking requires the user to provide an input file, a password and the start and end date during which time the lockbox can be open. The time check is based on the bitcoin blockchain, as the program connects to several bitcoin nodes and queries the header files for recent blocks.
The TimeLock Challenges have been launched by the software creator in order to test the time check part of the encryption algorithm for vulnerabilities. This means that a lockbox is supplied, together with the password, and the goal is to open it before the start time that it has been designed for.
This challenge was the 9th to be launched (you can view all of them here: https://www.algomachines.com/people), with the time limit of 26 January (or in other words, it was designed to be openable on the 26th).
I will not go into a lot of details about the general behaviour of the application since it is unchanged from the previous iteration (the report can be found here: https://medium.com/@elronvhubbard/timelock-v2-2-challenge-vulnerability-report-bb3cd61307c7 ) with the notable difference that the encryption algorithm was improved. The addresses referenced in this report are Virtual Addresses, rebased at 0x0.
Since the previous iteration’s lockbox files were vulnerable to patching of the encrypted start/end dates (as in the previous report), my first approach was to simply generate 2 identical lockboxes and examine the encrypted output. A quick comparison in a hex editor of the two generated files shows that they are completely different, unlike previously. This means that some sort of salt is used during the generation of each lockbox. Instead of starting to analyze the new methods implemented, I opted to look at where the decryption was being done and seeing if I could find weakness there.
In the previous build of the application, the anti-debugging present was in the form of some time checks which would crash you if the execution was stopped for a long enough time. As far as I’m aware, nothing really changed in the new version, but I felt like I was getting crashed faster and it was more annoying so I decided to take a closer look at it.
The first thing I did in this regard was to open up the imports table in IDA and search for QueryPerformanceCount. It’s only ever used in one single function, and if we use IDA to generate the pseudocode for the function, it looks like this:
It seems like this function is being run over and over at a fixed interval and if the delay between runs is not that exact interval, the security cookie is poisoned and the app fails. In order to bypass this, we can simply remove the instruction inside the innermost “if”. This is done by replacing the ”cmovz rax, rcx” instruction, which is 4 bytes long, with 4 bytes of NOPs (no operation). The patch has to be applied at file offset 21F3F8:
48 0F 44 C1 (cmovz rax, rcx) -> 90 90 90 90 (nop nop nop nop)
However, after saving the changes and trying to debug again, we still get crashed. In order to find this second crash I started looking at what threads were running during the decryption process, and I noticed that at least two threads were looping this named function:
So here again we can see that there’s a comparison done, checking if the elapsed time between the previous measurement(stored in qword_2EA048) and the current measurement are lower than a threshold. So let’s just patch that out so that it always thinks it’s lower. Since we want the code to always take the main branch of the “if”, we can just remove the jump instruction (by NOP-ing it). The patch has to be applied at file offset 43251:
76 0E (jbe 43E61) -> 90 90 (nop nop)
Aaaannd now if we attempt to run the code again under a debugger, we can make pauses as long as we feel like, with nothing ever bothering us. This makes analyzing and testing much more convenient.
Finding the vulnerability
Since I analyzed the previous challenge thoroughly, I knew where the final decryption function was, the place where the important date-time checks are made. Finding the same function in the new version was straightforward because the beginning part of the code didn’t change (nor was I expecting it to), so I only needed to do generate a byte pattern in V2.2 and search for the same byte pattern in V3.1. It’s a long complicated function that starts at Virtual Address 44F40. In the previous challenge I didn’t analyze much of it so that was my focus now.
Now, this is the most interesting part of the code:
We can see in the last line of the cropped image that “result” is 1 if everything happens according to plan, meaning if the decoding is successful. If we start looking at the functions before that, one of them is the one I’ve named “magic_decode_function” (just so I could find it easily), with the address at 69A70. Here it is:
So obviously this won’t be easy to understand just from looking at it, but I’ve played it with a debugger over and over, with different test lockboxes, some of them openable and some locked, in order to see what’s going on there.
First of all, it’s important to note that v25 holds the encrypted current date period calculated directly from the blockchain. I didn’t figure out where that was going on because the application launches dozens of threads to connect to peers and it’s a bit of a pain to monitor them all. After v25 is initialized, there’s quite a few parameters that get extracted from the lockbox file and they’re all used together in order to calculate a magic number, denoted here by v27. That number is then being fed into the sub_BC70 function which does some encryption operations on it. Finally the encrypted output is checked against the encrypted blockchain period, and if they’re different, the blockchain time is returned.
Now moving focus onto the instruction
v27 = v22 * v22 + floor(v17)
the debugger shows clearly that v22 is time agnsotic (the current date plays no part in its value) so the only thing that is time dependent is v17.
Now, testing the lockboxes in the debugger showed me the important thing about v17, namely, the interval that it has to satisfy for a box that is unlockable:
-0.5 ≤ v17 ≤ 0.0
Now, knowing what the value for an openable box and knowing that if the locally calculated value differs from the blockchain one, the blockchain takes priority, we’re ready to open our challenge lockbox.
Opening the lockbox
We’re going to do this with a debugger. First things first, let’s make sure the v17 variable is between -0.5 and 0. In order to control the variable’s value, we want to set a breakpoint at file offset 6915F and another at file offset 691D2. Once both are set, you can open the challenge file and input the correct password (77C8C66C-4452–41DF-A99A-599C9CA4AE11) and wait for the first breakpoint to hit. When it does get triggered, we need to modify the XMM0 register. We can use an online double-to-hex converter like https://gregstoll.com/~gregstoll/floattohex/ to convert, let’s say, -0.25 to 0xBFD0000000000000. We want to modify the XMM0 value from whatever it is into 0xBFD0000000000000.
We can then continue the run until the 2nd breakpoint is hit. Now, we want to take the first branch (we don’t want the jump to be triggered) so one way to make sure we always take the first branch is to simply patch the jump into NOP instructions, just like for the anti-debugging. We can do this during runtime (in memory) or we could have done it in the file, before starting the executable. The choice is yours. Once the patch is in place you can go ahead and delete the breakpoint and resume execution.
Okay, we’re done. Now there’s just a minor detail that I didn’t mention so far, which is that this decryption function is being run 3 times (most likely because it tries to triple-check the blockchain results). This doesn’t affect us much, it just means we will have to overwrite the XMM0 register at the first breakpoint two more times. So we end up again at the first breakpoint and we overwrite using the same value as the first time. And then continue execution again, and once again we’re at the same place, and overwrite the register. Finally, we continue execution and if everything went well, we should be greeted by a folder browse window that asks us for a location to save our decrypted file. Success!
Notes and final thoughts
- The application *still* has a bug which makes it impossible for the user to select start and end dates. By which I mean that the dates selected by the user are not taken into account.
- Though I have not spent much time exploring the current encryption scheme, I believe that a more secure final check together with code obfuscation (and maybe some stronger anti-debug/anti-tampering) are the most important things that could be improved in a future iteration.
I would like to thank the author for this fun and extremely interesting challenge. I have been really looking forward to it for a couple of months. Hopefully this report will help in making TimeLock secure and unbreakable, and, of course, I’ll be eagerly waiting for future challenges.