Crypto Deja vu in TimeLock 1.7 Vulnerability Writeup

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:

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.

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.

Head to the download page and fetch version 1.7 along with the challenge LockBox.

download

As always, I have mirrored the files here for you to follow along:

TimeLock_v1_7.msi SHA256 2b3938c7af9e73e2438e1d1707a9bd6914cc2367e06ba02e3668ee40316bfc3a Challenge5_V1.7.x SHA256 40a805bbb2740f86a24cd5f3933734f2bd832e3a7ccc55286b77a6ede71c1b9d

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.

Analysis

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:

top

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:

top

Now, in our debugger, jump to 0x7FF6FEE45774 and nop the call out. Save the file, since we will be restarting our debugging sessions often.

top

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 fread() and 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.

xref

We are looking for one that pairs with fwrite(), and we are looking for encryption routines in between. Eventually we find an interesting fread() at 0x7FF7EFB8EAF2 which looks to be the start of file encryption.

read

We can test it by making a new LockBox with a file filled with our favourite character, 0x41 (‘A’).

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:

break

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:

break

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 0x7FF7EFB7184E for fread().

break

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:

first

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 0x7FF7EFB8EC3D.

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:

ciphertext

If we re-encrypt the ciphertext, by replacing the plaintext with the ciphertext:

ciphertext

We find that this is a symmetric algorithm, and is vulnerable to what was discovered in the previous challenge.

plaintext

Second Round of Encryption

We see a very interesting second round of encryption:

second

We see a lot of imul, xor, shr and add instructions, and some interesting calls to sqrtf() and 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.

second_decrypt

We see the same imul, xor, shr instructions, but we also see some cosf(), sinf() and 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 0x47 and 0x40 bytes inserted. Strange.

second_decrypt

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.

Exploitation

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.

create

Place a breakpoint at 0x7FF7EFB8EFA6, 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.

buffer

It starts with 71 47 FA 40. Resume the program, and then open up the .x file in a hex editor:

buffer

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:

buffer

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:

buffer

Set a breakpoint at 0x7FF7EFB8EFA6, fwrite(), and disable the breakpoint at the xor. Let it run, and the third round of encryption should be reversed:

buffer

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.

buffer

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.

buffer

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.

buffer

Remove your breakpoint at 0x7FF7EFB8EC3D, and set a new one for 0x7FF7EFB8EC5E. Press start. Look at your buffer now:

buffer

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:

win

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.

Loot

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:

reddit

I got this response back:

reddit

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.

Matthew Ruffell