-
Lockbox
利用modifier,套娃。
modifier _() {
_;
assembly {
//第一步:拿到全局变量Stage, 判断如果stage的值没有更新则返回
let next := sload(next_slot)
if iszero(next) {
return(0, 0)
}
//第二步:调用Stage上的getSelector()函数,将结果存储在内存中
// keccak(abi.encode("getSelector"))[0:0x04] = 0x034899bc
mstore(0x00, 0x034899bc00000000000000000000000000000000000000000000000000000000)
pop(call(gas(), next, 0, 0, 0x04, 0x00, 0x04))
//第三步:调用Stage合约,函数选择器为getSelector()函数的返回值,参数为CALLDATA[0x04:]
calldatacopy(0x04, 0x04, sub(calldatasize(), 0x04))
switch call(gas(), next, 0, 0, calldatasize(), 0, 0)
//第四步:如果调用失败,则REVERT
case 0 {
returndatacopy(0x00, 0x00, returndatasize())
revert(0x00, returndatasize())
}
case 1 {
returndatacopy(0x00, 0x00, returndatasize())
return(0x00, returndatasize())
}
}
}
Entrypoint
随机数预测,传入 bytes4(blockhash(block.number - 1)) 即可。
function solve(bytes4 guess) public _ {
require(guess == bytes4(blockhash(block.number - 1)), "do you feel lucky?");
solved = true;
}
Stage 1
需要获得 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf 的私钥,这是一个非常出名的地址,其私钥为:0x0000000000000000000000000000000000000000000000000000000000000001
function solve(uint8 v, bytes32 r, bytes32 s) public _ {
require(ecrecover(keccak256("stage1"), v, r, s) == 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, "who are you?");
}
注意:在一般的web3.js中,在eth-sign时,会在签名的消息前加上x19Ethereum Signed Message + len(msg)一段bytecode。故需使用不加入此消息的eth-sign函数库。
起初使用we3py
from eth_account import Account, messages
from eth_account.messages import encode_defunct
from web3 import Web3, HTTPProvider
rpc = "https://mainnet.infura.io/v3/0aec7fd42e0a40f28dd6c1a185f7d3e6"
web3 = Web3(HTTPProvider(rpc))
messageshash = web3.toHex(web3.sha3(text='stage1'))
print(messageshash)
private_key_hex = "0x0000000000000000000000000000000000000000000000000000000000000001"
signed_message = Account.signHash(message_hash=messageshash, private_key=private_key_hex)
print("signature =", signed_message)
print("r = ", web3.toHex(signed_message.r))
print("s = ", web3.toHex(signed_message.s))
print("v = ", web3.toHex(signed_message.v))
计算得出结果:
r = 0x370df20998cc15afb44c2879a3c162c92e703fc4194527fb6ccf30532ca1dd3b
s = 0x35b3f2e2ff583fed98ff00813ddc7eb17a0ebfc282c011946e2ccbaa9cd3ee67
v = 0x1b
但是由于此结果不能满足Stage3所以使用原始ecdsaSign来签名。
const ethereumjs_util = require("ethereumjs-util");
const { randomBytes } = require('crypto');
const { ecdsaSign } = require('ethereum-cryptography/secp256k1')
const privateKeyStr = '0x0000000000000000000000000000000000000000000000000000000000000001';
const hashStr = '0x' + (ethereumjs_util.keccak(Buffer.from('stage1'), 256)).toString('hex');
const privateKey = Buffer.from(privateKeyStr.slice(2), "hex");
const hash = Buffer.from(hashStr.slice(2), "hex");
while (true) {
// node_modules /@types/secp256k1/index.d.ts
const { signature, recid } = ecdsaSign(hash, privateKey, { data: randomBytes(32) });
v = recid + 27;
r = Buffer.from(signature.slice(0, 32))
s = Buffer.from(signature.slice(32, 64))
if (v != 28) {
continue;
}
const rBN = '0x' + r.toString('hex');
const sBN = '0x' + s.toString('hex');
// stage3 require: out of order
if (sBN < rBN) {
continue;
}
// // stage3 require: this is a bit odd
if (sBN.slice(-2) % 2 != 0) {
continue;
}
break;
}
console.log('0x' + v.toString(16));
console.log('0x' + r.toString('hex'));
console.log('0x' + s.toString('hex'));
得到答案:
v = 0x1c
r = 0x1f9c5510565172835329f4e0107b3af787bf46d1690f7e81aba39e47c9940d43
s = 0x6e95dc6553997968a1be6cc8ae66dc1730cd1965f8b3e7114ca0f9df15fc3e98
Stage 2
function solve(uint16 a, uint16 b) public _ {
require(a > 0 && b > 0 && a + b < a, "something doesn't add up");
}
取了两个storagede uint。
Stage 3
function solve(uint idx, uint[4] memory keys, uint[4] memory lock) public _ {
require(keys[idx % 4] == lock[idx % 4], "key did not fit lock");
for (uint i = 0; i < keys.length - 1; i++) {
require(keys[i] < keys[i + 1], "out of order");
}
for (uint j = 0; j < keys.length; j++) {
require((keys[j] - lock[j]) % 2 == 0, "this is a bit odd");
}
}
这时的slot部署是这样的:
slot0 idx guess v
slot1 keys[0] r
slot2 keys[1] s
slot3 keys[2]
slot4 keys[3]
slot5 lock[0]
slot6 lock[1]
这里有三个条件
-
keys[idx % 4] == lock[idx % 4]由于idx = 0xff1c,所以idx % 4 = 0即keys[0] == lock[0]。
-
keys[i] < keys[i + 1]要求呈递增排列。
-
(keys[j] - lock[j]) % 2 == 0对应位置的差值必须是偶数,两个偶数相减肯定是偶数,两个奇数相减肯定也是偶数。
Stage 4
function solve(bytes32[6] choices, uint choice) public _ {
require(choices[choice % 6] == keccak256(abi.encodePacked("choose")), "wrong choice!");
}
求在前6块slot中部署一个abi.encodePacked(“choose”),这里还剩下slot3和slot4为空,由于需要满足差值为偶数,所以只能放在slot4。
Stage 5
function solve() public _ {
require(msg.data.length < 256, "a little too long");
}
要求整个data的长度不能超过256。
所以综上,得出攻击代码。
import "./Setup.sol";
contract lockBoxExploit {
Entrypoint public entrypoint;
constructor(address _setup) public {
entrypoint = lockBoxSetup(_setup).entrypoint();
}
function exploit() public {
bytes memory data = abi.encodePacked(
entrypoint.solve.selector,
uint(uint16(0xff1c) | (uint256(bytes32(bytes4(blockhash(block.number - 1))))),
bytes32(0x1f9c5510565172835329f4e0107b3af787bf46d1690f7e81aba39e47c9940d43), //r
bytes32(0x6e95dc6553997968a1be6cc8ae66dc1730cd1965f8b3e7114ca0f9df15fc3e98), //s
bytes32(0x6e95dc6553997968a1be6cc8ae66dc1730cd1965f8b3e7114ca0f9df15fc3e9a), //满足差值偶数
bytes32(keccak256('choose')),
bytes32(0x1f9c5510565172835329f4e0107b3af787bf46d1690f7e81aba39e47c9940d43), // =r
bytes32(0x0000000000000000000000000000000000000000000000000000000000000004) //做的choice 也就是指向abi.encodePakced("choose")的指针
);
uint size = data.length;
address entry = address(entrypoint);
assembly{
switch call(gas(),entry,0,add(data,0x20),size,0,0)
case 0 {
returndatacopy(0x00,0x00,returndatasize())
revert(0, returndatasize())
}
}
}
}
原文始发于微信公众号(ChainSecLabs):Paradigm CTF 2021——Lockbox