timezone |
---|
Asia/Shanghai |
-
自我介绍
Hi,I'm ha2ryzhang. 正在学习合约安全相关的内容.
-
你认为你会完成本次残酷学习吗?
尽力而为之.
接着之前一直做了一半的 Ethernaut
继续学习
transfer
transferFrom
他这里只对transfer
进行了重写 所以transferFrom
是可以绕过lockTokens
的检查的. 按顺序做的,这题还是相对简单点.
看到题目是需要获取合约的owner权限,但是又没看到set owner的方法.
看到了delegatecall
,想到了slot
相关的,很久之前看过了,有点忘记了,先查一下.
delegatecall 是需要顺序和数据类型保持一致的.
address
:地址类型存储一个 20 字节的值(以太坊地址的大小)。 地址类型也有成员变量,并作为所有合约的基础。
-----------------------------------------------------
| unused (12) | timeZone1Library (20) | <- slot 0
-----------------------------------------------------
| unused (12) | timeZone2Library (20) | <- slot 1
-----------------------------------------------------
| unused (12) | owner (20) | <- slot 2
-----------------------------------------------------
| storedTime (32) | <- slot 3
-----------------------------------------------------
| unused (28) | setTimeSignature (4) | <- slot 4
-----------------------------------------------------
上面是Preservation.sol
合约中变量的Storage Slot
问题就在于LibraryContract
合约中slot位置对不上
-----------------------------------------------------
| storedTime (32) | <- slot 0
-----------------------------------------------------
这么一对比可以很直观看到.理论上无论调用setFirstTime
或者setSecondTime
都会是修改
变量timeZone1Library
的值.那么只需要一个攻击合约对应上owner
所在的slot就能修改owner了
contract Attack{
address public tmp1;
address public tmp2;
address public owner;
function setTime(uint256 _time) public {
//类型转换 time 为 address
owner = address(uint160(_time));
}
}
合约中owner对上即可,测试通过! 还有一个类型转换的点:
- uint256 到 uint160: address 类型在 Solidity 中是一个 20 字节(160 位)的值,而 uint256 是一个 32 字节(256 位)的值。 因此,需要先将 uint256 转换为 uint160,因为 uint160 也正好是 20 字节,可以容纳 address 类型的数据。
- uint160 到 address: 将 uint160 转换为 address 类型非常简单,只需要将其强制转换即可。
看了眼题目,一开始没明白什么意思,为什么会找不到地址.etherscan里是可以看到地址的,哈哈.
直接调用destroy()
就可以拿回资金了.
去看了一眼Factory合约,答案直接都看到了,知识点是地址生成的规则,去年看过,这么久忘了,正好复习复习.
- create
create的用法很简单,就是new一个合约,并传入新合约构造函数所需的参数:
Contract x = new Contract{value: _value}(params)
CREATE如何计算地址
新地址 = hash(创建者地址, nonce)
创建者地址不会变,但nonce可能会随时间而改变,因此用CREATE创建的合约地址不好预测。
- create2
详解
和create差不多,多了一个salt
参数
CREATE2如何计算地址
新地址 = hash("0xFF",创建者地址, salt, initcode)
CREATE2 确保,如果创建者使用 CREATE2 和提供的 salt 部署给定的合约initcode,它将存储在 新地址 中。
回到题目 Recovery 使用的是create来创建的Token合约,所以模拟对应参数就行
address token=address( uint160(uint256(keccak256(abi.encodePacked(uint8(0xd6), uint8(0x94), levelAddress, uint8(0x01))))) );
这里levelAddresss是已知的,0x01
可以理解为nonce,如果再调用一次generateToken
那么就应该是0x02
.
这题第一次看的时候很懵,考察的点是opcode
和assembly
的使用.
知识点很多,目前还没看完,过几天补一补.
这一题也是storage slot
相关的.
因为低版本的solidity是没有溢出检查的
pragma solidity ^0.4.0;
contract C {
uint256 a; // 0
uint[] b; // 1
uint256 c; // 2
}
-----------------------------------------------------
| a (32) | <- slot 0
-----------------------------------------------------
| b.length (32) | <- slot 1
-----------------------------------------------------
| c (32) | <- slot 2
-----------------------------------------------------
| ... | ......
-----------------------------------------------------
| b[0] (32) | <- slot `keccak256(1)`
-----------------------------------------------------
| b[1] (32) | <- slot `keccak256(1) + 1`
-----------------------------------------------------
| ... | ......
-----------------------------------------------------
就是需要codex[] 越界访问 owner
(2**256 - 1) + 1 = 0
这题直接死循环消耗完gas即可
攻击合约定义一个price方法 通过isSold
判断价格,返回不同的价格
这题是第一个碰到的defi相关的题目.题目需要把dex的余额变为0.
问题出现在getSwapPrice
方法,这个算法有问题,如果多swap几次,余额只会慢慢变多.
这一题对比上一题缺少了require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
可以利用其他代币来换出token.
今天这道题卡了几个小时,最开始发现proxy的storage slot
对不上,就开始研究.
实际上delegatecall
proxy合约的proposeNewAdmin
其实就是对应 Wallet合约的owner
(同slot 0),
获取了owner
权限,就可以添加whiteliste,就可以调用wallet合约的方法,观察 如果想改变proxy合约的owner 就得改变wallet合约的maxBalance
(同样对应slot 1),可是maxBalance如果想要修改的话,又必须得保证address(wallet).balance == 0
,所以得想办法让合约的余额等于0.
分析了调用execute
方法,发现只能发送自己的余额,而wallet的初始余额是factory
合约的,
所以有陷入了死胡同,multicall
是唯一可以利用的地方,但是这个方法又有一个很巧妙设计的地方,为了mag.value
不被重复利用,做了selector
的判断,换个角度calldata的第二个交易如果还是multicall
就能完美绕过.
就可以存双份钱,绕过require(balances[msg.sender] >= value, "Insufficient balance");
,
然后就可以修改maxBalance
为player
地址,就结束.
是个uups的合约,实际上逻辑合约是没有init的,因为proxy合约delegatecall
了initialize
.
验证发现逻辑合约的upgrader
和horsePower
确实都是0,没有初始化的.
既然没有初始化,意味着可以调用initialize
让自己变成upgrader
,然后升级合约.
这题是要让合约selfdestruct
,写个攻击合约,带个selfdestruct
方法,然后调用upgradeToAndCall
即可.
貌似 foundry test中 selfdestruct
无效,所以 poc 中后面手动处理了
这题分析业务看了有点久,不太明白想要干啥.
CryptoVault
中的underlying
是不应该被transfer的,
require(token != underlying, "Can't transfer underlying token");
做了判断,可是sweptTokensRecipient
的token
是LegacyToken
(LGT), LegacyToken
的transfer又是代理的DoubleEntryPoint
(DET)
function transfer(address to, uint256 value) public override returns (bool) {
if (address(delegate) == address(0)) {
return super.transfer(to, value);
} else {
return delegate.delegateTransfer(to, value, msg.sender);
}
}
delegate
调用的其实就是DET,
这里的delegateTransfer
的origSender
是CryptoVault
,
所以 CryptoVault
中的DET
最后会被转移为0
DET
中有个 modifier fortaNotify()
modifier fortaNotify() {
address detectionBot = address(forta.usersDetectionBots(player));
// Cache old number of bot alerts
uint256 previousValue = forta.botRaisedAlerts(detectionBot);
// Notify Forta
forta.notify(player, msg.data);
// Continue execution
_;
// Check if alarms have been raised
if (forta.botRaisedAlerts(detectionBot) > previousValue) revert("Alert has been triggered, reverting");
}
Foeta
允许用户设置机器人来提醒交易,需要自己定义个合约,如果交易有问题调用raiseAlert
这个modifier
就会revert
.
最后实现Bot的handleTransaction
方法判断originSender == cryptoVault
来触发raiseAlert
.
这题requestDonation
里catch到错误了,会判断是否是NotEnoughBalance()
,是的话就转走剩余的coin,所以实现notify
来判断amount==10
(转走剩余的coin肯定不等于10),然后revert同样的错误就行
gateOne()
:attack合约调用construct0r()
即可,这里方法写错了,并不是构造器.gateTwo()
:调用getAllowance(uint256 _password)
来校验trick的密码,因为password
是private
的,可以通过storage slot
获取,poc里是带创建关卡一起的,所以直接用了block.timestamp
gateThree()
:给合约转账大于0.001e,并且attack合约在接受e的时候revert
就好
这题需要调用turnSwitchOn()
让switchOn
=true
,因为turnSwitchOn()
有onlyThis
修饰,所以只能通过flipSwitch(bytes memory _data)
来调用.
modifier onlyOff()
中判断了selector
// we use a complex data type to put in memory
bytes32[1] memory selector;
// check that the calldata at position 68 (location of _data)
assembly {
calldatacopy(selector, 68, 4) // grab function selector from calldata
}
这里68是calldata
中对应调用函数的selector
的offset,然后4字节大小.
modifier中判断了selector[0] == offSelector
要求必须是调用的turnSwitchOff()
,这就陷入了僵局.
看一下正常调用
switch.flipSwitch(abi.encodeWithSelector(Switch.turnSwitchOff.selector));
//calldata
// 0x
// 30c13ade flipSwitch函数签名
// 0000000000000000000000000000000000000000000000000000000000000020 _data offset 0x20=32
// 0000000000000000000000000000000000000000000000000000000000000004 _data length
// 20606e1500000000000000000000000000000000000000000000000000000000 turnSwitchOff
所以calldatacopy(selector, 68, 4)
中68对应的turnSwitchOff
通常情况下没问题,但是可以手动修改offset来绕过检查.
这里具体的calldata的细节见文档,
动态类型的 calldata 编码,前 32 字节用于存储偏移量(offset), 接下来的 32 字节用于存储长度(length),然后是用于存储值的区域.
// 0x
//4 bytes: 30c13ade
//32 bytes: 0000000000000000000000000000000000000000000000000000000000000060 offset 0x60=96
//32 bytes: 0000000000000000000000000000000000000000000000000000000000000004 length
//32 bytes: 20606e1500000000000000000000000000000000000000000000000000000000 turnSwitchOff
// 0x60:
//32 bytes: 0000000000000000000000000000000000000000000000000000000000000004 length
//32 bytes: 76227e1200000000000000000000000000000000000000000000000000000000 turnSwitchOn
在原有基础上增加 turnSwitchOn
的calldata 修改offset来正确调用turnSwitchOn
同时也满足modifier
的检查.
这题用的solc
还是0.6.x
的,abicoder还没有类型检查.0.8.0
以上不会有这个问题(默认abicoder
V2).
abi.encodeWithSignature("registerTreasury(uint8)", 256);
正常调用满足要求就行
0xdd62ed3e
对应 erc20的allowance(address owner, address spender)
要过bytesToUint(allowance) >= amount
的检查,先授权一遍就行.题目没有校验call
的返回值,所以没有weth也是可以增加totalStaked
的.
需要另一个账户stakeETH一次来满足totalStaked must be greater than the Stake contract's ETH balance.
到这里 ethernaut
完结撒花,学到了很多.
还是选择了A系列的DamnVaulnerableDefi,想做一些defi相关.
大概了解了什么是ERC-4626
卡住了(搞了好久依赖,和之前的ethernaut的冲突)
题目要求闪电贷失败.
if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance();
把player的token转给vault就好
又卡住了...
pool合约中的msgSender
function _msgSender() internal view override returns (address) {
if (msg.sender == trustedForwarder && msg.data.length >= 20) {
return address(bytes20(msg.data[msg.data.length - 20:]));
} else {
return super._msgSender();
}
}
如果来源是 trustedForwarder
合约的话并且data.length>=20,返回的地址是获取msg.data
最后20个字节来作为地址.
而这个msgSender又是withdraw
方法需要的
这里需要去了解EIP712,看看具体怎么个调用法
这道题,参考的sun哥的.实在是卡在这了,今天累了,明天再整理下,不会组织语言了.
问题出在target.functionCall(data);
可以调用token的approve来授权,再调用transferFrom
来转走资金.
题目有一笔tx的限制,写个attack合约在constructor
里调用