TimeLock V2.2 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.
Specifically, locking requires the user to provide an input file, a password, between 1 and 5 questions + answers and the start and end date during which time the lockbox can be open. For the unlocking process, the password and answers have to be identical with the ones supplied during locking and the current time has to be between the start and the end date. The current time 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 answer, and the goal is to open it before the start time that it has been designed for.
This challenge was the 8th to be launched (you can view all of them here: https://www.algomachines.com/people), with the time limit of 26 November (or in other words, it was designed to be openable on the 26th). It is the first lockbox that I’ve attempted, but the first 5 challenges have been solved and write-ups provided, which were fairly helpful in setting the general mindset and approach. I will mention the approach that I’ve followed in order to make clear the reasoning behind the solution. This report will try to highlight any particular points I feel the author might want to address, while also trying to succintly log the important steps towards discovering and exploiting the vulnerability, assuming one is familiar with reverse engineering. The addresses referenced in this report are Virtual Addresses, rebased at 0x0.
The first thing I did was to open the challenge lockbox in a hex editor to see if there’s any plaintext structure/keywords to follow. No luck there, the entire file is encrypted.
Running the program and attempting to unlock the lockbox using the password “TimeLock” and answer “0.1 BTC”, both provided by the author, gives us the message “Current time is not within the timelock range for this lockbox.” So a first naive attempt would be to directly look for a place in the program binary where this string is used, since that should around the place where the check is made whether the decryption was successful or not.
Opening up the program binary in a static analysis tool (like IDA Pro) shows generally well behaved code, indicating that the code is not (heavily) obfuscated, and looking at the Strings reveals quite a few plaintext phrases that are definitely used inside the main program. However, the string we’re looking for is not found in there. So it has to be decoded on the fly somehow. We’ll need another way to get close to the code of interest.
While I will not go into many details on the analysis of the binary file, another relatively easy approach to get close to where the interesting stuff happens is to try and think of Windows functions that *should* get called during the period of interest. In this case, at the beginning of the unlocking procedure we have to select a lockbox file. Opening a handle to a file is always done via kernel32.CreateFileA or kernel32.CreateFileW (which further call ntdll.CreateFile) so an idea is to try and put breakpoints on these functions, which can be done using a debugger.
I had read in the previous reports that anti-debugging was added and I was expecting that I’ll have a hard time bypassing everything. Firing up x64dbg and attaching to the process, nothing bad happened. No detection, no crashes. Good for me then. The one anti-debugging trick that I knew was employed was using QueryPerformanceCounter in order to check if the process or any of the running threads were suspended for a suspicious amount of time. I want to stress at this point that the anti-debug was a very minor annoyance, and for the most part I didn’t even bother to bypass it fully. There was one single moment, right at the end, when I did use the mostly-untested bypass that I found, but I could have just as easily skipped that part entirely.
Anyway, the annoyance that I’m mentioning is not directly related to QueryPerformanceCounter, but instead the program starts up a named function in a separate thread very early which is always running the following loop:
Iit seems to me like the function keeps getting the current time and comparing it with the previous stored value which is at 0x2E8048. If too much time has passed between the new time and the old, it marks the time as “stale” and therefore refuses to update it anymore. The variable at 0x2E8048 that holds the time is used in several of the encryption/decryption functions, though I didn’t explore what for. The only thing I did was to patch out that if check so the code always goes on the main branch, never on the else. It might be placebo but in the few times that I did use my patched executable with debugging, the execution worked with fewer crashes.
Finding the Vulnerability
Our task is to find a way to “trick” the program into thinking that the lockbox can be opened right now. We know the application connects to bitcoin nodes in order to validate the current date, and we can assume that the lockbox file must contain some kind of reference to the starting opening time.
This means that as far as I’m concerned, there are a few possible vectors of attack:
- Modify the application binary in order to bypass potential time checks
- Modify the lockbox file in order to reflect the current time
- Find a way to fake the data returned by the BTC nodes so that the application believes the node time is different
- Analyze the encryption/decryption scheme directly and find a way around it
First thing I wanted to do is to understand the code flow a bit and see which of the vectors would be the easiest. Using the above mentioned method of setting a breakpoint on the kernel32.CreateFile functions, and then tracing slowly through the code, one can eventually arrive at both the main encryption (at 0x8C10) and decryption (at 0xDBF0) functions. Both of these functions get started in separate threads using a custom thread starting function located at 0x84384.
I started by analyzing the decryption branch, slowly working my way through the code and documenting various functions used, while also hoping that I might spot the “check” and simply patching it out. Well, at some point, the program starts spawning many child threads, each connecting to BTC nodes and retrieving blockchain headers and then doing some stuff I didn’t quite understand because it’s complex and fairly confusing, and because I had an interesting idea fairly early on.
When I first downloaded the application, it was clear to me that in order to understand it, I need to encrypt my own lockbox file and see what a successful decryption path looks like, so I created a lockbox that could be opened. Most of the analysis that I did was on my own lockbox. At some point, I figured it’s worth studying the encryption more than studying the decryption. In order to do that, I created a 2nd lockbox, with the exact same input file, password and question/answer, but which would only decrypt the following day. Here’s a comparison of the hex contents:
As you can see, the headers are virtually identical, except for a couple of bytes! So those bytes must be where the date is stored somehow, and that clearly seems vulnerable. Even more, generating another lockbox which had the same password, question/answer and dates but a different payload, revealed that the lockbox file contains a “header” where some data is stored.
When I saw that, I thought that I already pretty much solved it, so I proceeded to make a new lockbox with the password and question/answer as the challenge lockbox, but which could be unlocked already. While the header was pretty much identical, changing the bytes from the challenge file into the ones from the unlocked file didn’t work. Time for some encryption analysis to see how the dates are stored, and then attempt to properly change these values so that the lockbox can be opened now.
Exploiting the Vulnerability
I won’t go into details on everything that’s going on in the encryption function, but instead will only focus on how the dates come into play:
Let’s call the start time, T1, and the end time, T2. In that case we can say that dT=T2-T1 is the time difference between the dates. There is also one constant used, K1, which is a sort of “pseudo-random generated number”, derived only from the password, so we can assume we know it, since we know the challenge lockbox password. The following variables are also constructed:
K2=(K1+T2) % dT
It turns out that lockbox files contain 3 of these defined quantities, namely K1, dT and K2. The author also provides T1 and T2 for this challenge. K3 is not contained in the file, but we can calculate it using the formula above. However, this means that there is no way for us to control K3, so instead we must be careful that when modifying the other variables, K3 has to be preserved.
Note: K3 is used to encrypt the data. I did not look into the encryption algorithm but if it symmetric, one could directly use this value and decrypt only that particular encryption layer and then re-encrypt it using their own K3 value which would defeat the time check (since K3 contains T1).
So now it’s time to exploit the vulnerability. Let’s call T0 the date for today. We want to substitute T1 with T0 in order for the box to open, but we can’t modify K3. We also can’t modify K1, because it depends on the password. That leaves us with K2 and dT which have to be modified so that:
Bear in mind that K2 < dT by definition, which implies X < Y. Also, T0<T1. At this point it is more effective to work with the actual numbers that the challenge lockbox used (in hexadecimal notation):
T1=5DDC6B00 (unix time corresponding to 26 November)
T2=5E03F800 (unix time corresponding to 26 December)
and I used T0=5DB8D280 (corresponding to 30 October).
So we can now calculate Y in the equation above by simply doing
We are not very interested in changing K2 into X yet because by definition K2 < dT so it should not affect the result. Putting in the values,
Since Y is our new dT, that means we’ve changed the end date! The new date is T2_new = 5DB8D280 + 278B28 = 5DE05DA8, corresponding to November 28, 2019 11:52:08 PM.
Now we can also calculate X using the formula X=(K1+T2_new)%Y,
Let’s check that what we did is correct by calculating the new K3, which should be 1351, just like the old one:
All right, we’re all set! All we need now is to create a new lockbox with password=”TimeLock” and question+answer=”Reward Amount?”+”0.1 BTC”, and the start+end dates=October 30, 2019 12:00:00 AM and November 28, 2019 11:52:08 PM.
Note: The V2.2 application has a bug which prevents you from actually building a box with an arbitrary start and end date. Instead, regardless of user input, the start date is the next day at 12.00 AM and the end date is 7 days after that. This means that in order to obtain the correct bytes I did not follow the steps above, but instead I put a breakpoint at 0x46B7C where that header gets encrypted. Once the breakpoint is hit, the buffer pointer for the header is in the rcx register and we’re free to modify it with our new values. Once the correct values are in place, stepping over the function call will reveal (at the same pointer location) the encrypted buffer, which can then be copy-pasted directly into the challenge lockbox.
Once the lockbox is done, we check for the differences in the header part between the challenge lockbox and our new lockbox, and overwrite the bytes in the challenge box where there’s differences.
Now if we attempt to open the lockbox, boom! It works!
Notes and final thoughts
- The application currently has a bug which makes it impossible for the user to select start and end dates.
- The current anti-debug is an annoyance at best. But please keep in mind that adding anti-debugging/anti-tampering/obfuscation to the code serves as a deterrant but a determined individual, given enough time, will inevitably get past any protection.
I would like to thank the author for this extremely interesting challenge, it was a lot of fun to analyze and finally obtain a solution. I do hope there will be future versions of TimeLock which address this vulnerability (though I do not know how that would look like) and, of course, future challenges.