Here we are, back for more TimeLock excitement. Let’s see what’s in store for this article, where we pull apart and attempt to find vulnerabilities in TimeLock 1.7.
A little while ago u/cryptocomicon posted a new announcement of TimeLock 1.7 to Reddit:
Looks like I’m getting some advertising of my blog =) Thanks u/cryptocomicon! Maybe it will introduce some people to reverse engineering.
Challenges are fun, so let’s jump into it.
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
- Double Trouble With Symmetric Encryption in TimeLock 1.5 Vulnerability Writeup
Head to the download page and fetch version 1.7 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 25/04/2019 00:00 UTC
- The time of revocation is 26/04/2019 00:00 UTC
The scope is the same. Knowing the above information, attempt to defeat the TimeLock mechanism.
Load TimeLock.exe into IDA, and open it up in x64dbg. The offset this time is
7FF7EFB60000 if you are following along.
Defeating Anti-Debugging Measures
Anti-debugging measures are still enforced, and we will use the same methods from the previous writeup to disable them. Search for
QueryPerformanceCounter() in IDA, and xref to the location. Head to the function above, and we are back to this familiar sight:
This time we won’t patch out the call to
CreateThread(), instead we will patch out the call to this complete function. Click
sub_7FF6FEE2CF00 (NOTE: The following offset is from an earlier debugging session and will not match the offset for the rest of this article), view the cross-references to find where it is called from:
Now, in our debugger, jump to
call out. Save the file, since we will be restarting our debugging sessions often.
Examining the New Three Round Encryption System
For this article, we will be looking into the new encryption and decryption routines to see if it has been fixed securely, since it feels like a path of lesser resistance than having to work out how TimeLock connects and fetches the time from the Bitcoin network.
I had a brief look at the latter, and it is very complex and borders on scary. Block hashes are now verified and the system is even more complex than before. We will leave that for the the future.
In order to find the encryption and decryption routines, we will search for instances of
fwrite(), since the contents of the plaintext file / encrypted file will be read in, and then subsequently encrypted / decrypted, and then written back to disk after processing.
Press ctrl-l in IDA to search for
fread, and then press x to look at the cross-references.
We are looking for one that pairs with
fwrite(), and we are looking for encryption routines in between. Eventually we find an interesting
0x7FF7EFB8EAF2 which looks to be the start of file encryption.
We can test it by making a new LockBox with a file filled with our favourite character,
Place a breakpoint at
0x7FF7EFB8EAF2, and once hit, fetch the address from
ECX, which holds the buffer, and view it in the memory dump. Step over, and we see:
We have the start of our encryption function. Looking at the overview, we can clearly see three different sections to how encryption is performed before
fwrite() is called:
From the picture, we can see that encryption rounds 1 and 3 look very visually similar. Interesting, we will need to look into that some more.
From the decryption side, we can see the same sort of thing, starting at address
We can see that the order is kept the same, and things are coloured in reverse of the encryption step. The last part, section 4, is for verifying the decryption was successful by checking for 64 leading zeroes.
Let’s drill down into each round and attempt to see if each stage is reversible, and if we can take advantage of any symmetric encryption.
First Round of Encryption
Here is a close up of the first round of encryption:
What seems to happen is that the first block iterates to create some sort of dynamic keystream, which is then further mixed in the second block. The final bottom block is where a single byte is xor’d with the keystream byte, at
It appears that the values of the keystring are dependant on the password, answers to questions and the TimeLock function. This appears to be where the real security is implemented.
If we encrypt our file filled with ‘A’ characters, we see the following ciphertext:
If we re-encrypt the ciphertext, by replacing the plaintext with the ciphertext:
We find that this is a symmetric algorithm, and is vulnerable to what was discovered in the previous challenge.
Second Round of Encryption
We see a very interesting second round of encryption:
We see a lot of
add instructions, and some interesting calls to
atan2f(). This is starting to look like a serious encryption function. Looking at the sister decryption round two functions, we see that they are not the same at all.
We see the same
shr instructions, but we also see some
roundf() calls. It is pretty clear that this is not a symmetric algorithm, but I will test anyway.
After a quick test, and I can confirm that the output of this encryption function is not symmetric, as re-encrypting ciphertext yields different ciphertext. One interesting thing to note though, is that the initial ciphertext is larger than the output of round one, and it has a lot of
0x40 bytes inserted. Strange.
This second round of encryption is what prevents the solution to the previous challenge from working. What we need to do now is determine if there is a secret key used as an input, and if it is hard coded or not. If it is, we can work with that to reverse the encryption, but if it relies on external data, such as the time, we may be out of luck.
Third Round of Encryption
The third round of encryption is instruction by instruction the same as the first round of encryption. This is good news for us as we don’t need to understand how another round of encryption works.
We know that it is reversible, since the first round is reversible. The only difference is that due to the second round increasing the size of the ciphertext, the third round needs to encrypt more data, so has the counters set higher. This is not a problem, but it means we will still need to use this third round for decryption purposes in the future.
Our ability to open the LockBox relies solely on the condition that we can reverse the second stage of encryption. We can only do that if it relies on hardcoded constants. Time to find this out.
This is going to be our game plan:
- We will locate the offset in the LockBox.x file where the encrypted file starts.
- We will then run those bytes through the third round of encryption, to reverse them. This exposes the second round of encryption.
- From there we will run the output of the above through the second round of decryption, and hope that it relies on hardcoded constants. This will expose the first round of encryption.
- We take those bytes and run them through the first round of encryption to reverse them. If all goes well, we will see the plaintext.
- We extract the plaintext, and hopefully solve the challenge.
Let’s get to it.
Locating the Encrypted File
We are going to make a new LockBox, and it will have the same attributes as the challenge lockbox, namely, the password is TimeLock, the answer to the question is 0.02, the dates are 25/04/2019 00:00 UTC and 26/04/2019 00:00 UTC, with 2 extra rounds of encryption. Select the file of ‘A’ characters.
Place a breakpoint at
fwrite(), and let it run until it hits. When it does, get the value from
ECX, which is the buffer which will be written to disk. Look at it.
It starts with
71 47 FA 40. Resume the program, and then open up the
.x file in a hex editor:
Find the bytes. It seems to be right after
1C 28, which is unhighlighted. This is the file. Now open up
Challenge5.x, and attempt to locate the same starting place. We see:
Select all these bytes and save to a new file. This is the output of the third round of encryption of the challenge file.
Third Round of Encryption
Again, make a new LockBox with the same setup as in the previous step. Place a breakpoint at the
xor [r9], al at
0x7FF7EFB8EF6C. Run the program.
We want to swap the output of the second round of encryption of ‘A’ file, for the output of the third round of encryption for the challenge file. Edit the bytes in the debugger like so:
Set a breakpoint at
fwrite(), and disable the breakpoint at the
xor. Let it run, and the third round of encryption should be reversed:
Select all these bytes and save to a new file. This is the output of the second round of encryption of the challenge file.
Second Round of Decryption
Now, since the second round of encryption / decryption is not symmetric, we need to use the proper decryption function. Go ahead and click “Open a LockBox” in TimeLock, and select
Challenge5.x. Fill in the password and answer to the question, and let it connect to the bitcoin network.
While it is doing that, we need to set some breakpoints. We are going to edit the data right at the end of the first stage of decryption, so set a breakpoint at
7FFEFB71A38. Eventually you will hit it.
Swap the bytes found in the buffer to what we got from the previous section, which is the output of the second round of encryption of the challenge file.
Set another breakpoint right before the third round of decryption starts, at address
0x7FF7EFB71C87. Press start. If this decryption function uses hard coded constants, we should have successfully reversed the second round of encryption.
Select all these bytes and save to a new file. This is the output of the first round of encryption of the challenge file.
First Round of Encryption
Here comes the moment of truth. Set a breakpoint at the
xor [r9], al instruction at
0x7FF7EFB8EC3D, and go and encrypt another LockBox with the file filled with ‘A’ characters.
When you hit the breakpoint, highlight the ‘A’ file (found in r9), and edit the bytes, swapping in the output we got from the previous section. This would be the output of the first round of encryption of the challenge file.
Remove your breakpoint at
0x7FF7EFB8EC3D, and set a new one for
0x7FF7EFB8EC5E. Press start. Look at your buffer now:
My my. Doesn’t that look wonderful. We aren’t out of the woods yet…
Extracting the Plaintext
Highlight the buffer and save it to disk, but this time, set the file extension as
.txt instead of
.bin that we have been using. Open it up and notepad, and we find:
YES! OH YES! Man I’m happy. A lot of hard work went into solving this challenge. Thank you very much to u/cryptocomicon for the challenge, and for the funds.
The private key for the challenge reward, 3NhhBeL9z7fbrKvGLXQyzRCEvCfDW5nQTQ is p2wpkh-p2sh:L2sbGB3LkFdgC4H4JUTD9DWt4oSzRxPbjYzj5upKGhkTMiyq4q67
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 of the vulnerability via Reddit:
I got this response back:
How To Fix The Vulnerability
The Second Round of Encryption Should Not Rely on Hard Coded Constants
The vulnerability exploited in this writeup is the fact that the second round of encryption and decryption use hard coded constants, instead of a key which changes. If the second round of decryption had simply required a key which was created from the current time, I would have been stuck, as when I went for decryption on that attempt, the time would have been wrong.
I suggest you change the second round of encryption and decryption to rely on a key which takes the the time into account. The first and third rounds change when the time changes, and the second round needs to follow suit. If you get stuck trying to work out what to use as a key, how about getting the first round to do an extra 10 iterations or so and make you a nice new 10 byte key?
That 10 bytes would be easily reproducible, given that the time parameters are correct, and might be difficult to transplant from the encrypting a lockbox dialog into the decryption dialog.
Just a thought anyway.
I hope you enjoyed the writeup, and as always, feel free to contact me.