All right, I hope you liked the previous articles on TimeLock, because here is another one! This will be my fourth bug bounty now. As always, interesting reverse engineering followed by an awesome Bitcoin reward awaits!
A little while ago u/cryptocomicon posted a new announcement of TimeLock 1.5 to Reddit:
I can’t turn down a good challenge, so lets get started!
If you aren’t familiar with my previous vulnerability write ups on TimeLock, I recommend you take a look before you read this post.
- Analysis of TimeLock and Vulnerability Writeup
- Revisiting TimeLock 1.2 and Vulnerability Writeup
- Unleashing a Sybil Attack Against TimeLock 1.3 Vulnerability Writeup
Head to the download page and fetch version 1.5 along with the challenge LockBox.
As always, I have mirrored the files here for you to follow along:
The facts we know are similar to previous challenges:
- The password to the LockBox is TimeLock
- The answer to the question is 0.02
- The earliest time it is available is 23/03/2019 00:00 UTC
- The time of revocation is 24/03/2019 00:00 UTC
The scope is the same. Knowing the above information, attempt to defeat the TimeLock mechanism.
Defeating Anti-Debugging Measures
First things first, I noticed some strange behaviour when debugging TimeLock in x64dbg. If you place a breakpoint, hit the breakpoint, wait for an extended period of time looking at instructions, and then restart execution, the program terminates.
That is very strange. This is probably caused by some anti-debugging functionality. We are going to have to find what is causing the program to exit and patch it out before we can continue any further.
Load TimeLock into IDA, and determine the offset needed to rebase the program by finding two functions and looking at the difference in their addresses. This time, mine is
In IDA do a search for a fixed value. The exit code was
0xFFFFFFF9 which is one odd number. In this case, I got a hit at
0x7FF7673F99EA shown below:
This function has a bunch of calls to
QueryPerformanceCounter() which fetches a high resolution clock, and then does a compare to see if there is any significant delta in subsequent fetches of the clock. If there is, then the program is probably stopped for debugging, so the program is terminated with
ExitProcess(). Very uncool. Well, for me anyway.
Looking at where the function is called, we find it is only called in one place:
It seems the function is being called on a thread, which is slightly problematic. If we don’t
nop it out correctly, we end up taking the left branch since the call to
CreateThread() would have failed, and we
Head to x64dbg, since I think it patches files better than IDA, and jump to the function with ctrl-g.
We will patch out
CreateThread(), by setting the instructions to
mov eax, 1 and set any remaining space to be filled by
CreateThread() is at address
Assemble the instructions and your result should look like this:
Great. It’s best to save the changes back to the file so go
File > Patch File... and save it to a new binary. Run TimeLock, and you will find that it no longer terminates when a breakpoint is hit. Excellent. We can continue exploration.
Analysis of Decryption Routines
I spent a lot of time wondering what to attack, and I decided to look into how encryption and decryption is implemented, and to look for weaknesses there. First, we need to understand how it all works.
We will start with decryption, since that is the most important. Remember back to my second writeup, where I located the final decryption function. Let’s track it down again. I remember that it was right before the loop that checks for 64 zeroes which indicates a successful decryption. After some digging I came across:
We see a loop that compares a address to 0,
0x40 == 64 times.
sub_7FF7673F9240 is our function. Looking inside it, we see all sorts of shifts and multiplies, so its clearly a crypto function. The details aren’t too important though. We want to know the overall workings.
Looking straight above that block, we see a call to
_fteli64() which moves the file position pointer forward (in the LockBox file), a call to a wrapper function for
malloc to allocate a new buffer, a call to
memset() with a
0 parameter to initialise the memory to
0, and then a call to
Odd. Looking at the process in the debugger, I can confirm that the encrypted file is loaded into memory.
Looking slightly above still, we find calls to
Here is what I think is going on. Calls to
sub_7FF7673FBCE0 prepare some sort of buffer in memory. This uses the password, answers to the questions and the time to derive the buffer. This is then used in some sort of mixing XOR fashion with the encrypted file in
sub_7FF7673F9240 which performs decryption based on those inputs.
Analysis of Encryption Routines
Now that we understand what is going on with decryption, we need to understand encryption. Since I have a hunch TimeLock uses symmetric encryption, encryption should be done in very much the same order. Press
x to get the xrefs of
sub_7FF7673F9240, and we see that it is used in numerous places:
We know that the questions, answers to the questions, unlock times and the file itself need to go through encryption and decryption, so we will look at calls closely grouped together. After some investigating, we come across
fread() is called above, the file is closed, the buffer is then encrypted with
sub_7FF7673F9240() and then written to a new file with
fwrite(). We have our encryption function alright.
We can verify this by attempting to create a LockBox. Make a new file filled with “AAA” and place a breakpoint at
0x7FF76741816F. The address of the buffer is stored in
On breakpoint hit:
Stepping over, file is encrypted:
Okay. Now we understand how encryption is performed. This is coming together nicely.
Symmetric Encryption Failures
TimeLock probably uses symmetric encryption, and we are going to examine how robust the implementation is. Now, in symmetric encryption, the encryption key and decryption key are the same. Encryption and decryption are the same operation, so if you can encrypt something, you can also decrypt it.
The process is like this:
encrypt(plaintext) -> ciphertext
decrypt(ciphertext) -> plaintext
- and finally, since
encrypt == decrypt:
encrypt(encrypt(plaintext)) -> plaintext
Since encrypting the file does not require connecting to the Bitcoin network to validate time, we will try using the encryption function to decrypt the LockBox.
Firstly, we need to determine if the implementation of symmetric encryption is vulnerable.
When we are making the LockBox, set the times to the future, by setting the dates to what the challenge requires, 23/03/2019 00:00 UTC and 24/03/2019 00:00 UTC, with the password TimeLock and answer 0.02. For now, use the file filled with “AAA”.
Generate a LockBox, and stop again at the breakpoint at
What we want to do is select the part of the file which will be encrypted. It starts at the series of zereos, and ends at the last
After encryption is done, save the encrypted data to a file, by right clicking > Binary > Save to a File.
You should then have the extracted data in a file, on the desktop.
Now, create another LockBox. Before encryption, replace the plaintext with the saved ciphertext, by right clicking > Binary > Edit option.
And click Ok:
Next, step over to “encrypt” the ciphertext:
Oh. It appears TimeLock’s symmetric encryption is very, very vulnerable. We just decrypted the ciphertext by using the time we provided to encrypt the LockBox. That’s not good for security. But it’s excellent for me.
Extracting Ciphertext of Reward File from LockBox
We need to extract the ciphertext of the file we wish to decrypt, and the best way to do this is to set a breakpoint right before decryption takes place, at
0x7FF7673FE0E2. Attempt to decrypt
Challenge5.x, and wait for the breakpoint to hit.
The address of the buffer which holds the ciphertext is stored in the
Select all the bytes which make up the file, until you start seeing
0xAB. Right click > Binary > Save to File… and save the ciphertext to a file on the desktop.
Decryption of Reward File
Now, the ciphertext is much larger than the little file filled with “AAA”, so copy paste those “AAA” characters until you have a large file which will be able to hold all the ciphertext data.
Attempt to create a LockBox, ensuring that you set the start and end times, password, answers to questions and cycles to the exact values supplied above.
When you generate a LockBox, hopefully the breakpoint at
0x7FF76741816F hits. From here, fetch the buffer address from
ECX, and open it up in the memory dump section.
Now, you want to select the buffer, again until the
0xAB characters. From there, Right click > Binary > Edit, and paste in the ciphertext we previously extracted to the desktop.
Now step over. What an incredible sight!
We have done it! We have decrypted the reward file.
Extracting Reward File Plaintext
Reading the extracted hexadecimal is sufficient for most purposes, but we will go the extra mile and properly extract the file. Select the buffer, just like we have done previously, and save the contents to disk.
Attempt to decrypt
Challenge5.x, and step over the breakpoint. Since the time is incorrect, you will see a buffer filled with random encrypted data. Overwrite that buffer with the freshly extracted plaintext.
Note, we only need to copy to the end of the plaintext, which is the
0x0d characters used to show line breaks. Make sure you keep those zeroes intact! That is how TimeLock verifies that encryption was successful.
Press resume and we see:
Here we go…
Why yes, we do want to see the file:
OH YES! YES! Fantastic! This challenge has been successfully completed. Thanks to u/cryptocomicon for the interesting problem to solve!
The private key for the challenge reward, 3GBxNQt9TcCJyhQkzAvYTpY97Bxj39LZXL is p2wpkh-p2sh:Kyb2fewqnFMS3mFD5CdBea4HGfdVW3DYbfKnyZi2YpFi5rSV2ByD
I quickly imported the key into Electrum and sent the funds into a wallet I control. Thank you very much to u/cryptocomicon for this interesting bug bounty, and for the funds.
I hope this writeup is sufficiently detailed in order to qualify for the additional 0.02 BTC reward.
This can be payable to the address which I swept the other BTC into: 19KuusbvKQrGLDTCGChysGe1fYTkUznXYf
Notification of Vulnerability
I notified u/cryptocomicon about the challenge being completed via private message on Reddit:
To which I got “Well done!” back:
How To Fix The Vulnerability
Now, fixing this vulnerability is not going to be easy. Symmetric encryption was selected since the key needs to be generated again at some point in the future, from basic primitives such as the password, answers to questions and the time.
You are going to have to investigate different encryption algorithms where encryption != decryption.
Creating Separate Applications
Consider separating the encryption and decryption portions of the app into two separate apps. Then, once a user has finished making their LockBox, they could simply delete the app which creates LockBoxes, and keep the decryption app with the LockBox for future use.
This is not ideal, but it prevents reverse engineers from being able to create their own LockBoxes and from performing all the analysis I have been doing. This plan is not infallible though, as someone could post the encryption app online for everyone to download, so it could still be placed under analysis in the future.
Implement a Trusted Third Party
Can I tempt you into implementing a trusted third party? It would fix all of your problems, as you use public / private key encryption by default.
The server would generate a public and private keypair. The user would then request a public key, and this is sent to TimeLock. TimeLock then encrypts the file on the user’s disk with the public key. TimeLock also uploads a challenge to the server, which has the password and answer to questions encrypted with the public key.
At some time in the future, the user attempts decryption. TimeLock contacts the server, and requests the private key. TimeLock supplies the password and the answer to the question. The server then decrypts the previous challenge with the private key, and if the password and answers match, then the time is checked.
If the time check is successful, then the private key is sent to TimeLock.
Perfect. Elegant. Probably pretty secure. Only flaw is that you have to keep the server around for the next fifty years.
Consider selling the software to customers, and then they have to self-host it. Dedicated people might go along with it, although it is quite complex and hard for most people.
I hope you enjoyed the writeup, and as always, feel free to contact me.