TimeLock V3.1 Challenge — Vulnerability Report

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.

Challenge V3.1

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.

Initial analysis


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:

Function that uses QueryPerformanceCounter

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:

Named function for time validation

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

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

Modifying the XMM0 register at the first breakpoint (in x64dbg)

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.

Patching the jump at the second breakpoint

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

  1. 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.