Here we are, back again for my third bug bounty! It really is a good time trying to break an applications security, and especially so when there is some Bitcoin waiting as a reward.
As always, I was on Reddit and saw that u/cryptocomicon has made some changes to TimeLock, and is ready for them to be tested again.
u/cryptocomicon has acknowledged that writing secure software is extremely hard, and is absolutely correct in that statement. We also see that a new challenge is issued:
Designing an un-hackable TimeLock is challenging. This is my third version and the third challenge, with a 0.02 BTC reward.
Please give it a try.
Will do. Challenge accepted.
Reconnaissance
If you aren’t familiar with TimeLock and what it is, or my previous vulnerability disclosures, you should probably read my previous posts:
Right. Let’s go to the download page and fetch the updated version:
I have mirrored the files if you want to follow along:
TimeLock_v1_3.msi SHA256 5a5172c1f48b26dcaeb58c5f68a298487db8f3c0d2a7c783c61eb10afa04f3de Challenge3.x SHA256 e8cd79d7738624047bf4f96a73cfbefac28a5eebf834cb4fdbc28855705c8ad2
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 09/03/2019 00:00 UTC
- The time of revocation is 10/03/2019 00:00 UTC
The scope is the same. Knowing the above information, attempt to defeat the TimeLock mechanism.
Analysis
The program has now changed, and no longer connects to the Bitcoin network at start-up. The button to “Open a LockBox” is now available immediately.
If we click this button, and enter the password and answer the question, the following window is spawned:
This means that the peers are only connected to when required.
Dumping the strings of the binary produces a horrible sight - all the strings appear to have been removed. It looks like strings are stored encrypted and decrypted right before they will be used. It means advanced static analysis will be difficult.
Sybil Attack Planning
The plan for today is to execute a Sybil attack. A Sybil attack is where you introduce malicious nodes into a network who masquerade as legitimate nodes, and then get the program in question to trust your malicious nodes.
In this case, I will set up my own Bitcoin full nodes with their clock set to the future, and I will get TimeLock to connect to and trust those nodes. TimeLock will then use the Bitcoin network time, which is set by me, to a value in the future.
The Sybil attack will only work if ALL nodes are malicious nodes, since Bitcoin full nodes peer with each other, exchanging information such as the time. So I need an environment where I can control all the nodes that TimeLock is connecting to.
Determining How TimeLock Connects to the Network
The first thing to do is work out what TimeLock does to find the IP addresses of Bitcoin full nodes. Let’s open up Wireshark and start listening. Set the filter to “DNS” and have a poke around.
We find a request to dnsseed.bluematt.me:
If we look at the reply, we get a list of IP addresses:
Looking up what a “seed node” is, we find that they are used to bootstrap connections to the Bitcoin network. These nodes are custom DNS servers which serve out a long list of randomly chosen full nodes which are alive.
I have determined that TimeLock connects to the following DNS seed nodes:
- seed.bitcoin.spia.be
- bitseed.xf2.org
- dnsseed.bitcoin.dashjr.org
- dnsseed.bluematt.org
- missionctrl.info
If we are able to get these DNS records to point back to a Bitcoin full node we control, we will be able to influence TimeLock.
Setting Up DNS Redirection
We need to control DNS. To do this, we will set up our own local DNS server, and tell Windows to use it.
I found a cool Python script called fakedns. It implements a ultra basic DNS server that you can hack on, and is exactly what we need.
Download Python3 for your Windows box and run the script. I just ran it in IDLE, nothing fancy here:
I changed the IP address to point to 127.0.0.1, since we want to serve out addresses for the local machine.
Next we need to tell Windows to change its default DNS server.
Right click the network indicator > Open Network and Connection Settings > Change Adapter Options > Ethernet Adapter > Properties > IPV4
You should get to this window:
Change the adapter to use 127.0.0.1 and 127.0.0.2 as the DNS server, so it connects to the Python script.
If we run nslookup, we now get redirected to 127.0.0.1:
Great. But it’s not exactly what we want. We need to return a list of random seed nodes to fool TimeLock. We need to make some changes to the Python script.
If we look at RFC 1035, we see that for DNS, we get multiple addresses for each question by including more than one answer RR per packet:
If we tweak the script to add another for loop when we generate answer records, and randomise the IP addresses as we go, we get something a little like this:
I have attached the final Python script if anyone wants it.
If we query with nslookup, we see:
Just like a Bitcoin DNS seed node. Excellent.
Getting Bitcoin Core Running
Now we need to set up our malicious Bitcoin full nodes. Download Bitcoin Core and install it. The latest will do.
Before we run it, make sure to disconnect your computer from the internet. This is easy, since I’m working in a virtual machine.
We also need to change the time. Right click the time on the toolbar and click “Adjust Date/time”
Turn off automatic time sync and set the time to 9th March 2019, at some time during the day, such that it is after 00:00 UTC.
Time to start Bitcoind, the daemon behind all full nodes. Open a command prompt and enter:
C:\Program Files\Bitcoin\daemon\bitcoind.exe
.
The node will start-up.
bitcoind
will bind to port 0.0.0.0:8333, so any loopback address in 127.x.x.x:8333 will automatically connect to our now malicious node. If you look at the timestamp on the left side of the picture, our node believes the time is 2019-03-09, in the future.
First TimeLock Attempt
Okay, head back to TimeLock and attempt to open Challenge3.x
. We quickly find that TimeLock will happily make DNS requests to those five DNS seed nodes we saw previously, and connect back to our node, but it won’t make any further connections.
How annoying. Time to fix that.
Exploitation
Load up TimeLock.exe into IDA Pro, and open it up in x64dbg. Look at the differences in address schemes, and rebase the program in IDA. My offsets this time around are 7FF613460000
, so I rebased the program by that amount with Edit > Segments > Rebase Program…
We need to work out how these DNS seeds are used. We first try a quick string search:
We got lucky. These strings aren’t encrypted.
Double click the string and take the xref to the function it is used in:
We end up at loc_7FF613464D98
, and it is probably safe to assume from the string that this function is called AddSeedNode().
Head up the top and rename the function, and have a look at the xref’s it provides:
AddSeedNode()
is called in exactly five places. Interesting, seeing as we were only getting five connections to our malicious Bitcoin node.
And there we have it. Five calls to AddSeedNode()
in a row. I suppose that mov ecx, 5Dh
instruction followed by a call sub_7FF613476C30
is where the string decryption happens.
My theory is that calls to AddSeedNode()
is getting us connections to our Bitcoin network. If I patch one of the calls out with a jmp
instruction, then TimeLock only connects to four nodes.
What we want to do is somehow get all 8 connections via these calls. Patching in new assembly code is out of the question - I attempted to insert three new blocks of code, but the alignment was all wrong and IDA would not let me assemble long function calls into small spaces.
So, instead, what we will do is attempt to execute these five sections twice, and we need a debugger to achieve this. Move over to x64dbg.
Place a breakpoint at the first block of code that references AddSeedNode()
, which is 00007FF6134778D1
.
Start the execution of TimeLock, and open up Challenge3.x, clicking resume on the debugger whenever you get trapped, until you hit the breakpoint we set. The password is TimeLock and the answer to the question is 0.02
Step over the first two instructions. We find a familiar sight - the string missionctrl.info
Step over all five blocks of code, but stop when you come to the final jne
instruction:
What we want to do, is change the instruction pointer from the jnz short loc_7FF613477A06
instruction, back to the top of loc_7FF61347794E
. This means we can re-execute this block twice, and we will be able to add 9 DNS seeds instead of 5. This is more than the required amount of 8.
In the debugger, change RIP to 0007FF61347794E:
Now we are back at the top of the second block of code. What we want to do this time, is change the strings of domain names passed to AddSeedNode()
. AddSeedNode()
rejects double ups, so each new strings must be unique. Decrypt the first string, and we see that RAX has a pointer to the heap where the string is stored:
Use the address in RAX, and press ctrl-g
in the bottom heap box, and paste the address in. We find the string in the heap:
Modify the string such that it is different, by double clicking the characters and changing them:
And then keep stepping over the functions in the debugger. Each time a string is decrypted, modify it.
When you reach the final jne
instruction that we stopped at last time, press resume on the debugger.
TimeLock will then go and connect to one of the IP addresses from each of the DNS seed nodes. Since we control DNS and redirect every single request back to localhost, TimeLock will talk to our malicious Bitcoin node.
The malicious Bitcoin node has the time set to the future, and passes all integrity checks, because it is a real, unmodified Bitcoind daemon.
Head back to TimeLock and watch the magic happen.
This sure looks promising…
Yes, yes I do want to reveal the file…
YES YES YES! Oh yeah! YES! Completing challenges never gets old. What a feeling.
Thank you very much u/cryptocomicon for the challenge!
This took me 2.5 days to get here. I tried all sorts of things, but this is the method that worked. TimeLock is starting to get nice and hardened now.
Loot
The private key for the challenge reward, 34r4PbKUM2odwf1EV2Jnxx9d3k1rWKgAzD is p2wpkh-p2sh:Kx4TLBeaMLG19wkeocVX6YG63BTWErKvnTvnPfVgvXf5tD1U1Mij
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 the author of TimeLock by again posting to the Reddit thread
And the reply:
As always, I will be interested in having a look at the next version.
How To Fix The Vulnerability
I successfully executed a Sybil attack against TimeLock. A Sybil attack is really as bad as it can get, when you think about it:
- Every Bitcoin network node is attacker controlled
- Every Bitcoin network node has it time set to the future
- Internet access is disabled and no communication with legitimate nodes is possible
- The attacker controlled Bitcoin nodes look exactly like real nodes
This makes defending against this attack very, very hard. The attacker literally controls everything.
Here is what I suggest:
Do More Validation Against Bitcoin Nodes
I managed to trick TimeLock into accepting a single bitcoind node, as 8 or more nodes. This is a problem.
First things first, you should probably make an attempt to block / ban connections to loopback addresses. That includes 0.0.0.0/24, 127.0.0.1/24, 10.0.0.1/24 and so on. Then an attacker would be forced to find these checks and have to remove them.
Next up, is that you may want to consider making the peering rules a little more concrete. Set up a few connections via seed nodes, five is probably okay. But the remaining connections MUST come from asking nodes for peers. I shouldn’t be able to just add 9 seed nodes and be done with it. Make fetching additional peers mandatory. I would then have to run at least 8 nodes, which is annoying, since I would need 8 virtual machines for that. Attacks get more expensive and difficult.
Try to implement a mechanism to ensure that all the nodes are different. Maybe they have a slight variation in timestamp. On a Bitcoind node hosted locally, there will not be enough latency to have a different returned timestamp in the version command. Over a internet connection there will be. Perhaps each node might have a slightly different view of the mempool.
Attempt to see if there is a live internet connection. Close if there is not. It is very hard to stop bitcoind nodes from connecting to the outside world if the computer has a network connection. If it does, then bitcoind will soon realise it has the wrong time, by talking to other nodes. Even if you control DNS, bitcoind still has fallback hard-coded IP addresses to connect to, and it will use these to connect to other nodes.
I think its worth trying to link block height to the current time. In this case, the hard coded block height in Bitcoin 0.17.1 is from a few weeks ago, and it is the most recent block the node knows about. If a new block is generated every ten minutes-ish, you can at least ballpark predict what the block height is for the current time. If a node says the time is in the future, but when you query the node, the block height is not very high, it can indicate that the node is lying about what the time is.
Continue to Remove Strings
Not all strings are encrypted yet. I recommend going through the rest of them and encrypting them. It managed to give me a leg up in solving this challenge.
Sybil attacks are tricky things to defend against. But if you can defend against one, then you can defend against most attacks. I will keep thinking about defences as well.
If you have any questions, email me. See my about page.
Thanks for reading!
Matthew Ruffell