Skip to content

Instantly share code, notes, and snippets.

@YangSeungWon
Last active June 11, 2020 08:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save YangSeungWon/c0441468c4d25a8487f3bd8a8737d8e1 to your computer and use it in GitHub Desktop.
Save YangSeungWon/c0441468c4d25a8487f3bd8a8737d8e1 to your computer and use it in GitHub Desktop.

2020 Defenit CTF - Bad Tumblers

tags: blockchain

[name=whysw@PLUS]

Attachments

Attachments are uploaded on gist.

Challenge

[Precondition]
0. Hundreds of wallets contain about 5 ether (tumbler)
0. Hackers steal more than 400 ethers through hacking per exchange
0. Hacker commissions ethereum tumbler to tumbling 400 ether from his wallet
0. After tracking the hacking accident that reported by exchange A, we confirmed that it was withdrawn to the hacker wallet of exchange C.
0. After checking the amount withdrawn from the wallet of exchange C, it seems that it was not the only exchange(exchange A) that was robbed.
0. Therefore, it is a problem to find a hacker wallet of some exchange(in this chall, it will be exchange B). So, we should find clues to track the hacker.

[Description]
Hacker succeeded in hacking and seizing cryptocurrency, Ethereum! Hacker succeeded in withdraw Ethereum by mixing & tumbling. What we know is the account of the hacker of exchange A which reported the hacking and exchange C which hacker withdrew money. In addition to Exchange A, there is another exchange that has been hacked. Track hackers to find out the bad hacker's wallet address on another exchange!

