Intro to Smart Contract Security Audits | Delegatecall (2)

SlowMist
4 min readJul 4, 2022

--

Background

In our previous post, “Insert previous article,” we learnt about the common vulnerability known as the DelegateCall function. To further our understanding of the delegate call function, we will be expanding on what we learned in the previous article.

Introduction to DelegateCall

Check out our precious article.

Vulnerability example

contract Lib { uint public someNumber;
function doSomething(uint _num) public { someNumber = _num; }}
contract HackMe { address public lib; address public owner; uint public someNumber;
constructor(address _lib) { lib = _lib; owner = msg.sender; }
function doSomething(uint _num) public { lib.delegatecall(abi.encodeWithSignature("doSomething(uint256)", _num)); }}

The goal of this attack is still to obtain the owner permission in the HackMe contract. We can see that there are two contracts here, however only the owner of the contract can modify the contract. Take some time and think about how someone can exploit this contract.

Give up? Let’s work on this together.

// SPDX-License-Identifier: MITpragma solidity ^0.8.13;
contract Attack { // Make sure the storage layout is the same as HackMe // This will allow us to correctly update the state variables address public lib; address public owner; uint public someNumber;
HackMe public hackMe;
constructor(HackMe _hackMe) { hackMe = HackMe(_hackMe); }
function attack() public { // override address of lib hackMe.doSomething(uint(uint160(address(this)))); // pass any number as input, the function doSomething() below will // be called hackMe.doSomething(1); }
// function signature must match HackMe.doSomething() function doSomething(uint _num) public { owner = msg.sender; }}

Let’s break this down step by step:

1.Alice deploys the Lib contract.
2. Alice deploys the HackMe contract and passes the address of the Lib contract in the constructor.
3. The attacker Eve deploys the Attack contract and passes the address of the HackMe contract in the constructor.
4. The attacker calls the Attack.attack() function and changes the owner in the HackMe contract to himself.

Now how did this happen?

This attack method cleverly uses the DelegateCall function to modify the characteristics of the storage type variable:
The execution environment of the delegatecall function is the caller’s environment, and the modification of the storage type variable is modified according to the slot location where the called contract variable is stored.

1.The Attack.attack() function first converts its own address to uint256 (making it compatible with the data type in the target contract) and calls the HackMe.doSomething() function for the first time.
2. The HackMe.doSomething() function uses the DelegateCall function to call the Lib.doSomething() function within the address of the incoming Attack contract.
3. The Lib.doSomething() function changes the parameter stored in slot0 of the contract to the incoming value. When the HackMe contract uses DelegateCall to call the Lib.doSomething() function, it will also change the parameter stored in slot0. The value of the variable is changed in the lib parameter (stored here is the address of the Lib contract) to the address of the Attack contract we passed in. At this point, the address of the Lib contract previously stored in the HackMe.lib parameter has been modified to the address of the Attack contract.
4. The Attack.attack() function is called again using the HackMe.doSomething() function. Since we have already changed the HackMe.lib variable in the previous step to the address of the Attack contract, the HackMe.doSomething() function will no longer call the previous Lib contract, but use DelegateCall to call the Attack.doSomething() function. Let’s observe the code of the Attack contract again. We can see that the storage location of its variables is deliberately consistent with the HackMe contract. It’s not difficult to see that the content of the Attack.doSomething() function is also written by the attacker as owner = msg.sender. This operation modifies the variable with the storage location of slot1 in the contract. Therefore, the HackMe contract uses the DelegateCall to call the Attack.doSomething() function to change the variable owner with the storage location of slot1 in the contract to msg. Sender from the attackers address, completing their attack.

Repair Suggestions

For Developers
1. When using DelegateCall, it should be noted that the address of the called contract cannot be controllable.

2. In a more complex contract environment, you need to pay attention to the declaration order and storage location of variables. This is because when using DelegateCall for external calls, the data stored in the corresponding slot of this contract will be modified according to the data structure of the called contract. When the data structure changes, this may cause unexpected variables to be overwritten.

For Auditors

1. When encountering the use of DelegateCall in the contract during the audit process, auditors need to pay attention to whether the called contract address is controllable.

2. When the function in the called contract modifies the storage variable, auditors should pay attention to the location of the variable storage slot. This is to avoid the storage variable stored in this contract being overwritten by error due to inconsistent data structure.

--

--

SlowMist

SlowMist is a Blockchain security firm established in 2018, providing services such as security audits, security consultants, red teaming, and more.