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!
Reconnaissance
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:
TimeLock_v1_5.msi SHA256 01f2a93fcb87c388fe9d0ade75f94fab7dce0ebe93af025d704015139066f257 Challenge5.x SHA256 a6c918854d5cc9d6159f4dc805b99f989878c5d1262ffaf9663dfa155929c9f9
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.
Analysis
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 0x7FF7673F0000
.
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 ExitProcess()
.
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 nop
. Note CreateThread()
is at address 0x7FF7673F9A9F
.
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 fread()
.
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 sub_7FF7673FB9D0
and sub_7FF7673FBCE0
.
Here is what I think is going on. Calls to sub_7FF7673FB9D0
and 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 0x7FF76741816F
:
Notice how 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 ECX
.
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 0x7FF76741816F
.
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 0x41
.
Step over.
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.
Exploitation
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 ECX
register.
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.
Hit save:
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 0x0a
and 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!
Loot
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.
Matthew Ruffell