1. 智能合约安全基础:Solidity语言特性、常见漏洞模式、EVM执行模型

各位同学好,我是你们这趟安全之旅的向导。说实话,每次带新人入门智能合约安全,我都要先问一句:你真正理解EVM是怎么跑起来的吗?

很多人一上来就学漏洞,结果连msg.sendertx.origin的区别都搞混。嗯,这不行。今天我们就从最底层开始,把地基打牢。

1.1 Solidity语言特性:那些让你又爱又恨的设计

Solidity这门语言,说白了就是以太坊上的“官方方言”。它借鉴了JavaScript和C++的语法,但又有自己独特的脾性。我个人习惯把它的核心特性归纳为三点:

  • 面向合约:每个合约就是一个类,部署后就是一个地址。你调用它,就像调用一个远程对象。
  • payable 与 fallback:函数可以接收ETH,合约可以没有名字地接收转账。这设计很灵活,但也埋下了重入的雷。
  • 修饰符(modifier):用来做权限检查、重入锁等。我建议你把它当成“函数的前置过滤器”来理解。

避坑指南:我曾经在审计一个DeFi项目时,发现开发者用tx.origin做身份验证。结果呢?攻击者构造一个中间合约,让受害者调用,tx.origin始终是受害者地址,权限检查形同虚设。记住:永远用msg.sender做身份判断。

1.2 EVM执行模型:合约到底是怎么跑的?

EVM(以太坊虚拟机)是一个栈式虚拟机。你想想看,它就像一个只有栈、内存和存储的计算器。每次执行一条指令,就从栈顶取数据,算完再压回去。

这里有个关键概念:Gas。每一条指令都要消耗Gas,你的交易Gas不够,执行到一半就回滚。但回滚不是“撤销一切”,而是状态回退到交易开始前。嗯,这里要注意:已经消耗的Gas不会退还

我画了一张图,帮你理解EVM的执行流程:

EVM 执行模型流程图 交易发起(Tx) Gas 检查与预扣 逐条执行指令(OPCODE) 栈操作 · 内存读写 · 存储读写 每步扣除 Gas Gas 够? 继续下一条 或执行完毕 状态回滚 Gas 不退还 交易成功,状态更新 交易失败,状态不变

你看,EVM的执行就是这样一个“要么全做,要么全不做”的过程。但这里有个陷阱:如果合约内部调用另一个合约,子调用失败,父调用可以选择捕获异常继续执行。这就是重入攻击能得逞的底层原因之一。

1.3 常见漏洞模式:三个最经典的坑

1.3.1 重入攻击

重入攻击,说白了就是“你还没更新余额,对方又提了一次款”。我记得2016年The DAO事件就是栽在这上面,300多万ETH被盗。

核心问题出在:先转账,后更新状态

// 有漏洞的代码
function withdraw(uint _amount) public {
    require(balances[msg.sender] >= _amount);
    (bool success, ) = msg.sender.call{value: _amount}("");  // 先转账
    require(success);
    balances[msg.sender] -= _amount;  // 后更新
}

攻击者合约在receive()函数里再次调用withdraw(),因为余额还没扣,就能反复提款。我建议的修复方案是:先更新状态,再转账,或者用OpenZeppelin的ReentrancyGuard

警告:不要以为用了transfer()就安全。虽然transfer()只发2300 Gas,但攻击者可以用selfdestruct强制发送ETH,绕过Gas限制。我见过不止一个项目因此被黑。

1.3.2 整数溢出

Solidity 0.8.0之前,整数溢出不会自动回滚。你想想看,一个uint8最大是255,加1就变成0。这在代币转账、奖励计算中简直是灾难。

举个例子:

// 老版本Solidity,没有SafeMath
uint8 public totalSupply = 255;
function mint() public {
    totalSupply += 1;  // 变成0!代币凭空消失或无限增发
}

我个人习惯在0.8.0以下的项目里,一律用OpenZeppelin的SafeMath库。0.8.0以上虽然内置了检查,但unchecked块里还是要小心。

1.3.3 访问控制

访问控制漏洞,说白了就是“不该干活的人干了活”。最常见的是:

  • 函数没有权限修饰符:比如onlyOwner没加,任何人都能调用销毁函数。
  • 初始化函数未保护:合约的initialize()函数没加initializer修饰符,攻击者可以重新初始化,把自己设为owner。
  • 错误的地址比较:用tx.origin代替msg.sender

我的经验:审计时我第一件事就是看所有publicexternal函数,检查它们有没有权限控制。曾经有个项目,把withdrawAll()写成了public,结果任何人都能提取合约里的ETH。嗯,这种低级错误其实比高级漏洞更常见。

1.4 知识体系总览

最后,我用一张图把今天的内容串起来。你对照着看,心里就有谱了:

智能合约安全基础 Solidity 语言特性 面向合约 · payable · modifier EVM 执行模型 栈 · 内存 · 存储 · Gas 常见漏洞模式 重入 · 溢出 · 访问控制 msg.sender fallback / receive modifier 权限 栈操作 存储布局 Gas 计量 重入攻击 整数溢出 访问控制 核心原则 先检查-后生效-再交互(Checks-Effects-Interactions) 最小权限原则 · 不要信任外部调用

好了,这一章的内容就到这里。记住:安全不是靠背漏洞列表,而是理解底层原理。你把这些基础吃透了,后面学具体攻击手法就会轻松很多。


专注资料整理