Skip to content

Latest commit

 

History

History
440 lines (289 loc) · 14.8 KB

Ha2ryzhang.md

File metadata and controls

440 lines (289 loc) · 14.8 KB
timezone
Asia/Shanghai

Ha2ryzhang

  1. 自我介绍

    Hi,I'm ha2ryzhang. 正在学习合约安全相关的内容.

  2. 你认为你会完成本次残酷学习吗?

    尽力而为之.

Notes

2024.08.29

接着之前一直做了一半的 Ethernaut 继续学习

A-Ethernaut-NaughtCoin

link

这题问题主要是 ERC20转账方式

WTF Academy

  1. transfer
  2. transferFrom 他这里只对 transfer进行了重写 所以 transferFrom 是可以绕过lockTokens的检查的. 按顺序做的,这题还是相对简单点.

2024.08.30

A-Ethernaut-Preservation

看到题目是需要获取合约的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对上即可,测试通过! 还有一个类型转换的点:

  1. uint256 到 uint160: address 类型在 Solidity 中是一个 20 字节(160 位)的值,而 uint256 是一个 32 字节(256 位)的值。 因此,需要先将 uint256 转换为 uint160,因为 uint160 也正好是 20 字节,可以容纳 address 类型的数据。
  2. uint160 到 address: 将 uint160 转换为 address 类型非常简单,只需要将其强制转换即可。

2024.08.31

A-Ethernaut-Recovery

看了眼题目,一开始没明白什么意思,为什么会找不到地址.etherscan里是可以看到地址的,哈哈. 直接调用destroy()就可以拿回资金了.

去看了一眼Factory合约,答案直接都看到了,知识点是地址生成的规则,去年看过,这么久忘了,正好复习复习.

合约创建方式
  1. create

create的用法很简单,就是new一个合约,并传入新合约构造函数所需的参数:

Contract x = new Contract{value: _value}(params)

CREATE如何计算地址

新地址 = hash(创建者地址, nonce)

创建者地址不会变,但nonce可能会随时间而改变,因此用CREATE创建的合约地址不好预测。

  1. 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.

2024.09.01

A-Ethernaut-MagicNumber

这题第一次看的时候很懵,考察的点是opcodeassembly的使用.

evm.code WTF 教程

知识点很多,目前还没看完,过几天补一补.

2024.09.02

A-Ethernaut-AlienCodex

这一题也是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

2024.09.03

A-Ethernaut-Denial

这题直接死循环消耗完gas即可

A-Ethernaut-Shop

攻击合约定义一个price方法 通过isSold判断价格,返回不同的价格

A-Ethernaut-Dex

这题是第一个碰到的defi相关的题目.题目需要把dex的余额变为0.

问题出现在getSwapPrice方法,这个算法有问题,如果多swap几次,余额只会慢慢变多.

A-Ethernaut-DexTwo

这一题对比上一题缺少了require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens"); 可以利用其他代币来换出token.

2024.09.04

A-Ethernaut-PuzzleWallet

今天这道题卡了几个小时,最开始发现proxy的storage slot对不上,就开始研究. 实际上delegatecallproxy合约的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");, 然后就可以修改maxBalanceplayer地址,就结束.

2024.09.05

A-Ethernaut-Motorbike

是个uups的合约,实际上逻辑合约是没有init的,因为proxy合约delegatecallinitialize. 验证发现逻辑合约的upgraderhorsePower确实都是0,没有初始化的. 既然没有初始化,意味着可以调用initialize让自己变成upgrader,然后升级合约.

这题是要让合约selfdestruct,写个攻击合约,带个selfdestruct方法,然后调用upgradeToAndCall 即可.

貌似 foundry test中 selfdestruct 无效,所以 poc 中后面手动处理了

2024.09.06

A-Ethernaut-DoubleEntryPoint

这题分析业务看了有点久,不太明白想要干啥.

CryptoVault中的underlying是不应该被transfer的, require(token != underlying, "Can't transfer underlying token");做了判断,可是sweptTokensRecipienttokenLegacyToken(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, 这里的delegateTransferorigSenderCryptoVault,

所以 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.

2024.09.07

A-Ethernaut-GoodSamaritan

这题requestDonation里catch到错误了,会判断是否是NotEnoughBalance(),是的话就转走剩余的coin,所以实现notify来判断amount==10(转走剩余的coin肯定不等于10),然后revert同样的错误就行

2024.09.08

A-Ethernaut-GatekeeperThree

  1. gateOne():attack合约调用construct0r()即可,这里方法写错了,并不是构造器.
  2. gateTwo():调用getAllowance(uint256 _password)来校验trick的密码,因为passwordprivate的,可以通过storage slot获取,poc里是带创建关卡一起的,所以直接用了block.timestamp
  3. gateThree():给合约转账大于0.001e,并且attack合约在接受e的时候revert就好

2024.09.09

A-Ethernaut-Switch

这题需要调用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的检查.

A-Ethernaut-HigherOrder

这题用的solc还是0.6.x的,abicoder还没有类型检查.0.8.0以上不会有这个问题(默认abicoderV2).

abi.encodeWithSignature("registerTreasury(uint8)", 256);

A-Ethernaut-Stake

正常调用满足要求就行

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完结撒花,学到了很多.

2024.09.11

A-DamnVaulnerableDefi-UnStoppable

还是选择了A系列的DamnVaulnerableDefi,想做一些defi相关.

大概了解了什么是ERC-4626

卡住了(搞了好久依赖,和之前的ethernaut的冲突)

2024.09.12

A-DamnVaulnerableDefi-UnStoppable

题目要求闪电贷失败.

if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance();

把player的token转给vault就好

2024.09.13

A-DamnVaulnerableDefi-NaiveReceiver

又卡住了...

2024.09.14

A-DamnVaulnerableDefi-NaiveReceiver

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哥的.实在是卡在这了,今天累了,明天再整理下,不会组织语言了.

2024.09.16

A-DamnVaulnerableDefi-Truster

问题出在target.functionCall(data); 可以调用token的approve来授权,再调用transferFrom来转走资金. 题目有一笔tx的限制,写个attack合约在constructor里调用