原理
关于solidity内存布局文章: Solidity内存布局
任意地址写入是指合约中包含用户可控的对任意Storage地址写入数据的漏洞。在以太坊中,智能合约的状态变量会存储在storage区域中,Storage是重要且开放的合约存储空问,对于特定Storage地址的数据读写权限完全由合约开发者自行控制。合约中的重要Storage变量的修改需要设置严格的访问限制,才能保证合约的安全性。Storage的存储模型可以视为一个Key-Value的映射,如果用户可以控制Storage写入时的Key,则可以对任意的
Storage变量进行修改,攻击者就可以通过修改storage存储区域的状态变量值,来规避合约中所有借助于该状态变量值进行权限校验的检测,从而达到权限提升的目的。另外,由于攻击者可以利用此漏洞破坏合约存储结构,进行任意变量覆盖操作,如覆盖存储合约所有者地址的状态变量的值,这就有可能造成合约功能执行异常、资金冻结等危害。
示例
pragma solidity ^0.4.25;
contract Wallet {
uint[] private bonusCodes;
address private owner;
constructor() public {
bonusCodes = new uint[](0);
owner = msg.sender;
}
function () public payable {
}
function PushBonusCode(uint c) public {
bonusCodes.push(c);
}
function PopBonusCode() public {
require(0 <= bonusCodes.length);
bonusCodes.length--;
}
function UpdateBonusCodeAt(uint idx, uint c) public {
require(idx < bonusCodes.length);
bonusCodes[idx] = c;
}
function Destroy() public {
require(msg.sender == owner);
selfdestruct(msg.sender);
}
}
- 该合约漏洞没有利用任何编译器或EVM的bug。这个bug在popBonusCode()的下面一行:
require(0 <= bonusCodes.length);
此条件始终为真,因为数组长度是无符号的。代码试图检查数组是否为空,因此它应该使用>而不是>=。这是一个常见的“差一”错误,可能会被视为不重要的错误而不予考虑。此外,bug处于与资金完全无关的函数部分。不管有没有bug,似乎任何代码方法都不会影响资金机制。这表明了一个问题: 动态数组处理中的错误可能会以不明显的方式导致合约的脆弱性。 - 合约的Storage是由256位指针寻址的。合约的所有存储变量都存储在这个内存空间的不同偏移量中。布局算法试图确保这些存储位置不会重叠或碰撞。每个变量在存储中占用一个32字节的“槽”(slot),该“槽”是按照变量声明的顺序分配的,从地址0x0开始。第一个32字节的变量位于地址0x0,第二个位于地址0x1,以此类推。由于mapping和动态数组的大小波动,它们的内容不存储在该槽中。为了解决这个问题,我们使用了哈希算法。
- 对于动态数组,保留的插槽包含作为uint256的数组的长度,并且数组数据本身顺序地位于地址keccak256(p),p为该动态数组原本属于的slot,该slot储存数组长度。动态数组从它的散列偏移量开始按顺序存储。如果该数组的索引在攻击者的控制下,那么存储地址也在数组的范围内受到控制。当然,与keccak256范围相比,实际的数组大小是微不足道的
- 对于该合约。由于require保护无效,当bonusCodes数组的长度为0时,攻击者可以尝试通过执行以下代码来溢出数组大小:
bonusCodes.length--
length属性被视为一个普通的变量,因为这个操作会使数组长度达到最大uint值(2^256 - 1)。因此,数组边界检查可以被有效的绕过。由于攻击者可以调用UpdateBonusCodeAt
改变数组中的任何元素,几乎包含了所有的存储地址空间,任意写入存储的任何位置是可能的。这有点类似于C语言指针操作错误。
编译部署合约,合约部署者: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
, 查看storage,可以看到bonusCodes位于slot0,value为0。owner位于slot1, value为合约部署者。
- 调用PopBonusCode()函数后,数组长度溢出为2^256-1
此时,计算数组索引值为2^256-lceooak256(0x0)时,动态数组写入的位置将溢出而重新指向槽位1。使用Perl计算2^256-keccak256(0x0)+1的结果为:0xd6f21326ab749…6ce9f10c1a9e,如图所示。
传入参数,2^256-lceooak256(0x0)与任意一个地址,可以看到原owner的value被覆盖。
参考
[1]https://github.com/Arachnid/uscc/blob/master/submissions-2017/doughoyte/README.md
[2]杨坤. 基于符号执行的智能合约自动化安全审计[D].电子科技大学,2020.