原理

关于solidity内存布局文章: Solidity内存布局
任意地址写入是指合约中包含用户可控的对任意Storage地址写入数据的漏洞。在以太坊中,智能合约的状态变量会存储在storage区域中,Storage是重要且开放的合约存储空问,对于特定Storage地址的数据读写权限完全由合约开发者自行控制。合约中的重要Storage变量的修改需要设置严格的访问限制,才能保证合约的安全性。Storage的存储模型可以视为一个Key-Value的映射,如果用户可以控制Storage写入时的Key,则可以对任意的
Storage变量进行修改,攻击者就可以通过修改storage存储区域的状态变量值,来规避合约中所有借助于该状态变量值进行权限校验的检测,从而达到权限提升的目的。另外,由于攻击者可以利用此漏洞破坏合约存储结构,进行任意变量覆盖操作,如覆盖存储合约所有者地址的状态变量的值,这就有可能造成合约功能执行异常、资金冻结等危害。

示例

  1. pragma solidity ^0.4.25;
  2. contract Wallet {
  3. uint[] private bonusCodes;
  4. address private owner;
  5. constructor() public {
  6. bonusCodes = new uint[](0);
  7. owner = msg.sender;
  8. }
  9. function () public payable {
  10. }
  11. function PushBonusCode(uint c) public {
  12. bonusCodes.push(c);
  13. }
  14. function PopBonusCode() public {
  15. require(0 <= bonusCodes.length);
  16. bonusCodes.length--;
  17. }
  18. function UpdateBonusCodeAt(uint idx, uint c) public {
  19. require(idx < bonusCodes.length);
  20. bonusCodes[idx] = c;
  21. }
  22. function Destroy() public {
  23. require(msg.sender == owner);
  24. selfdestruct(msg.sender);
  25. }
  26. }
  • 该合约漏洞没有利用任何编译器或EVM的bug。这个bug在popBonusCode()的下面一行:require(0 <= bonusCodes.length);此条件始终为真,因为数组长度是无符号的。代码试图检查数组是否为空,因此它应该使用>而不是>=。这是一个常见的“差一”错误,可能会被视为不重要的错误而不予考虑。此外,bug处于与资金完全无关的函数部分。不管有没有bug,似乎任何代码方法都不会影响资金机制。这表明了一个问题: 动态数组处理中的错误可能会以不明显的方式导致合约的脆弱性。
  • 合约的Storage是由256位指针寻址的。合约的所有存储变量都存储在这个内存空间的不同偏移量中。布局算法试图确保这些存储位置不会重叠或碰撞。每个变量在存储中占用一个32字节的“槽”(slot),该“槽”是按照变量声明的顺序分配的,从地址0x0开始。第一个32字节的变量位于地址0x0,第二个位于地址0x1,以此类推。由于mapping和动态数组的大小波动,它们的内容不存储在该槽中。为了解决这个问题,我们使用了哈希算法。

11.png

  • 对于动态数组,保留的插槽包含作为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为合约部署者。
12.png

  • 调用PopBonusCode()函数后,数组长度溢出为2^256-1

13.png
此时,计算数组索引值为2^256-lceooak256(0x0)时,动态数组写入的位置将溢出而重新指向槽位1。使用Perl计算2^256-keccak256(0x0)+1的结果为:0xd6f21326ab749…6ce9f10c1a9e,如图所示。
15.png
传入参数,2^256-lceooak256(0x0)与任意一个地址,可以看到原owner的value被覆盖。
14.png

参考

[1]https://github.com/Arachnid/uscc/blob/master/submissions-2017/doughoyte/README.md
[2]杨坤. 基于符号执行的智能合约自动化安全审计[D].电子科技大学,2020.