前言
22 shop
这题貌似已经下线,嘤嘤嘤嘤- 以下
wp
都是基于0.4.*
版本的 - 记录一下刚学习 Smart Contract 做题的平台的
WP
(2333入门级,开心就好~~~),如果没有任何基础,可以参考 CryptoZombies 等教程,下面是题目平台地址: - https://ethernaut.openzeppelin.com/
Hello Ethernaut
- 熟悉关卡挑战的模式,以及执行操作的方式,根据其介绍一步一步操作即可
- 由于网络不太稳定的原因,可以多试几次,成功的结果花里胡哨的23333
Fallback
Require
you claim ownership of the contract
you reduce its balance to 0
Source
1 | pragma solidity ^0.4.18; |
Analyse
合约可以有一个未命名的函数。这个函数不能有参数也不能有返回值。 如果在一个到合约的调用中,没有其他函数与给定的函数标识匹配(或没有提供调用数据),那么这个函数(
fallback
函数)会被执行。除此之外,每当合约收到以太币(没有任何数据),这个函数就会执行。此外,为了接收以太币,fallback
函数必须标记为payable
。很明显我们如果通过反复调用
contribute
来触发owner
不现实,因为我们每次最多向合约贡献不大于0.001 ether
,而要超过owner
需要1000 ether
(构造函数赋予owner
的)。但我们惊喜地发现fallback
函数同样可以改变owner
的值,那么对应的操作就非常清晰了:- 调用合约的
contribute
使得合约中我们账户对应的balance
大于0
- 触发
fallback
函数使得合约对应的owner
变成我们 - 调用
withdraw
函数清空balance
- 调用合约的
Solution
1 | // step 1 |
Fallout
Require
Claim ownership of the contract below to complete this level.
Source
1 | pragma solidity ^0.4.18; |
Analyse
- 我们可以发现一个很明显的问题,理论上应该写成
Fallout
的构造函数被写成了Fal1out
,那么该函数就不是构造函数,意味着该函数可以被我们调用(我们无法调用构造函数)。
Solution
1 | // 调用该函数,修改 owner |
Coin Flip
Require
This is a coin flipping game where you need to build up your winning streak by guessing the outcome of a coin flip. To complete this level you'll need to use your psychic abilities to guess the correct outcome 10 times in a row.
Source
1 | pragma solidity ^0.4.18; |
Analyse
代码处理流程为:
- 获得上一块的
hash
值 - 判断与之前保存的
hash
值是否相等,相等则会退 - 根据
blockValue/FACTOR
的值判断为正或负,即通过hash
的首位判断
- 获得上一块的
以太坊区块链上的所有交易都是确定性的状态转换操作,每笔交易都会改变以太坊生态系统的全球状态,并且是以一种可计算的方式进行,这意味着其没有任何的不确定性。所以在区块链生态系统内,不存在熵或随机性的来源。如果使用可以被挖矿的矿工所控制的变量,如区块哈希值,时间戳,区块高低或是
Gas
上限等作为随机数的熵源,产生的随机数并不安全。
Solution
1 | pragma solidity ^0.4.18; |
- 调用
10
次 exploit() 即可
Telephone
Require
Claim ownship of the contract below to complete this level
Source
1 | pragma solidity ^0.4.18; |
Analyse
- 这里区分一下
tx.origin
和msg.sender
,msg.sender
是函数的直接调用方,在用户手动调用该函数时是发起交易的账户地址,但也可以是调用该函数的一个智能合约的地址。而tx.origin
则必然是这个交易的原始发起方,无论中间有多少次合约内/跨合约函数调用,而且一定是账户地址而不是合约地址。 - 给定这样一个场景如:用户通过 合约A 调 合约B ,此时:
- 对于 合约A :
tx.origin
和msg.sender
都是用户 - 对于 合约B :
tx.origin
是用户,msg.sender
是 合约A
所以,这里部署一个第三方合约即可。
- 对于 合约A :
Solution
1 | pragma solidity ^0.4.18; |
- 攻击者调用
exploit()
即可
Token
Require
The goal of this level is for you to hack the basic token contract below.
You are given 20 tokens to start with and you will beat the level if you somehow manage to get your hands on any additional tokens. Preferably a very large amount of tokens.
Source
1 | pragma solidity ^0.4.18; |
Analyse
- 经典的整数溢出问题,在
transfer()
函数第一行require
里,这里的balances
和value
都是uint
。此时balances
为20
,令value=21
,产生下溢,从而绕过验证,并转出一笔很大的金额。
Solution
1 | // 转给谁不重要,关键是利用 20-21 触发整数下溢 |
- 但是也并非没有办法来处理该问题,最简单的处理是在每一次数学运算时进行判断,如
a=a+b
;就可以写成if(a+b>a) a=a+b;
。题目建议的另一种解决方案则是使用 OpenZeppelin团队 开发的 SafeMath库 ,如果整数溢出漏洞发生时,函数将进行回退操作,此时加法操作可以写作这样:a=a.add(b);
Delegation
Require
claim ownership of the instance
Source
1 | pragma solidity ^0.4.18; |
Analyse
- 我们看下
delegatecall
的文档
There exists a special variant of a message call, named delegatecall which is identical to a message call apart from the fact that the code at the target address is executed in the context of the calling contract and msg.sender and msg.value do not change their values.
考点一
- 考点一在于
Solidity
支持两种底层调用方式 call 和 delegatecall - call 外部调用时,上下文是外部合约
- delegatecall 外部调用时,上下文是调用合约
- 所以
delegate.delegatecall(msg.data)
其实调用的是delegate
自身的msg.data
考点二
- 熟悉
raw
格式的交易的data
的会知道:data
头4
个byte
是被调用方法的签名哈希,即bytes4(keccak256("func"))
,remix
里调用函数,实际是向合约账户地址发送了(msg.data[0:4]
== 函数签名哈希 )的一笔交易 - 所以我们只需调用
Delegation
的fallback
的同时在msg.data
放入pwn
函数的签名即可
考点三
- 这里其实主要思路就是
fallback
的触发条件:- 一是如果合约在被调用的时候,找不到对方调用的函数,就会自动调用
fallback
函数 - 二是只要是合约收到别人发送的
Ether
且没有数据,就会尝试执行fallback
函数,此时fallback
需要带有payable
标记,否则,合约就会拒绝这个Ether
- 一是如果合约在被调用的时候,找不到对方调用的函数,就会自动调用
综上,我们只需调用 Delegation
的 假pwn()
即可,这样就会触发 Delegation
的 fallback
,这样 pwn
的函数签名哈希就会放在 msg.data[0:4]
了,这样就会只需 delegate
的 pwn()
把 owner
变成自己
Solution
web3
中sha3
就是keccak256
Force
Require
make the balance of the contract greater than zero
Source
1 | pragma solidity ^0.4.18; |
Analyse
- 骚操作,
selfdestruct
自毁合约强转 - 所以只需要再部署一个合约,打一点钱,然后自毁把合约金额转给目标合约即可
Solution
1 | pragma solidity ^0.4.18; |
- 部署
hack
的时候转一点钱,然后执行exploit()
即可
Vault
Require
Unlock the vault to pass the level!
Source
1 | pragma solidity ^0.4.18; |
Analyse
- 通关条件是
locked = false
- 考点关键是区块链上的所有信息是公开的
- 可以用
web3
的getStorageAt
来访问合约里变量的值
Solution
King
Require
When you submit the instance back to the level, the level is going to reclaim kingship. You will beat the level if you can avoid such a self proclamation.
Source
1 | pragma solidity ^0.4.18; |
Analyse
- 代码逻辑很简单,谁给的钱多谁就能成为 King ,并且将前任 King 的钱归还。当提交
instance
时,题目会重新夺回 King 的位置,需要阻止其他人成为 King方可通关 - 首先看一下
Solidity
中几种转账方式:- address.transfer()
当发送失败时会throw
;回滚状态
只会传递部分Gas
供调用,防止重入 - address.send()
当发送失败时会返回false
只会传递部分Gas
供调用,防止重入 - address.call.value()()
当发送失败时会返回false
传递所有可用Gas
供调用,不能有效防止重入
- address.transfer()
- 回头看下代码,当我们成为 King 后,如果有人出价比我们高,会首先把钱退回给我们,使用的是 transfer ,上面提到当 transfer 调用失败时会回滚状态,那么如果合约在退钱这一步骤一直调用失败的话,那么代码将无法继续向下运行,其他人也就无法成为新的 King,达到攻击效果
Solution
- 首先查看一下当前最高出价
1 | await fromWei((await contract.prize()).toNumber()) |
- 部署一个新的合约,当收到转账时主动抛出错误
1 | pragma solidity ^0.4.18; |
- 调用
hack()
即可,可以看到调用hack()
后成为了新的 King ,而且Submit innstance
后,仍然是 King
Re-entrancy
Require
The goal of this level is for you to steal all the funds from the contract
Source
1 | pragma solidity ^0.4.18; |
Analyse
- DASP 排第一的重入漏洞,也是著名的 DAO 事件里用到的方法
- 漏洞主要在于
withdraw()
函数,合约在进行提币时,使用require
依次判断提币账户是否拥有相应的资产,随后使用msg.sender.call.value(amount)
来发送Ether
,处理完成后相应修改用户资产数据 - 在提币的过程中,存在一个递归
withdraw
的问题(因为-=_amount
在转账之后),攻击者可以部署一个包含恶意递归调用的合约将公共钱包合约里的Ether
全部提出 - 其中,转账使用的是
address.call.value()()
函数,传递了所有可用gas
供调用,是可以成功执行递归的前提条件
Solution
- 查看题目账户余额信息:
- Reentrance 合约余额为
1 eth
balances[address(Attacker)] = 0
- Reentrance 合约余额为
- 所以部署 Attacker 合约时,可以给 Attacker 合约先转
1 eth
,然后donate 1 eth
,这样的话 Reentrance 合约余额就是2 eth
了,balances[address(Attacker)]
就是1 eth
,然后每次withdraw 1 eth
,这样的话,重入2
次就能将钱全部转出
1 | pragma solidity ^0.4.19; |
- 合约刚部署好余额如下
- 调用 Attacker 的
donate()
- 调用
hack()
进行重入攻击2
次,可以看到 Reentrance 合约余额变为0
,而balances[address(Attacker)] = 1 - 2
,由于是uint256
类型,所以下溢变成了uint256
类型最大值,攻击成功
Elevator
Require
reach the top of your building
Source
1 | pragma solidity ^0.4.18; |
Analyse
- 通关条件是使
contract.top = true
Building
接口中声明了isLastFloor
函数,用户可自行编写- 在主合约中,先调用
building.isLastFloor(floor)
进行if
判断,然后将building.isLastFloor(floor)
赋值给top
。要使top = ture
,则building.isLastFloor(floor)
第一次调用需返回false
,第二次调用返回true
- 所以就有了思路:设置一个初始值为
true
的变量,每次调用isLastFloor()
时,将其取反再返回 - 但是,题目在声明
isLastFloor
时,赋予了 view 属性,view 表示函数会读取合约变量,但是不会修改任何合约的状态 - 看了下题目的提示
Sometimes solidity is not good at keeping promises.
- 翻阅了文档,找到对 view 的描述:
view functions: The compiler does not enforce yet that a view method is not modifying state.
- 意思是当前
Solidity
编译器没有强制执行 view 函数不能修改状态,所以上述做法就是可行的1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32pragma solidity ^0.4.18;
interface Building {
function isLastFloor(uint) view public returns (bool);
}
contract Elevator {
bool public top;
uint public floor;
function goTo(uint _floor) public {
Building building = Building(msg.sender);
if (! building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}
contract hack {
address instance_address = 0x89fa2727ad30129f657994117323f7e15b3c626a;
Elevator e = Elevator(instance_address);
bool public flag = true;
function isLastFloor(uint) public returns (bool){
flag = !flag;
return flag;
}
function exploit() public{
e.goTo(123);
}
}
调用 exploit()
即可
Privacy
Require
Unlock this contract to beat the level
Source
1 | pragma solidity ^0.4.18; |
Analyse
- 之前
Vault
题目的升级版,还是一样,用getStorageAt()
把链上的数据读出来
Solution
1 | await web3.eth.getStorageAt(instance, 0, function(x,y){console.info(y);}) |
- 可以看到,每一个存储位是
32
个字节。根据Solidity
优化规则,当变量所占空间小于32
字节时,会与后面的变量共享空间,如果加上后面的变量也不超过32
字节的话,除去ID
常量无需存储:bool public locked = true
占1
字节 ->01
uint8 private flattening = 10
占1
字节 ->0a
uint8 private denomination = 255
占1
字节 ->ff
uint16 private awkwardness = uint16(now)
占2
字节 ->df9d
- 刚好对应了第一个存储位的
df9dff0a0a
- 所以
data[2]
应该在第四个存储位0xf001da34b0220001e65b894cb5ea3d0a3155843c51477b29215a6aa43348b697
Gatekeeper One
Require
Make it past the gatekeeper and register as an entrant to pass this level.
Source
1 | pragma solidity ^0.4.18; |
Analyse
- 满足三个
modifier
条件即可 gateOne
很简单,通过第三方合约调用enter
即可gateTwo
需要满足msg.gas % 8191 == 0
msg.gas
是remaining gas
,在remix
的Javascript VM
环境下进行Debug
, 在Step detail
可以看到这个变量,假设在进入enter
之前的remaining gas = 81910
- 调试到
gateTwo
的msg.gas
地方,此时remaining gas = 81697
那么这个过程间消耗的
gas = 81910 - 81697
,加上gas
本身消耗的2
即可,所以为了满足gateTwo
,在进入enter
之前的gas
可以设置为81910-91697+81910+2
gateThree
也比较简单,最后的逻辑是将tx.origin
倒数三四字节换成0000
即可,可以通过bytes8(tx.origin) & 0xFFFFFFFF0000FFFF
实现
Solution
1 | pragma solidity ^0.4.18; |
调用 exploit()
即可
Gatekeeper Two
Require
Register as an entrant to pass this level
Source
1 | pragma solidity ^0.4.18; |
Analyse
- 满足三个
modifier
即可 gateOne
很简单,通过第三方合约调用enter
即可gateThree
毕竟简单,直接异或逆运算_gateKey = bytes8(uint64(keccak256(address(this))) ^ (uint64(0) - 1))
gateTwo
比较有技巧性,用了 内联汇编 的写法,翻了一下文档 https://ethereum.github.io/yellowpaper/paper.pdf :- caller : Get caller address.
- extcodesize : Get size of an account’s code.
- 按照题目的意思,要使当前合约代码区为空,显然与解题是矛盾的,仔细读文档,有一些细节
Note that while the initialisation code is executing, the newly created address exists but with no intrinsic body code.
……
During initialization code execution, EXTCODESIZE on the address should return zero, which is the length of the code of the account while CODESIZE should return the length of the initialization code.
- 也就是说,在执行初始化代码(构造函数),而新的区块还未添加到链上的时候,新的地址已经生成,然而代码区为空,此时,调用
EXTCODESIZE()
返回为0
- 那么,只需要在第三方合约的构造函数中调用题目合约中的
enter()
即可
Solution
1 | pragma solidity ^0.4.18; |
直接部署合约 hack
即可
Naught Coin
Require
Complete this level by getting your token balance to 0.
Source
1 | pragma solidity ^0.4.18; |
Analyse
- 根据题意,需要将自己的
balance
清空。合约提供了transfer()
进行转账,但有一个modifier lockTokens()
限制,只有10
年后才能调用transfer()
- 注意该合约是
StandardToken
的子合约,题目中也给了 The ERC20 Spec 和 The OpenZeppelin codebase - 在子合约找不出更多信息的时候,把目光更多放到父合约 StandardToken.sol 和接口上
- 在 The ERC20 Spec 中,除了
transfer()
之外,还有transferFrom()
函数也可以进行转账 - 直接看父合约 StandardToken.sol
1 | contract StandardToken { |
- 跟进 ERC20Lib.sol
1 | library ERC20Lib { |
可以直接调用这个 transferFrom
,但是 transferFrom
需要 msg.sender
获得授权,由于我们就是合约的 owner
,所以可以自己调用 approve
给自己授权
Solution
1 | await contract.approve(player, (await contract.INITIAL_SUPPLY()).toNumber()) |
Preservation
Require
This contract utilizes a library to store two different times for two different timezones. The constructor creates two instances of the library for each time to be stored.
The goal of this level is for you to claim ownership of the instance you are given.
Source
1 | pragma solidity ^0.4.23; |
Analyse
delegatecall
定义:.delegatecall(…) returns (bool): issue low-level DELEGATECALL, returns false on failure, forwards all available gas, adjustabledelegatecall
与call
功能类似,区别在于delegatecall
仅使用给定地址的代码,其它信息则使用当前合约(如存储,余额等等)。注意delegatecall
是危险函数,它可以完全操作当前合约的状态,可以参考第7题Delegation
delegateCall
方法仅仅使用目标合约的代码, 其余的storage
等数据均使用自己的,这就使得某些访存操作会错误的处理对象- 所以这个题可以这样解决:
- 我们调用
Preservation
的setFirstTime
函数实际通过delegatecall
执行了LibraryContract
的setTime
函数,修改了slot 1
,也就是修改了timeZone1Library
变量 - 这样,我们第一次调用
setFirstTime
将timeZone1Library
变量修改为我们的恶意合约的地址,第二次调用setFirstTime
就可以执行我们的任意代码了
- 我们调用
Solution
1 | pragma solidity ^0.4.23; |
先调用 attack1()
,再调用 attack2()
即可
Locked
Require
This name registrar is locked and will not accept any new names to be registered.
Unlock this registrar to beat the level.
Source
1 | pragma solidity ^0.4.23; |
Analyse
- 典型的利用
struct
默认是storage
的题目 - 函数中声明的
newRecord
结构体修改name
和mappedAddress
实际分别改的是unlocked
和bytes32 name
- 所以把
name
对应的slot 0
的值改成1
就行了
Solution
1 | pragma solidity ^0.4.23; |
调用 exploit()
即可
Recovery
Require
A contract creator has built a very simple token factory contract. Anyone can create new tokens with ease. After deploying the first token contract, the creator sent 0.5 ether to obtain more tokens. They have since lost the contract address.
This level will be completed if you can recover (or remove) the 0.5 ether from the lost contract address
- 其实简单来说就是已知一个
Recovery
合约地址,恢复一下它创建的SimpleToken
地址,然后将0.5 eth
从丢失地址的合约中提出即可
Source
1 | pragma solidity ^0.4.23; |
Analyse
- 区块链上所有信息都是公开的,直接上
ropsten
测试网的官方网页查就可以了
Solution
方法一
- 从
console
找到实例地址:0xd29fcc4b193a576a17af9194d706b17ce5da24e2 - 通过
ropsten.etherscan.io
找到这个实例的交易信息:
https://ropsten.etherscan.io/address/0xd29fcc4b193a576a17af9194d706b17ce5da24e2#internaltx
- 再通过交易信息找到生产合约
lost contract
的地址: 0x77a70a61a077e3aee72404e0e70211bfa72e962b
- 在
remix
部署SimpleToken
,使用At address
指定lost contract
的地址,然后执行destroy(play_address)
即可
- 查看合约地址可以看到已经被销毁
https://ropsten.etherscan.io/address/0x77a70a61a077e3aee72404e0e70211bfa72e962b#internaltx
一些分析
- 方法一提交之后,
Zeppelin
给出了原理如下:
Contract addresses are deterministic and are calculated by keccack256(address, nonce)
where the address
is the address of the contract (or ethereum address that created the transaction) and nonce
is the number of contracts the spawning contract has created (or the transaction nonce, for regular transactions).
Because of this, one can send ether to a pre-determined address (which has no private key) and later create a contract at that address which recovers the ether. This is a non-intuitive and somewhat secretive way to (dangerously) store ether without holding a private key.
An interesting blog post by Martin Swende details potential use cases of this.
If you’re going to implement this technique, make sure you don’t miss the nonce, or your funds will be lost forever.
- 原来题目的考点是合约地址可计算,所以这题有两种解法
方法二
- 参考 https://www.freebuf.com/articles/blockchain-articles/179662.html
- 参考 https://github.com/ethereum/wiki/wiki/RLP
1 | def rlp_encode(input): |
- 结果是 d694d29fcc4b193a576a17af9194d706b17ce5da24e201
- 拿到
solidity
计算地址
1 | pragma solidity ^0.4.18; |
- 可以看到计算的地址和方法一是一样的
MagicNumber
Require
To solve this level, you only need to provide the Ethernaut with a “Solver”, a contract that responds to “whatIsTheMeaningOfLife()” with the right number.
Easy right? Well… there’s a catch.
The solver’s code needs to be really tiny. Really reaaaaaallly tiny. Like freakin’ really really itty-bitty tiny: 10 opcodes at most.
Hint: Perhaps its time to leave the comfort of the Solidity compiler momentarily, and build this one by hand O_o. That’s right: Raw EVM bytecode.
Good luck!
- 题目的意思就是部署一个合约
Solver
,要求在被调用whatIsTheMeaningOfLife()
函数时返回 42 就可以了,但有一个限制是不能超过 10 个opcode
Source
1 | pragma solidity ^0.4.24; |
Analyse
多说的话
- 参考 https://medium.com/coinmonks/ethernaut-lvl-19-magicnumber-walkthrough-how-to-deploy-contracts-using-raw-assembly-opcodes-c50edb0f71a2
- 参考 https://f3real.github.io/Ethernaut_wargame19.html
先看一下 contract creation 期间会发生什么:
1、首先,用户或合约将交易发送到以太网网络。此交易包含数据,但没有 to
地址,表明这是一个合约创建,而不是一个 send/call transaction
2、其次,EVM
将 Solidity
(高级语言)的合约代码编译为 bytecode(底层的机器语言),该 bytecode 直接转换为 opcodes ,在单个调用堆栈中运行
需要注意的是:contract creation 的 bytecode 包含两部分:initialization code
和 runtime code
3、在 contract creation 期间,EVM
仅执行 initialization code
直到到达堆栈中的第一条 STOP 或 RETURN 指令,在此阶段,合约的 constructor()
会被运行,合约便有地址了
在运行 initialization code
后,只有 runtime code
在堆栈上,然后将这些 opcode 拷贝 到 memory
并返回到 EVM
4、最后,EVM
将 runtime code
返回的 opcode 存储在 state storage
,并与新的合约地址相关联,在将来对新合约的调用时,这些 runtime code
将被执行
对于该题
- 所以为了解决该题,我们需要
initialization opcodes
和runtime codes
initialization opcodes
: 由EVM
运行创建合约并存储将来要用的runtime codes
runtime codes
: 包含所需的实际执行逻辑。对于本题来说,这是应该返回的代码的主要部分,应该 return 42 并且 under 10 opcodes
1、先来看 runtime codes
:
返回值由 return(p, s) 操作码处理,但是在返回值之前,必须先存储在内存中,使用 mstore(p, v) 将 42 存储在内存中
首先,使用 mstore(p, v) 将 42 存储在内存中,其中
p
是在内存中的存储位置,v
是十六进制值,42 的十六进制是 0x2a1
2
30x602a ;PUSH1 0x2a v
0x6080 ;PUSH1 0x80 p
0x52 ;MSTORE然后,使用 return(p, s) 返回 0x2a ,其中
p
是值 0x2a 存储的位置,s
是值 0x2a 存储所占的大小0x20
,占32字节1
2
30x6020 ;PUSH1 0x20 s
0x6080 ;PUSH1 0x80 p
0xf3 ;RETURN
所以
runtime codes
应该是 602a60805260206080f3 ,正好 10 opcodes
2、再来看 initialization codes
:
首先,
initialization codes
需要先将runtime codes
拷贝到内存,然后再将其返回到EVM
。将代码从一个地方复制到另一个地方是 codecopy(t, f, s) 操作码。t 是代码的目标位置,f 是runtime codes
的当前位置,s 是代码的大小,以字节为单位,对于 602a60805260206080f3 就是 10 bytes1
2
3
4
5;copy bytecode to memory
0x600a ;PUSH1 0x0a S(runtime code size)
0x60?? ;PUSH1 0x?? F(current position of runtime opcodes)
0x6000 ;PUSH1 0x00 T(destination memory index 0)
0x39 ;CODECOPY然后,需要将内存中的 runtime codes 返回到
EVM
1
2
3
4;return code from memory to EVM
0x600a ;PUSH1 0x0a S
0x6000 ;PUSH1 0x00 P
0xf3 ;RETURNinitialization codes
总共占了 0x0c 字节,这表示runtime codes
从索引 0x0c 开始,所以 ?? 的地方是 0x0c- 所以,
initialization codes
最后的顺序是 600a600c600039600a6000f3
所以,opcodes最后的顺序是 0x600a600c600039600a6000f3602a60805260206080f3
Solution
1 | var bytecode = "0x600a600c600039600a6000f3602a60805260206080f3"; |
- 得到https://ropsten.etherscan.io/tx/0x2e2a636712b37e27af795073a7be6fca9ddfdf964a2356d98c113463c69359ff,点击查看如下,得到
Contract address
为 0x7baa1861df4eff11ff258e657bff2420be19b564
- 调用题目合约 setsolver(“0x7baa1861df4eff11ff258e657bff2420be19b564”) 即可
Alien Codex
Require
You've uncovered an Alien contract. Claim ownership to complete the level.
Source
1 | pragma solidity ^0.4.24; |
Analyse
- 合约开头
import
了 Ownable.sol 合约,同时也引入了一个 owner 变量
1 | await web3.eth.getStorageAt(instance, 0, function(x, y) {console.info(y)}); |
其中 owner = 0x73048cec9010e92c298b016966bde1cc47299df5 ,contract = 0x0,这是由于
EVM
存储优化的关系,可以参考 https://solidity.readthedocs.io/en/v0.4.25/miscellaneous.html#layout-of-state-variables-in-storage 并且数组 codex 的slot
为1
,同时这也是存储数组 length 的地方,而 codex 的实际内容存储在keccak256(bytes32(1))
开始的位置Keccak256
是紧密打包的,意思是说参数不会补位,多个参数也会直接连接在一起,所以要用keccak256(bytes32(1))
- 参考 Solidity中各种变量的存储方式
这样我们就知道了 codex 实际的存储的
slot
,可以将动态数组内变量的存储位计算方法概括为:array[array_slot_index] == SLOAD(keccak256(slot(array)) + slot_index)
. 因为总共有 2^256 个slot
,要修改 slot 0 ,假设 codex 实际所在slot x
,(对于本题来说,数组的slot
是1
, x=keccak256(bytes32(1))) ,那么当我们修改 codex[y],(y=2^256-x+0) 时就能修改 slot 0 ,从而修改 owner- 我们要修改 codex[y] ,那就要满足
y < codex.length
,而这个时候 codex.length =0 ,但是我们可以通过 retract() 使length
下溢,然后就可以操纵 codex[y] 了
- 我们要修改 codex[y] ,那就要满足
1 | await web3.eth.getStorageAt(instance, 1, function(x, y) {console.info(y)}); |
- 但是无论调用题目合约哪个函数,都要满足
modifier contacted()
,所以要先使contact=true
,也就是要先解决make_contact
这个问题
Solution
1、先看 make_contact
函数,我们需要传人一个 length>2^200
的数组,OPCODE 中数组长度是存储在某个 slot
上的,并且没有对数组长度和数组内的数据做校验,所以可以构造一个存储位上长度很大,但实际上并没有数据的数组,打包成 data
发送1
2
3
4
5
6
7
8
9
10
11
12sig = web3.sha3("make_contact(bytes32[])").slice(0,10) // 函数id
// "0x1d3d4c0b"
data1 = "0000000000000000000000000000000000000000000000000000000000000020" //偏移,指的是除了函数id,数组内容开始的位置,在这里我们设置的是offset=32
// 除去函数选择器,数组长度的存储从第 0x20 位开始
data2 = "1000000000000000000000000000000000000000000000000000000000000001" //length>2^200
// 数组的长度
await contract.contact()
// false
contract.sendTransaction({data: sig + data1 + data2});
// 发送交易
await contract.contact()
// true
2、计算 codex 位置为 slot 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6
- 所以 y = 2^256 - 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 + 0
- 即 y = 35707666377435648211887908874984608119992236509074197713628505308453184860938
1 | pragma solidity ^0.4.24; |
3、可以看到 y = 35707666377435648211887908874984608119992236509074197713628505308453184860938 很大,而 codex.length=0(见Analyse)
很小,我们通过 retract()
使得 codex 数组 length
下溢,使其满足 y < codex.length1
2
3
4
5
6
7
8web3.eth.getStorageAt(instance, 1, function(x, y) {console.info(y)}); // codex.length
// 0x0000000000000000000000000000000000000000000000000000000000000000
contract.retract()
// codex.length--
web3.eth.getStorageAt(instance, 1, function(x, y) {console.info(y)}); // codex.length
// 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
4、由2和3已经计算出 codex[35707666377435648211887908874984608119992236509074197713628505308453184860938] 对应的存储位就是 slot 0 ,在 Analyse
中提到 slot 0 中同时存储了 contact
和 owner ,我们只需将 owner 换成 player 地址即可1
2
3
4
5
6
7
8
9await contract.owner()
// "0x73048cec9010e92c298b016966bde1cc47299df5"
player
// "0x88d3052d12527f1fbe3a6e1444ea72c4ddb396c2"
contract.revise('35707666377435648211887908874984608119992236509074197713628505308453184860938','0x00000000000000000000000088d3052d12527f1fbe3a6e1444ea72c4ddb396c2')
// 调用 revise()
await contract.owner()
// "0x88d3052d12527f1fbe3a6e1444ea72c4ddb396c2"
// Submit instance
Denial
Require
This is a simple wallet that drips funds over time. You can withdraw the funds slowly by becoming a withdrawing partner.
If you can deny the owner from withdrawing funds when they call withdraw() (whilst the contract still has funds) you will win this level.
- 结合代码看了一下,要求就是在调用
withdraw
时,禁止owner
转走账户的1%
的余额
Source
1 | pragma solidity ^0.4.24; |
Analyse
- 可以使
transfer
失败,也就是把gas
耗光 - 使用
assert
失败的话,将会spend all gas
,这样的话owner.transfer(amountToSend)
将执行失败 - 这里还有一个很明显的重入漏洞
partner.call.value(amountToSend)()
,利用重入漏洞把gas
消耗完,应该也可以达到目的(自行尝试)
Solution
1 | pragma solidity ^0.4.24; |
直接调用 hack1()
即可
Shop
Require
- 题目意思是修改
price
小于100
- (不过这个题目好像下线了23333)
Source
1 | pragma solidity 0.4.24; |
Analyse
- 本来想的是利用
storage
修改,可是修改变量需要 5000 gas,但是我们只有 3000 - 所以需要另想办法,发现
isSold
是public
属性,所以可以利用isSold
,根据isSold
进行判断,两次调用_buyer.price.gas(3000)()
第一次返回大于等于100
,第二次返回小于100
即可
Solution
1 | pragma solidity 0.4.24; |
直接调用 attack()
即可
Reference
http://mitah.cn/index.php/archives/14/
https://f3real.github.io/Ethernaut_wargame2022.html#lvl-21-denial
https://www.codercto.com/a/38161.html
https://www.secpulse.com/archives/73682.html
https://www.anquanke.com/post/id/148341#h2-12
https://xz.aliyun.com/t/2856#toc-4
https://blog.riskivy.com/智能合约ctf:ethernaut-writeup-part-4/
https://medium.com/coinmonks/ethernaut-lvl-19-magicnumber-walkthrough-how-to-deploy-contracts-using-raw-assembly-opcodes-c50edb0f71a2