智能合约安全审计入门篇 —— Contract Size Check

By:小白


背景概述


第一期文章中我们提过防止重入攻击的多种方式,其中一种是通过检查调用者身份并禁止合约调用来防止重入攻击。这期文章我们就来了解如何通过 Contract Size Check 判断调用者身份。


前置知识


众所周知,合约是由代码编写的,所以部署后的合约地址肯定有相应的字节码,而 EOA 地址是没有字节码的,所以我们只需要检查调用者地址是否有字节码就可以判断调用者是否为合约。这里需要使用 Solidity 内联汇编中的 extcodesize() 函数。下面我们还是通过实例来看看这个函数的实际效果。


代码示例


首先,我们用一个代码示例来了解 extcodesize() 的作用:


// SPDX-License-Identifier: MITpragma solidity ^0.8.20;
contract CheckCodeSize { function Size(address account) public view returns (uint256) { uint size; assembly { size := extcodesize(account) } return size; }}

我们在 Remix 上部署后,调用 Size() 并传入 Deployer 的 EOA 地址(0x787···cabaB),发现返回 0。


智能合约安全审计入门篇 —— Contract Size Check


我们再次调用 Size() 并传入合约自身地址(0xC58···905F2),返回 348。


智能合约安全审计入门篇 —— Contract Size Check


综上可知,当 account 为 EOA 地址时,extcodesize() 会返回 0 ,当 account 为合约地址时,extcodesize() 会返回合约部署后的字节码大小。到这里,相信大家不难想到可以通过调用 extcodesize() 并看其返回值是否为 0 来判断该地址是 EOA 还是部署后的合约。诶,漏洞就这么写出来了。


漏洞示例


// SPDX-License-Identifier: MITpragma solidity ^0.8.20;
contract Target { function isContract(address account) public view returns (bool) { uint size; assembly { size := extcodesize(account) } return size > 0; }
bool public pwned = false;
function protected() external { require(!isContract(msg.sender), "no contract allowed"); pwned = true; }}

漏洞分析


可以看到,isContract() 判断 account 字节码是否为 0 并返回 bool 类型的值。protected() 则会根据 isContract() 的返回值来修改 pwned 的状态。但是这里忽略了一个点,即合约部署时,constructor 函数中的代码逻辑会跟随部署合约的交易一起发送给矿工打包上链。由于此时合约部署的操作还没有完成,所以合约地址还没有存入相应的字节码,这时在 consturctor 函数中调用 extcodesize() 检查合约地址的字节码就会返回 0 。下面我们来看正常调用的情况下会返回什么:


// SPDX-License-Identifier: MITpragma solidity ^0.8.20;
contract FailedAttack { function pwn(address _target) external { Target(_target).protected(); }}

调用 pwn() 并将合约地址 0x058···4c899 传入会被 revert,这里说明合约部署后调用 protected() 会无法通过 require(!isContract(msg.sender) 检查。


智能合约安全审计入门篇 —— Contract Size Check


攻击合约


下面我们将调用逻辑放在 constructor 函数中再次尝试:


// SPDX-License-Identifier: MITpragma solidity ^0.8.20;
contract Hack { bool public isContract; address public addr;
// When contract is being created, code size (extcodesize) is 0. // This will bypass the isContract() check constructor(address _target) { isContract = Target(_target).isContract(address(this)); addr = address(this); // This will work Target(_target).protected(); }}

部署 Hack 合约时传入 0xbd7···9EFB3 合约地址,发现成功绕过了 require(!isContract(msg.sender) 检查,而且成功修改了 pwned 的状态。


智能合约安全审计入门篇 —— Contract Size Check


修复建议


作为开发者


使用 extcodesize() 判断地址是否为 EOA 地址并不准确,在实际开发中最稳妥的判断调用者身份的方式还是通过 tx.origin 来判断。类似下面这样:


// SPDX-License-Identifier: MITpragma solidity ^0.8.0;
contract EOAChecker { function isEOA() external pure returns (bool) { return msg.sender == tx.origin; }}

由于 tx.origin 只可能是 EOA 地址,我们只需要判断上层调用者的 msg.sender 是否与 tx.origin 为同一地址就可以。以刚刚的漏洞代码为例,我们只需要稍加修改就可以修复漏洞。


// SPDX-License-Identifier: MITpragma solidity ^0.8.20;
contract Target { function isContract() public view returns (bool) { return (msg.sender == tx.origin); }
bool public pwned = false;
function protected() external { require(isContract(), "no contract allowed"); pwned = true; }}

此时部署下面这个 Hack 合约再进行攻击,就会被 revert 并返回 “no contract allowed”。


// SPDX-License-Identifier: MITpragma solidity ^0.8.20;
contract Hack { address public addr;
constructor(address _target) { Target(_target).protected(); }}

智能合约安全审计入门篇 —— Contract Size Check


作为审计者


在审计过程中,如果遇到限制合约地址调用的逻辑,应当结合实际业务逻辑判断该检查是否严谨以及是否存在被绕过的可能。


往期回顾

一周动态 | Web3 安全事件总损失约 4255.4 万美元

慢雾作为内容贡献者参与的《数字资产安全、合规与风险管理》白皮书已正式发布

哈希函数的隐藏危险:长度扩展攻击与服务端验证的安全隐患

一周动态 | Web3 安全事件总损失约 83 万美元

慢雾:被盗急救指南之链上留言

智能合约安全审计入门篇 —— Contract Size Check

慢雾导航


慢雾科技官网

https://www.slowmist.com/


慢雾区官网

https://slowmist.io/


慢雾 GitHub

https://github.com/slowmist


Telegram

https://t.me/slowmistteam


Twitter

https://twitter.com/@slowmist_team


Medium

https://medium.com/@slowmist


知识星球

https://t.zsxq.com/Q3zNvvF

原文始发于微信公众号(慢雾科技):智能合约安全审计入门篇 —— Contract Size Check

版权声明:admin 发表于 2023年9月14日 下午6:07。
转载请注明:智能合约安全审计入门篇 —— Contract Size Check | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...