CS8803: Exploiting Smart Contract and DeFi

Taesoo Kim



CS8803: Solidity Internal

Taesoo Kim

Recap

About Part 2

Timeline

A Few Pointers

This Week’s Study

Ethereum Layers

Ref. EI §2

EVM Architecture

Ref. EI §2

EVM: Memory Spaces

Ref. EI §2

Transaction Types

Ref. EI §2

Example: A Message Call

>>> s = Storage.deploy()
>>> tx = s.store(0xdeadbeef)
>>> web3.eth.get_transaction(tx.txid)
  hash             : 0x50fee6c148904dd7dd5c7017f79bc4396aa35007b677ebcce250aa58c530e967,
  nonce            : 11,
  blockHash        : 0xadf3166bedf07267d3d31113e3b8b616164ff530534f77a4570d8b210899fb31,
  blockNumber      : 12,
  transactionIndex : 0,
  from             : 0x66aB6D9362d4F35596279692F0251Db635165871,
  to               : 0xb6286fAFd0451320ad6A8143089b216C2152c025,
  value            : 0,
  gas              : 12000000,
  gasPrice         : 200000000,
* input            : 
*  0x6057361d00000000000000000000000000000000000000000000000000000000deadbeef,
  v                : 2710,
  r                : 0xf8d77b2bc96345c2c9a91f55cca6487af2af303c98885bcfc1f5e8dff0dabd7a,
  s                : 0x6d77c32a0db4d5a9a1773c9486ecd4ca2353db3b9b66790414b64c14c755108b,

A Message Call → EVM Code

> web3.eth.get_code(s.address)
'0x6080604052348015600f57600080fd5b506004361060...'

> print(evm_disasm(web3.eth.get_code(s.address)))
00000: PUSH1 0x80
00002: PUSH1 0x40
00004: MSTORE
00005: CALLVALUE
00006: DUP1
00007: ISZERO
00008: PUSH1 0x0f
0000a: JUMPI
0000b: PUSH1 0x00
0000d: DUP1
0000e: REVERT
0000f: JUMPDEST
...

Dispatching to Solidity Functions

contract Storage {
    uint256 number;

    function store(uint256 num) public {
        number = num;
    }
    ...
}

Function Selector

[4-byte function selector][a list of arguments]

1) function selector: web3.keccak(text="store(uint256)")[:4] == 0x6057361d
2) appending a list of arguments
e.g., input:

[0x6057361d][00000000000000000000000000000000000000000000000000000000deadbeef]
+----------  +---------------------------------------------------------------
|            |
+-> 4 bytes  +-> 0x20 bytes (uint256 num)

How Function Selector is Used? (Decompiler)

function main() {
    // 1. what's this for?
    var var0 = msg.value;
    if (var0) { revert(memory[0x00:0x00]); }

    // 2. what's this for?
    if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); }

    // 3. what's this for?
    var0 = msg.data[0x00:0x20] >> 0xe0;

    if (var0 == 0x2e64cec1) {
        // Dispatch table entry for retrieve()
    } else if (var0 == 0x6057361d) {
        // Dispatch table entry for store(uint256)
    } else { revert(memory[0x00:0x00]); }
}

Today’s Tutorial: Finding Collision!

function submitProofOfCollision(string memory func2) external {
    string memory func1 = "callMeIfYouCan(uint256,bool)";

    uint hash1 = keccak256(abi.encodePacked(func1));
    uint hash2 = keccak256(abi.encodePacked(func2));

    require(hash1 != hash2, "func1 != func2")
    require(hash1 >> (256 - 32) == hash2 >> (256 - 32),
            "Find two functions that have the same selector");
    completed1 = true;
}

Wait, What About a “Constructor”?

contract Ballot {
    address public chairperson;
    Proposal[] public proposals;

    constructor(bytes32[] memory proposalNames) {
        chairperson = msg.sender;
        voters[chairperson].weight = 1;
        ...
    }
}

How to Map High-level Data Structures to EVM?

// primitive types
uint weight;
bool voted;
string name;
bytes data;
address chairperson;

// data structure
mapping(address => Voter) voters;
Proposal[] proposals;

Ref. EI §2

Understanding Storage Slot (256-bit per Slot)

