Sleuthing DarkSide Crypto-Ransom Payments with the Wolfram Language
Let me tell you a story about how to trace Russian hackers’ cryptocurrency funds using only public knowledge, some educated guesses and the Wolfram Language.
But first, a little background information.
On May 7, 2021, Colonial Pipeline was the victim of a ransomware attack, which forced the largest fuel pipeline in the US to shut down its operations. The next day, the FBI confirmed the Russian hacker group DarkSide was responsible. At almost the same time, German chemical distribution company Brenntag was the victim of a separate DarkSide ransomware attack.
On May 12, Colonial Pipeline resumed operations, and the next day DarkSide announced it was disbanding and would compensate all outstanding financial obligations by May 23. Reports also emerged that Colonial Pipeline and Brenntag had paid substantial bitcoin ransoms.
To see what we can learn about all this, we first need to synchronize our watches and blockchains:
Engage with the code in this post by downloading the Wolfram Notebook
✕
$TimeZone = 0; $BlockchainBase = "Bitcoin"; CloudConnect[]; |
Some news outlets said they obtained information from the victims and identified the wallets that payments were sent to, but didn’t disclose which ones. All transactions on Bitcoin are public, so I decided to look for myself and put our blockchain integrations to the test. Bitcoin has, however, hundreds of thousands of daily transactions, and I needed something solid to look for.
From widely shared reports, I knew Colonial Pipeline paid 75 bitcoins (BTC) in ransom at some point on May 8, and Brenntag paid 78.29 BTC on May 11 to the same group of hackers. News reports usually speak with respect to their local time zone, and information systems, like blockchains, with respect to Coordinated Universal Time (UTC). I wasn’t sure how large a window of transactions I should scan for Colonial Pipeline’s payment on May 7–9 to cover the whole day of May 8 in all time zones from the United States’s East Coast to eastern Europe.
On the other hand, Brenntag is in Germany, which is the same time zone I live in, and close to UTC. The 78.29 BTC paid also looked like a more definitive pattern to search for, so I decided to investigate that clue first.
As a starting point, I took a random block mined a couple of hours after midnight on May 12 and started iterating backward, looking for transactions with an input amount larger than 78 BTC. Being such a big sum, there were only a handful per block and sometimes none.
(Why look for larger transactions and not exactly equal ones? Just as with cash banknotes, it’s common for transactions to have a bigger input value than the actual amount and then return the difference.)
I planned initially to generate a list of all suspiciously large transactions on that day and analyze them one by one. But luckily I didn’t have to because the scan quickly came across a transaction of exactly 78.29 BTC:
✕
currentblock = 683175; blocktxs = ParallelMap[{#, BlockchainTransactionData[#, "TotalInput"]} &, BlockchainBlockData[currentblock, "TransactionList"]]; Select[blocktxs, Last[#] > Quantity[78, "Bitcoins"] &] |
Right away, I checked at what time the block the transaction was in was mined:
✕
BlockchainBlockData[683175, "TimeMined"] |
The block was mined, which effectively performed the transfer, at 22:24 GMT on May 11. Could it be the deadline the hackers gave was midnight? We may never know.
Following the Bitcoin Tracks
This transaction had the exact amount I was looking for as input:
✕
First[BlockchainTransactionData[ "b5174dfd8fc409fd8576b59ba49983b49da2294a5975ef8f9dfea7c1fee0a65f", "Inputs"]] |
The address the funds were transferred from was:
✕
First[%["Addresses"]] |
Let’s see what we can learn from public information about this address:
✕
BlockchainAddressData[%] |
The balance on this address is zero and the total transaction count is two, which means the address was only used as a temporary relay for receiving the ransom payment and transferring it somewhere else. Single-use addresses are a common practice, so nothing is suspicious about this particular detail.
The first transaction received the 78.29 BTC payment from Brenntag at 22:04 on May 11, tracing back to the Binance exchange. The second and last transaction 20 minutes later showed the bitcoins were split between two new addresses:
✕
%[["TransactionList", 1, "Outputs"]] |
No surprise… those were now empty too:
✕
BlockchainAddressData["bc1qxu83k5qkj8kcqdqqenwzn7khcw4llfykeqwg45", \ "Balance"] |
✕
BlockchainAddressData["bc1qqklq840yf39zdm4kvcvjfwrxdm27fxtxe37sjj", \ "Balance"] |
Then a transaction in one of the two addresses caught my eye:
✕
Last[BlockchainAddressData[ "bc1qxu83k5qkj8kcqdqqenwzn7khcw4llfykeqwg45", "TransactionList"]] |
It received part of the 75 BTC payment on May 8, which matched the Colonial Pipeline payment and can be traced back to the Coinbase exchange:
✕
BlockchainTransactionData[%["TransactionID"], {"TotalInput", "Timestamp"}] |
The latest transaction listed on the same address emptied it and several other addresses on May 13, the day DarkSide announced it was ceasing operations:
✕
First[BlockchainAddressData[ "bc1qxu83k5qkj8kcqdqqenwzn7khcw4llfykeqwg45", "TransactionList"]] |
The output address still had the 107.8 BTC, currently worth $4.5 million USD, on it:
✕
BlockchainAddressData["bc1q2sewgrnau4e4gvceh8ykzf8lqxawpluu0k0607", \ {"Balance", "ValueInUSDollars"}] |
Of course, 75 and 78 do not add up to 107, so some of the bitcoins were likely paid to people who provided information about victims’ system vulnerabilities, and some were cashed out in smaller amounts.
The Cluster of Addresses DarkSide Used
The transaction that hoarded 107.8 BTC to a single address has quite a few senders:
✕
BlockchainTransactionData[ "b0e381d02d966acbcd9224817e3db50b2bc3566e0060db36a6a17ee163152dd7", "Inputs"] // Length |
It’s generally easier for me to digest information visually. Here’s how the transaction is represented as a graph of all those senders combining their funds at a single address:
✕
(* generate directed edges from transaction inputs to the outputs *) transactionGraph[txid_] := Module[ {txinfo = BlockchainTransactionData[txid], edges}, edges = Join[ (*inputs in red*) Style[DirectedEdge[#[["Addresses", 1]], txid, -#["Amount"]], Darker[Red]] & /@ txinfo["Inputs"], (*outputs in green*) Style[DirectedEdge[txid, #[["Addresses", 1]], #["Amount"]], Darker[Green]] & /@ txinfo["Outputs"] ]]; (* layered plot for transaction graphs *) transactionGraphPlot[edges_, txids : {__}] := Block[{txvertices = (Rule[#, "Square"] & /@ txids)}, If[$CloudEvaluation, LayeredGraphPlot[edges, VertexShapeFunction -> txvertices], LayeredGraphPlot[edges, VertexShapeFunction -> txvertices, VertexLabels -> Placed["Name", Tooltip], EdgeLabels -> Placed["EdgeTag", Tooltip]] ] ]; (* plot multiple transactions together *) transactionsCommonGraph[txid_] := transactionsCommonGraph[{txid}]; transactionsCommonGraph[ txids : {__}] := {Flatten[ transactionGraph /@ DeleteDuplicates[txids]], DeleteDuplicates[txids]} (* filter incoming transactions to an address*) addressInTransactions[address_] := Block[ {alltx = BlockchainAddressData[address, "TransactionList"], incomingtx}, incomingtx = Select[ alltx, (! MemberQ[Flatten[#[["Inputs", All, "Addresses"]]], address] && MemberQ[Flatten[#[["Outputs", All, "Addresses"]]], address] && ! MemberQ[Flatten[#[["Outputs", All, "Addresses"]]], "1Lets1xxxx1use1xxxxxxxxxxxy2EaMkJ"]) &]; (* Note: 1Lets1xxxx1use1xxxxxxxxxxxy2EaMkJ is an address used in spam \ transactions, had to filter out the noise *) incomingtx = incomingtx[[All, "TransactionID"]] ]; (* filter outgoing transactions from an address*) addressOutTransactions[address_] := Block[ {alltx = BlockchainAddressData[address, "TransactionList"], outcomingtx}, outcomingtx = Select[ alltx, (MemberQ[Flatten[#[["Inputs", All, "Addresses"]]], address] ) &]; outcomingtx = outcomingtx[[All, "TransactionID"]] ]; (* time between first and last transaction of address*) addressLifetime[address_] := Block[ {txs = BlockchainAddressData[address, "TransactionList"]}, DateInterval[{Last[txs]["Timestamp"], First[txs]["Timestamp"]}] ]; (* get addresses that sent transactions to address*) sendersTo[address_] := Block[ {alltx, incomingtx}, alltx = BlockchainAddressData[address, "TransactionList"]; (* find only the transactions where given address received funds*) incomingtx = Select[ alltx, ! MemberQ[Flatten[#[["Inputs", All, "Addresses"]]], address] && MemberQ[Flatten[#[["Outputs", All, "Addresses"]]], address] &]; DeleteDuplicates@ Flatten[incomingtx[[All, "Inputs", All, "Addresses"]]] ]; (* total bitcoin amount of received transactions *) addressTotalIncome[address_] := Block[ {txs}, txs = addressInTransactions[address]; Total[ Flatten[ Select[BlockchainTransactionData[#, "Outputs"], MatchQ[ #["Addresses"], {address}] &] & /@ txs][[All, "Amount"]]]]; id = "b0e381d02d966acbcd9224817e3db50b2bc3566e0060db36a6a17ee163152dd7\ "; transactionGraphPlot[transactionGraph[id], {id}] |
The top layer of vertices is the “spending” addresses, a square vertex in the middle symbolizes the transaction that unites them and the vertex at the bottom is the receiving address. Red and green arrows symbolize a negative or positive change in the address’s balance.
The receiver address bc1q2sewgrnau4e4gvceh8ykzf8lqxawpluu0k0607 was only active for a few minutes on May 13 with these three transactions:
✕
BlockchainAddressData["bc1q2sewgrnau4e4gvceh8ykzf8lqxawpluu0k0607", "TransactionList"][[All, "Timestamp"]] |
Let’s visualize them together:
✕
transactionGraphPlot @@ transactionsCommonGraph[ addressInTransactions["bc1q2sewgrnau4e4gvceh8ykzf8lqxawpluu0k0607"]] |
This address has only three transactions, but they come from three dozen different senders:
✕
cluster = sendersTo["bc1q2sewgrnau4e4gvceh8ykzf8lqxawpluu0k0607"] |
It’s not known whether the receiving address belongs to DarkSide, but most of these 27 addresses that have sent this combined $5 million transferred to it do.
✕
Length[%] |
Both the Colonial Pipeline and Brenntag ransom payments were made to these addresses, and they all signed this joint transaction.
At least, these addresses did belong to DarkSide. Both payments that we know of were split among several other addresses and then reunited multiple times. The creation of addresses and the divide-and-conquer transfers were probably done automatically to randomize DarkSide’s tracks and make piecing the puzzle together more difficult.
All the addresses had been active only since March and the last transactions were on May 13:
✕
TimelinePlot[ addressLifetime /@ cluster] |
In this time period, the addresses received transactions in varying amounts:
✕
addressTotalIncome /@ cluster |
These transactions total an impressive 370 BTC, which is over $13 million USD:
✕
Total[%] |
This suggests that there were other DarkSide ransomware victims who didn’t get into the media spotlight or who preferred to keep their ransom events secret.
The Bigger Picture
Let’s go a step further to see “who paid the payer.” This graph takes all 27 previous “sender” addresses and traces funds a layer back: the transactions that have sent bitcoins to these addresses. Most of the transactions merely split a payment between two addresses in the 27-cluster, showing again how interwoven they are:
✕
l1tx = addressInTransactions[ "bc1q2sewgrnau4e4gvceh8ykzf8lqxawpluu0k0607"]; l2tx = Flatten[addressInTransactions /@ cluster]; transactionGraphPlot @@ transactionsCommonGraph[Flatten[{l1tx, l2tx}]] |
On the right, we can see the splits of the infamous 78.29 and 75 BTC payments by ransom victims Brenntag and Colonial Pipeline. In the structure of our graph, there are also more transactions that follow the same pattern, adding up to the 370 BTC computed earlier.
If we plot the outgoing transactions from the cluster, we see many more addresses to which parts of ransom payments were sent:
✕
clusterOutTx = Flatten[addressOutTransactions /@ cluster]; transactionGraphPlot @@ transactionsCommonGraph[clusterOutTx] |
Most of them were sent on May 13, when the group disbanded:
✕
outTxTimes = BlockchainTransactionData[#, "Timestamp"] & /@ clusterOutTx; DateHistogram[outTxTimes, "Day", ChartElementFunction -> ChartElementDataFunction["GradientScaleRectangle", "ColorScheme" -> "ThermometerColors"]] |
May 8 has a notable bump, likely meaning hackers paid some dues for the work and cashed out some of the gains after the Colonial Pipeline payment. Some of those can be traced to exchanges big and small, centralized and peer to peer. Others have finished in people’s wallets.
A Final Note
These are the highlights of my findings, and there’s surely much more left to discover, so feel free to interact with the code embedded in this article and share your findings too.
On a personal note, it’s an empowering feeling to have all the right tools in the Wolfram Language at hand to embark on a cyber-investigative computational journalism adventure. Even more so, it’s great being a part of Wolfram Blockchain Labs, which develops the blockchain integration and analytics for the Wolfram technology ecosystem.
We’ll continue to sharpen our tools to be ready whenever needed. Just like this time… and the inevitable next time.
Want to know more? Read Dariia Porechna’s follow-up post “DarkSide Update: The FBI Hacks the Hackers?” about the FBI’s June 7, 2021, seizure of $2.3 million Colonial Pipeline paid DarkSide.
Connect with Wolfram Blockchain Labs to find out about integrating your blockchain into the Wolfram Language. Connect with Wolfram Technical Consulting to start a blockchain project. |
It is hard to feel enthusiastic about Blockchain and cryptocurrency technology when each proof-of-work validation uses more energy than a typical American household in a month and the cryptocurrencies are being used for illicit activity like the ransom described above. When other cryptocurrencies like Monero and Tor are being used by drug lords and illegal money launderers then you have to wonder about the supposed social benefits of such technologies.