合约安全:delegatecall使用时的危险

一、漏洞一

contract Lib {
    address public owner;

    function pwn() public {
        owner = msg.sender;
    }
}

contract HackMe {
    address public owner;
    Lib public lib;

    constructor(Lib _lib) {
        owner = msg.sender;
        lib = Lib(_lib);
    }

    fallback() external payable {
        address(lib).delegatecall(msg.data);
    }
}

contract Attack {
    address public hackMe;

    constructor(address _hackMe) {
        hackMe = _hackMe;
    }

    function attack() public {
        hackMe.call(abi.encodeWithSignature("pwn()"));
    }
}

我们创建了一个库合约Lib,这里面的owner变量是一种形式变量,并不参与实际的运算,仅仅用来占用storage内存的slot位置,只有当内存位置和使用库合约的合约位置相同,才能用于delegatecall。

库合约的功能很简单,就是给owner设置为msg.sender。应用合约HackMe,在fallback中提供了delegatecall的用法,可以对传入的msg.data进行调用。

在攻击合约Attack中,我们给pwn()函数签名作为msg.data传了过去,HackMe合约拿到后,经过了fallback,然后又用库合约执行pwn(),执行完,HackMe合约上的owner值就变成了Attack的合约地址了。

试想一下,如果这个owner值是开放关键权限的一个变量的话,如果被人随意修改,最后就会导致资产的流失。

二、漏洞二

delegatecall还有一个危险的地方,就是如果开发者没有真正理解delegatecall和内存布局的关系的话,很容易发生一些危险的行为。一旦库合约的变量顺序和应用合约的变量顺序不统一时,内存布局就是不统一的:

contract Lib {
    address public owner;
    ...
}

contract HackMe {
    Lib public lib;
    address public owner;
    ...
}

比如上面的合约,如果我们HackMe的合约中lib和owner的顺序颠倒了,此时slot0的变量就是lib,而库合约Lib的slot0的位置还是owner。此时如果进行delegatecall调用lib中的函数的话,那么最后HackMe中被修改的其实不是owner,而是lib。

试想一下,如果是一些其他涉及资产逻辑的变量可以被修改的话,那么整个合约最后就会被hack得面目全非。

三、预防手段

使用library关键字创立库函数。这确保了库合约是无状态(Stateless)且不可自毁的。强制让 library 成为无状态的,可以缓解本节所述的存储环境的复杂性。无状态库也可以防止攻击者直接修改库状态的攻击,以实现依赖库代码的合约。作为一般的经验法则,在使用时delegatecall时要特别注意库合约和调用合约的可能调用上下文,并且尽可能构建无状态库。

在我们之前的文章《Solidity中library的机制和内幕》中,有这么一段话:
当library因为有public而单独部署时,相比proxy pattern,都是利用另一个合约承载逻辑,但方式不同,一个是利用存储布局,一个是直接传递storage的引用,上下文变量都保持在调用者一边。调用者以delegatecall调用,由于library没有成员,被调用者只操作传入的参数,因此delegatecall不是像proxy pattern中的那样通过兼容存储布局利用另一合约逻辑的作用,而是通过操作storage属性的参数利用另一合约的逻辑。

四、真实世界示例:Parity Multisig Wallet(Second Hack)

Parity 多签名钱包第二次被黑事件是一个例子,说明了如果在非预期的环境中运行,良好的库代码也可以被利用。

我们来看看这个合约的相关方面。这里有两个包含利益的合约,库合约和钱包合约。

先看 library 合约,

contract WalletLibrary is WalletEvents {
  
  ...
  
  // throw unless the contract is not yet initialized.
  modifier only_uninitialized { if (m_numOwners > 0) throw; _; }

  // constructor - just pass on the owner array to the multiowned and
  // the limit to daylimit
  function initWallet(address[] _owners, uint _required, uint _daylimit) only_uninitialized {
    initDaylimit(_daylimit);
    initMultiowned(_owners, _required);
  }

  // kills the contract sending everything to  ` _to ` .
  function kill(address _to) onlymanyowners(sha3(msg.data)) external {
    suicide(_to);
  }
  
  ...
  
}

再看钱包合约,

contract Wallet is WalletEvents {

  ...

  // METHODS

  // gets called when no other function matches
  function() payable {
    // just being sent some cash?
    if (msg.value > 0)
      Deposit(msg.sender, msg.value);
    else if (msg.data.length > 0)
      _walletLibrary.delegatecall(msg.data);
  }
  
  ...  

  // FIELDS
  address constant _walletLibrary = 0xcafecafecafecafecafecafecafecafecafecafe;
}

请注意,Wallet 合约基本上会通过 delegate call 将所有调用传递给 WalletLibrary。此代码段中的常量地址 _walletLibrary,即是实际部署的 WalletLibrary 合约的占位符(位于 0x863DF6BFa4469f3ead0bE8f9F2AAE51c91A907b4 )。

这些合约的预期运作是生成一个简单的可低成本部署的 Wallet 合约,合约的代码基础和主要功能都在 WalletLibrary 合约中。不幸的是,WalletLibrary 合约本身就是一个合约,并保持它自己的状态。你能能不能看出为什么这会是一个问题?

因为有可能向 WalletLibrary 合约本身发送调用请求。具体来说,WalletLibrary 合约可以初始化,并被用户拥有。一个用户通过调用 WalletLibrary 中的 initWallet() 函数,成为了 Library 合约的所有者。同一个用户,随后调用 kill() 功能。因为用户是 Library 合约的所有者,所以修改传入、Library 合约自毁。因为所有现存的 Wallet 合约都引用该 Library 合约,并且不包含更改引用的方法,因此其所有功能(包括取回 Ether 的功能)都会随 WalletLibrary 合约一起丢失。更直接地说,这种类型的 Parity 多签名钱包中的所有以太都会立即丢失或者说永久不可恢复。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容