* Please refer to attached concept map
* In this challenge, all address is on ropsten network
* Please ignore fauset address (or assume it is an exchange's wallet)
* exchange A, hacker's address : 0x5149Aa7Ef0d343e785663a87cC16b7e38F7029b2
* exchange C, hacker's address : 0x2Fd3F2701ad9654c1Dd15EE16C5dB29eBBc80Ddf
* flag format is 0xEXCHANGE_B_HACKER_CHECKSUM_ADDRESS Defenit{0x[a-zA-Z0-9]}

Solution

Get Transaction Data

In https://ropsten.etherscan.io/, we can see all ethereum accounts and transactions on ropsten network.

So I saw hacker's account in exchange A. https://ropsten.etherscan.io/address/0x5149Aa7Ef0d343e785663a87cC16b7e38F7029b2

At first, it gets 5ETH from faucet address(which the author make us ignore). After that, it get money from the accounts of victims.

Then it goes to tumbler network.

So I decided to follow transactions of this account, and find all related accounts.

Unfortunately, I didn't know that ethscan provides API 😿 This is my crawler.

import sys
import cfscrape
import re


def parseData(data):
    ret = ""
    regex = re.compile("<tr>.*?(?P<from>0x.{40})<.*?(?P<to>0x.{40}).*?</tr>",re.MULTILINE)
    matchedAll = regex.findall(data)
    for matched in matchedAll:
        ret += matched[0]
        ret += " "
        ret += matched[1]
        ret += "\n"
    return ret


def getTXs(address):
    TXs = ""
    for i in range(10):
        scraper = cfscrape.create_scraper()
        res = scraper.get(f'https://ropsten.etherscan.io/txs?ps=100&p={i+1}&a={address}').content
        TXs += parseData(res.decode('utf-8'))
    return TXs



assert(len(sys.argv) == 2)

with open(sys.argv[1].lower() + ".csv", "w") as f:
    f.write(getTXs(sys.argv[1]))

Now I can parse Transaction Data.


it's time to Travel!

I saved TX datas in sub folder and reused that.

1. Distinguish Faucet

If you reach faucet address, then you should stop your travel since it has so many transactions to explore, which has no meaning. I filtered faucet account by checking if the number of TXs is larger than 1,000.

2. Distinguish Hacker's Account

The Hacker's account initially receives a large amount of deposit from victims. So I checked if there is a countinuous deposit at first.


Solver

import sys
import os
import cfscrape
import re


def isCalculated(address):
    return os.path.isfile(f"./data/{address}.csv")


def parseData(data):
    ret = ""
    regex = re.compile("<tr>.*?(?P<from>0x.{40})<.*?(?P<to>0x.{40}).*?</tr>",re.MULTILINE)
    matchedAll = regex.findall(data)
    for matched in matchedAll:
        ret += matched[0]
        ret += " "
        ret += matched[1]
        ret += "\n"
    return ret


def getTXs(address):
    TXs = ""
    for i in range(10):
        scraper = cfscrape.create_scraper()
        res = scraper.get(f'https://ropsten.etherscan.io/txs?ps=100&p={i+1}&a={address}').content
        if res == None:
            break
        TXs += parseData(res.decode('utf-8'))
    return TXs


def writeTXs(address):
    if isCalculated(address): return True
    with open(f"data/{address}.csv", "w") as f:
        data = ""
        while data == "":
            data = getTXs(address)
        f.write(data)
        if len(data)/86 == 10*100:
            print("more than 1000 TX??? may be faucet")
            return False
    return True


def find(address):
    global visited
    address = address.lower()
    visited.append(address)
    print(f"trying to find TXs related to {address}...")
    if not writeTXs(address):  # may be faucet
        return
    with open(f"data/{address}.csv", "r") as f:
        alldata = f.readlines()

    if checkMainAcc(address, alldata):
        print("Important Account! maybe hacker's main one...")
        sys.exit()

    for line in alldata:
        data = line.strip().split(" ")
        _from = data[0]
        _to = data[1]
        if _from == address and _to not in visited:
            find(_to)
        elif _to != sys.argv[1].lower() and _to == address and _from not in visited:
            find(_from)


def checkMainAcc(address, data):
    if address == sys.argv[1].lower():   # account A
        return False
    for i, line in enumerate(reversed(data)):  # from the end(past in time)
        if line.strip().split(" ")[1] == address:  # if it's deposit
            pass
        elif i < 50:
            return False # less than 50 deposits at first
        else:
            return True  # more than 50 deposits at first
    return False



assert(len(sys.argv) == 2)  # get account A's address by sys.argv[1]

visited = []
find(sys.argv[1])

output :

trying to find TXs related to 0xf34bd0333333ed358964fd7ef04047daeae62ee6...
trying to find TXs related to 0xc06e1b1deaf684143de564d9bcc9e9f19609c5b1...
trying to find TXs related to 0xd634026e25128f6c4316223b8f606213497b39ef...

...

trying to find TXs related to 0xd25be7b3c3008e3508f1cbfc0880585561938d43...
trying to find TXs related to 0x1fc64526d77d079f7c8f6680d0b88b6046663949...
trying to find TXs related to 0x4c5e179bbc6d393affb72018f9bba4b3cee6de65...
Important Account! maybe hacker's main one...

FLAG : Defenit{0x4c5E179bBc6D393AffB72018f9bba4b3Cee6dE65}

import sys
import os
import cfscrape
import re
def isCalculated(address):
return os.path.isfile(f"./data/{address}.csv")
def parseData(data):
ret = ""
regex = re.compile("<tr>.*?(?P<from>0x.{40})<.*?(?P<to>0x.{40}).*?</tr>",re.MULTILINE)
matchedAll = regex.findall(data)
for matched in matchedAll:
ret += matched[0]
ret += " "
ret += matched[1]
ret += "\n"
return ret
def getTXs(address):
TXs = ""
for i in range(10):
scraper = cfscrape.create_scraper()
res = scraper.get(f'https://ropsten.etherscan.io/txs?ps=100&p={i+1}&a={address}').content
if res == None:
break
TXs += parseData(res.decode('utf-8'))
return TXs
def writeTXs(address):
if isCalculated(address): return True
with open(f"data/{address}.csv", "w") as f:
data = ""
while data == "":
data = getTXs(address)
f.write(data)
if len(data)/86 == 10*100:
print("more than 1000 TX??? may be faucet")
return False
return True
def find(address):
global visited
address = address.lower()
visited.append(address)
print(f"trying to find TXs related to {address}...")
if not writeTXs(address): # may be faucet
return
with open(f"data/{address}.csv", "r") as f:
alldata = f.readlines()
if checkMainAcc(address, alldata):
print("Important Account! maybe hacker's main one...")
sys.exit()
for line in alldata:
data = line.strip().split(" ")
_from = data[0]
_to = data[1]
if _from == address and _to not in visited:
find(_to)
elif _to != sys.argv[1].lower() and _to == address and _from not in visited:
find(_from)
def checkMainAcc(address, data):
if address == sys.argv[1].lower(): # account A
return False
for i, line in enumerate(reversed(data)): # from the end(past in time)
if line.strip().split(" ")[1] == address: # if it's deposit
pass
elif i < 50:
return False # less than 50 deposits at first
else:
return True # more than 50 deposits at first
return False
assert(len(sys.argv) == 2) # get account A's address by sys.argv[1]
visited = []
find(sys.argv[1])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment