KienDT

Talk is cheap. Show me the code.

Vài dòng tóm lược vụ hack lịch sử 600 triệu đô của Poly Network

Vụ hack 600 triệu đô của Poly Network ngày 11/8 đã làm rúng động toàn bộ thị trường Crypto.

https://twitter.com/PolyNetwork2/status/1425073987164381196

Vụ hack xảy ra như thế nào?

Poly Network có một contract tên là EthCrossChainManager. Đây chính à contract có quyền trigger message từ chain khác, trong kiến trúc của các dự án cross-chain, có thể coi đây chính là người điểu khiển của hệ thống.

Có một hàm tên là verifyHeaderAndExecuteTx nơi mọi người gọi đến để thực hiện cross-chain transaction. Về cơ bản nó có 2 nhiệm vụ:

  • (1) verify lại blockchain header bằng cách kiểm tra chữ ký có hợp lệ hay không
  • (2) check xem transaction đã được đưa vào trong block hay chưa nhờ Merkle Proof.
function verifyHeaderAndExecuteTx(
    bytes memory proof,
    bytes memory rawHeader,
    bytes memory headerProof,
    bytes memory curRawHeader,
    bytes memory headerSig
    ) whenNotPaused public returns (bool)

Cuối cùng nó gọi hàm _executeCrossChainTx, trên target contract. Đây chính là lỗ hổng mấu chốt của vụ tấn công.

function _executeCrossChainTx(
    address _toContract,
    bytes memory _method,
    bytes memory _args,
    bytes memory _fromContractAddr,
    uint64 _fromChainId
    ) internal returns (bool)

Tất nhiên để tránh lỗi, Poly Network có kiểm tra xem target có phải contract hay không bằng hàm này:

require(Utils.isContract(_toContract), "The passed in address is not a contract!");

Nhưng họ quên phải loại trừ viêc target là một contract vô cùng quan trọng - chính là contract EthCrossChainData.

Tại sao điều này quan trọng đến vậy? EthCrossChainData lưu giữ public key của account giữ tiền - gọi là Keeper. Nếu public key này bị thay đổi, thì đồng nghĩa với việc giao tiền cho người khác giữ. Mục tiêu của hacker chính là bằng cách nào đó thay đổi public key này thành public key của account của mình, chuyển mình thành Keeper.

bytes public ConKeepersPkBytes;

Cách duy nhất có thể thay đổi public key của Keeper chính là bởi owner của contract EthCrossChainData, và không ai khác, đó chính là người điều khiển, contract EthCrossChainManager

function putCurEpochConPubKeyBytes(
    bytes memory curEpochPkBytes
    ) public whenNotPaused onlyOwner returns (bool) {
    ConKeepersPkBytes = curEpochPkBytes;
    return true;
}

Như vậy bằng cách nào đó, ta có thể từ contract EthCrossChainManager gọi đến EthCrossChainData contract, nghiễm nhiên ta đã pass qua được bước kiểm onlyOwner check. Giờ chỉ còn lại việc gọi làm sao cho đúng hàm putCurEpochConPubKeyBytes thôi là có thể thay đổi được public key rồi.

Quay trở lại bước 3 bên trên, ta có thể thực hiện hàm _executeCrossChainTx với target bất kì, rỏ ràng đây chính là gợi ý cho ta để hiện thực hóa việc gọi hàm putCurEpochConPubKeyBytes từ contract EthCrossChainData.

Có một điểm khó ở đây là, contract EthCrossChainManager chỉ chấp nhận method có dạng _method(bytes, bytes, uint64) trong đó _method là tên hàm tự định nghĩa

(success, returnData) = _toContract.call(abi.encodePacked(bytes4(keccak256(abi.encodePacked(_method, "(bytes,bytes,uint64)"))), abi.encode(_args, _fromContractAddr, _fromChainId)));

hàm này khác với hàm putCurEpochConPubKeyBytes(bytes) bên trên. Làm sao ta có thể gọi được?

Ta biết rằng trong solidity, mỗi khi contract gọi hàm, thì thực chất là nó gọi đến signature hash, tức 4 bytes đầu tiên của giá trị hash của tên hàm kèm tham số <function name>(<function input types>). Ví dụ sighash của hàm transfer trong ERC20 function chính là 4 bytes đầu tiên của giá trị hash của transfer(address,uint256).

Hàm _executeCrossChainTx chỉ nhận method có dạng _method(bytes, bytes, uint64), nhưng điểm chết người ở đây chính là _method là tên phương thức tự định nghĩa; vậy nên kẻ tấn công hoàn toàn có thể brute force hàng loạt các method để có thể tạo ra được giá trị hash trùng 4 bytes đầu với giá trị hash của putCurEpochConPubKeyBytes(bytes). Điều này là hoàn toàn khả thi vì sighash chỉ có 4 bytes mà thôi.

(success, returnData) = _toContract.call(abi.encodePacked(bytes4(keccak256(abi.encodePacked(_method, "(bytes,bytes,uint64)"))), abi.encode(_args, _fromContractAddr, _fromChainId)));

Và kẻ tấn công đã tìm ra được một method phù hợp

  • method gốc
>http://ethers.utils.id('putCurEpochConPubKeyBytes(bytes)').slice(0, 10)
'0x41973cd9'
  • method của hacker
> http://ethers.utils.id('f1121318093(bytes,bytes,uint64)').slice(0, 10)
'0x41973cd9'

Tuyệt vời! Không cần phải lộ key! Chỉ đơn giản là tìm ra một hash collision để tấn công vào contract chứa hàng trăm triệu đô.

Lời bàn

Vụ hack khá dễ hiểu, nhưng phải nói để nghĩ ra được kịch bản hash collision này thì hacker quá đỗi thông minh.

Respect!