contract A {
    uint public x;      // -> storage @slot-0 (key=0)
    uint256 public y;   // -> storage @slot-1 (key=1)
    uint128 public z0;  // -> storage @slot-2 (key=2)
    uint8 public z1;    // -> storage @slot-?
    uint16 public z2;   // -> storage @slot-?
    bool public b;      // -> storage @slot-?
    S public s;         // -> storage @slot-?
    address public addr;// -> storage @slot-?
    ...
}

Ref. EI §2

Example: Mapping Primitive Types to Slots

>>> src = open("contract/A.sol").read()
>>> dump_layout(src)
[00]@000-031 x                    : I/032B uint256
[01]@000-031 y                    : I/032B uint256
[02]@000-015 z0                   : I/016B uint128
[02]@016-016 z1                   : I/001B uint8
[02]@017-018 z2                   : I/002B uint16
[02]@019-019 b                    : I/001B bool
...

Squeezing Multiple Primitive Types into One Slot

Bytes and String (In-place)

Example: Bytes and String (In-place)

# when size <= 31B, inplace encoding. See, one byte size, 0x06/2 = 3B.
>>> a.set_b1(0xaabbcc)
>>> a.get_storage_at(12)
'0xaabbcc0000000000000000000000000000000000000000000000000000000006'
                                                          (size) --
# size == 31B, 0x3e/2 = 31B
>>> a.set_b1(2**(256-8)-1)
>>> a.get_storage_at(12)
'0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff3e'
                                                          (size) --

Bytes and String (Overflowed)

Example: Bytes and String (Overflowed)

# if the data is bigger than 32B, it uses keccak256(12) + N
# where N == size // 32
>>> a.set_b1(2**400-1)
>>> a.get_storage_at(12)
'0x0000000000000000000000000000000000000000000000000000000000000065'

>>> a.get_storage_at(web3.solidityKeccak(["uint256"], [12]))
'0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'

>>> a.get_storage_at(web3.solidityKeccak(["uint256"], [12]).int()+1)
'0xffffffffffffffffffffffffffffffffffff0000000000000000000000000000'

Mapping Dynamic Array to Slots

Example: Dynamic Array

# two elements in the array declared at slot 10
>>> a.get_storage_at(10)
'0x0000000000000000000000000000000000000000000000000000000000000002'

>>> a.get_storage_at(web3.solidityKeccak(["uint256"], [10]).int()+0)
'0x00000000000000000000000000000000000000000000000000000000deadbeef'
>>> a.get_storage_at(web3.solidityKeccak(["uint256"], [10]).int()+1)
'0x0000000000000000000000000000000000000000000000000000000000c0ffee'

>>> a.get_storage_of_array(10, element=2)
[0xc65a7bb8d6351c1cf70c95a316cc6a92839c986682d98bc35f958f4883f9d2a8] len=2
  [00] 0x00000000000000000000000000000000000000000000000000000000deadbeef
  [01] 0x0000000000000000000000000000000000000000000000000000000000c0ffee

Mapping Composite Type to Slots

Example: Composite Type

struct S {
    uint128 a;             // 1. what's size?
    uint128 b;             // 2. what's size?
    uint[2] staticArray;   // 3. what's size?
    uint[] dynArray;       // 4. so in total?
}
S public s;         // -> storage @slot-?
address public addr;// -> storage @slot-?
>>> dump_layout(src)
[03]@000-127 s                    : I/128B struct A.S
[07]@000-019 addr                 : I/020B address
...
struct A.S:
[00]@000-015 a                    : I/016B uint128
[00]@016-031 b                    : I/016B uint128
[01]@000-063 staticArray          : I/064B uint256[2]
[03]@000-031 dynArray             : D/032B uint256[]

Mapping Mapping to Slots

Mapping Mapping to Slots

Mapping Nested Mapping to Slots

Potential Problems? Hash collision!

  1. Function selector might (4-byte), yet a compiler attempts to resolve!
  2. Mapping/dynamic array depend on key, which likely controllable by an attack!

Today’s Tutorial

  1. Function selector might (4-byte), yet a compiler attempts to resolve!
    • → Seeking a hash collision on function selectors
  2. Mapping/dynamic array depend on key, which likely controllable by an attack!
    • → Navigating through the mapping structure

Lab04!

References