目录
还有最后几道题,赶紧刷完,加油冲鸭!
Naught Coin
题目要求我们把自己的余额变为0,但是限制了我们使用转账函数,源码:
pragma solidity ^0.4.18;
import 'zeppelin-solidity/contracts/token/ERC20/StandardToken.sol';
contract NaughtCoin is StandardToken {
using SafeMath for uint256;
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 = (10 ** decimals).mul(1000000);
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 {
_;
}
}
}
从这里找到了定义和接口,StandardToken.sol 文件还有另一个转账方法 transferFrom(),在 ERC20Lib.sol 里跟进一下具体的实现:
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;
}
发现这个方法不仅要钱够,还有个 allowed 变量的限制,接着往下有个 approve 方法也操作了这个变量:
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;
}
那么就很简单了,首先调用 approve 把所有的钱设置为可以转账的,在调用 transferFrom 转账即可。payload:
contract.approve(player,1000000000000000000000000)
contract.transferFrom(player,contract.address,1000000000000000000000000)
Preservation
题目要求拿到合约所有权,源码:
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;
}
}
这个题说实话根本不知道从何下手,需要改变 owner 才能过关,但是合约中没有任何跟 owner 交互的函数,果然是 difficulty 8/10 的题目。看了看师傅们的 wp,发现问题还是出在 delegatecall() 上(见这篇文章的DELEGATION部分), delegatecall() 只会调用其他合约的代码,而变量还会接着使用原合约的,甚至更改变量也是在原合约的基础上更改(用位置做索引),因此这个 delegatecall() 调用过程是这样:首先调用合约 timeZone1Library 的 setTime() 函数,在这里更改了变量 storedTime 的值,这个变量是合约的第一个变量,对应的 storage 地址是 slot1,但是目前操作的变量都是原合约Preservation 的变量,因此会将合约 Preservation 的 slot1 (变量 timeZone1Library)替换为 _time(Solidity 是世界上最好的语言!)。明白了原理,直接编写 exp 即可:
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
}
}
题目的考点是在 NameRecord newRecord; 这样定义的 struct 默认在 storage 上,对应到这个题目的环境,就是 newRecord.name 在 slot0,newRecord.mappedAddress 在 slot1,而 unlock 在storage 上对应的位置也是 slot0。也就是说可以通过控制 newRecord.name 来控制 unlock 变量的值。payload:
contract.register("0x0000000000000000000000000000000000000000000000000000000000000001", player)
Recovery
题目有个套娃合约,创建的时候会创建一个子合约并向其转账 0.5 ether,我们要做的就是把子合约的 0.5 个 ether 销毁挥着转走。源码:
pragma solidity ^0.4.23;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract Recovery {
//generate tokens
function generateToken(string _name, uint256 _initialSupply) public {
new SimpleToken(_name, msg.sender, _initialSupply);
}
}
contract SimpleToken {
using SafeMath for uint256;
// 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.mul(10);
}
// allow transfers of tokens
function transfer(address _to, uint _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] = balances[msg.sender].sub(_amount);
balances[_to] = _amount;
}
// clean up after ourselves
function destroy(address _to) public {
selfdestruct(_to);
}
}
由于区块链的公开性,所有内容都可以在这个网站找到,直接搜索我们的合约地址,查看交易:

果然发现了一个创建合约的交易,跟进这个交易查看具体信息:

图中标注的即为创建的地址合约,再查看新创建的合约的交易:

果然有人转账了 0.5 ether,源码中有销毁合约的接口,直接部署到 remix 上把钱拿回来即可。

此外,看了看别的师傅的 wp,发现还有一种做法是通过计算获取地址。由于区块链系统任何事物都是可计算的,因此合约地址也可以预测。参考文章。
MagicNumber
题目要我们提供一个 solver 来返回正确的数字,源码里有个 42,那么就部署个合约只能返回 42,将地址传给目标合约。但是有一个难点在于,题目要求了攻击合约的 opcodes 小于等于 10。

首先我们先了解一下什么是 opcodes。在一个合约创建时,实际上是创建者向区块链网络发送了一笔交易(为了使网络能区分出这是创建合约的请求而非正式的交易,这类交易的接收方会被置空);随后 EVM ( Ethernet 虚拟机 ) 会把 Solidity 编译为字节码 ( bytecode )。
字节码分为两部分,第一部分叫初始码(initialization code),主要功能是执行合约的构造函数,并给与合约地址,剩下的部分叫做 runtime code ( 实在不知道怎么翻译 ),最后他会返回到 storage 中并与合约关联,以便日后的调用(可以理解为 runtime code 就是合约的方法的具体实现)。(以上内容参考这篇文章)
也就是说,我们想要过关必须实现一个 return 42 的操作并且让这个操作的 runtime code 小于 10,那么直接看看 return 对应的 runtime code。
return 操作码需要两个参数(return(p, s)),其中 p 是返回变量在内存中的位置(position),s 是返回变量的长度(size),也就是说我们不能直接 return 42,需要先把他放到内存中。内存定义变量的对应的操作码是 mstore,mstore 与 return 一样需要 p 和 s 两个参数来定义位置和值,所以我们最后的 payload:
6042 // v: push1 0x42 (value is 0x42)
6080 // p: push1 0x80 (memory slot is 0x80)
52 // mstore
6020 // s: push1 0x20 (value is 32 bytes in size)
6080 // p: push1 0x80 (value was stored in slot 0x80)
f3 // return
// 604260805260206080f3
这个题到最后没复现出来,不知道是哪里出了问题,再加上这道题实在是过于硬核,就先咕咕咕了。
Alien Codex
Denial
最后一题 shop 404 了没法做,所以这个系列就算通关了,完结撒花~