序
上次的事情一直挂在心头。某个晚上,在写密码学作业的时候,突然想起,既然钓鱼者会设置PassHash,那么原生的pass, 也就是PassHash的原像会在某个交易中或者代碼中出现吧!能不能逆向分析代码或者交易hash,得到pass,然后自己计算keccak256的Hash,不就得到PassHash然後可以得到eth了吗?
说干就干![下面是字节码分析,太长不看的拉倒最后]
先附上钓鱼代码
contract GIFT_1_ETH
{
bytes32 public hashPass;
bool closed = false;
address sender;
uint unlockTime;
function SetPass(bytes32 hash)
public
payable
{
if( (!closed&&(msg.value > 1 ether)) || hashPass==0x0 )
{
hashPass = hash;
sender = msg.sender;
unlockTime = now;
}
}
function GetGift(bytes pass)
external
payable
canOpen
{
if(hashPass == keccak256(pass))
{
msg.sender.transfer(this.balance);
}
}
function PassHasBeenSet(bytes32 hash)
public
{
if(hash==hashPass&&msg.sender==sender)
{
closed=true;
}
}
modifier canOpen
{
require(now>unlockTime);
_;
}
function() public payable{}
}
提要
上一篇说了,实施整个攻击一共有4个账户,分别是N_A(普通账户), N_B(普通账户),C_A(合约账户, 也是钓鱼代码的账户),C_B(合约账户).
重点
+N_B是C_B的创建者.
+N_B先于N_A通过C_B设置了C_A的PassHash,并设置Closed为true。
字节码分析
N_B(普通账户)在创建C_B后向C_B发送了一笔交易,TX_HASH是0x45e45a3de69a0e301eb8f9aeb0dd95311e66e5859b1675ecf0e1e2aa7754f6fe
让我们调试这次交易看看他到底做了什么
- 首先我们来看看开头的字节码
0000 PUSH1 60
// stack: [0x60]
0002 PUSH1 40
// stack: [0x40 0x60]
0004 MSTORE
// 内存中从0x40開始的32个字节用于存储0x60
// stack: []
0005 PUSH1 04
// stack: [0x04]
0007 CALLDATASIZE
// stack: [0x44 0x04]
0008 LT
// lt(x, y) 1 如果x < y, 否则0
// 显然这个结果是0
// stack: [0x0]
0009 PUSH2 0078
// stack: [0x0078, 0x0]
0012 JUMPI
// jumpi(x, y) 如果y为真,则跳到x,否则继续
// stack:[]
0013 PUSH1 00
// stack:[0x00]
0015 CALLDATALOAD
// 从msg.data的index 0开始,读取32个字节
// stack: [0x1562621f0000000000000000000000005dac036595568ff792f5064451b6b37e]
0016 PUSH29 0100000000000000000000000000000000000000000000000000000000
0046 SWAP1
0047 DIV
0048 PUSH4 ffffffff
0053 AND
// 上面4个指令后的stack状态
// stack: [0x1562621f]
0054 DUP1
// 复制第ith个item到栈顶,从栈顶开始计数
// stack: [0x1562621f, 0x1562621f]
0055 PUSH4 1562621f
// stack: [0x1562621f, 0x1562621f, 0x1562621f]
0060 EQ
// stack: [0x1, 0x1562621f]
0061 PUSH2 007a
// stack: [007a, 0x1, 0x1562621f]
0064 JUMPI
// 跳转到0x007a指令去
// summary:
// 大家其实也已经猜到了这里就是在**匹配**函数标志符
// 如果失败,则会失败revert
- 让我们看看进入到函数之后发生了什么?
0122 JUMPDEST
0123 PUSH2 00b3
0126 PUSH1 04
0128 DUP1
0129 DUP1
// stack: [0x4,0x4, 0x4,0xb3,0x1562621f]
0130 CALLDATALOAD
// 从msgdata中index4的位置起,读取32个字节
//stack: [0x5dac036595568ff792f5064451b6b37e801ecab9,0x4, 0x4,0xb3,0x1562621f]
0131 PUSH20 ffffffffffffffffffffffffffffffffffffffff
0152 AND
// stack: [0x5dac036595568ff792f5064451b6b37e801ecab9,0x4, 0x4,0xb3,0x1562621f]
0153 SWAP1
0154 PUSH1 20
0156 ADD
0157 SWAP1
0158 SWAP2
0159 SWAP1
0160 DUP1
// stack: [0x24,0x24,0x4,0x5dac036595568ff792f5064451b6b37e801ecab9,0xb3,0x1562621f]
0161 CALLDATALOAD
// 从msgdata中index34的位置起,读取32个字节
// stack: [0x30a602cddf72988a065febf9b3257b03c23a17b75220a422685e0bc152db5241,0x24,0x4,0x5dac036595568ff792f5064451b6b37e801ecab9,0xb3,0x1562621f]
0162 PUSH1 00
0164 NOT
0165 AND
0166 SWAP1
0167 PUSH1 20
0169 ADD
0170 SWAP1
0171 SWAP2
0172 SWAP1
0173 POP
0174 POP
0175 PUSH2 01d6
0178 JUMP
// stack:[ 0x1d6, 0x30a602cddf72988a065febf9b3257b03c23a17b75220a422685e0bc152db5241,0x5dac036595568ff792f5064451b6b37e801ecab9,0xb3,0x1562621f]
// summary:
// EVM在为函数执行做了很多工作,比如基本的逻辑判断,msgdata的载入
- C_B中肯定会调用c_A的函数,让我们直接找到有call的地方
******省略了无关紧要的字节码******
//此时stack: [0x5dac036595568ff792f5064451b6b37e801ecab9,0x0, 0x60, 0x24,0x60,0x0,0x84,0xa6fbb05,0x5dac036595568ff792f5064451b6b37e801ecab9,0x5dac036595568ff792f5064451b6b37e801ecab9,0x30a602cddf72988a065febf9b3257b03c23a17b75220a422685e0bc152db5241,0x5dac036595568ff792f5064451b6b37e801ecab9,0xb3, 0x1562621f]
0684 JUMPDEST
0685 PUSH2 02c6
0688 GAS
//得到当前可用的gas, 结果是0x2e0b8,与交易显示的GAS LIMIT相差无几
0689 SUB
0690 CALL
//此时跳入被调用者的运行环境中....
0691 ISZERO
0692 ISZERO
0693 PUSH2 02bd
0696 JUMPI
- 经过上述处理,代码调用C_B的代码,已经进入了C_B的运行环境,让我们看看他做了什么
.....省略了一些字节码
.....
0015 CALLDATALOAD
0016 PUSH29 0100000000000000000000000000000000000000000000000000000000
0046 SWAP1
0047 DIV
0048 PUSH4 ffffffff
0053 AND
0054 DUP1
0055 PUSH4 0a6fbb05
0060 EQ
//stack: [0xa6fbb05, 0xa6fbb05,0xa6fbb05]
// 这段代码是匹配函数,大家都知道
// 显示也是成功匹配的
// 那么我们再看看,这个是那个函数的标识符??
// SetPass(bytes32 hash)!!!!
// 所以n_b确实是通过c_b设置pass!!
// 那么会出现pass吗?
- 我们继续看..
0133 JUMPDEST
0134 PUSH2 009f
0137 PUSH1 04
0139 DUP1
0140 DUP1
0141 CALLDATALOAD
// 加载msg.data
// 此时stack:[0x30a602cddf72988a065febf9b3257b03c23a17b75220a422685e0bc152db5241,0x4,0x4,0x9f,0xa6fbb05]
//
.......省略......
......
.................
0450 JUMPDEST
0451 PUSH1 01
0453 PUSH1 00
0455 SWAP1
0456 SLOAD
0457 SWAP1
0458 PUSH2 0100
0461 EXP
0462 SWAP1
0463 DIV
0464 PUSH1 ff
0466 AND
0467 ISZERO
0468 DUP1
0469 ISZERO
0470 PUSH2 01e6
0473 JUMPI
///summary:
/// 执行!closed表达式
0475 PUSH8 0de0b6b3a7640000
0484 CALLVALUE
0485 GT
0486 JUMPDEST
// summary:
//
// 执行msg.value > 1 ether表达式
.........省略
..............
0516 PUSH1 00
0518 NOT
0519 AND
0520 SWAP1
0521 SSTORE
// 上述几个指令即其前面的指令
// 将storage第0个位置存入callvalue
//即0x30a602cddf72988a065febf9b3257b03c23a17b75220a422685e0bc152db5241
// 而第0个位置的值就是PassHash
// 所以钓鱼者没有将Hash原像出现在交易中,而是**直接**设置了hash
0523 CALLER
.........省略.....
0585 SSTORE
0586 POP
// 设置sender
0587 TIMESTAMP
0588 PUSH1 02
0590 DUP2
0591 SWAP1
0592 SSTORE
0593 POP
//// 设置unlockTime
- 如果到现在为止,都只是在设置passHash,那么设置closed是在哪儿呢??? 字节码还没有执行完呢,继续看.....
// 调回原来的运行环境
// 检查返回值
0691 ISZERO
0692 ISZERO
0693 PUSH2 02bd
0696 JUMPI
.........省略
...............................
/// 又发现一个call调用
0822 JUMPDEST
0823 PUSH2 02c6
0826 GAS
0827 SUB
0828 CALL
//stack: [0x1e959, 0x5dac036595568ff792f5064451b6b37e801ecab9, .........]
// 第一个是gas的可用量,第二个是被调用者的地址,即C_A
/// 跳入被调者的运行环境。。。。。
.........
0829 ISZERO
0830 ISZERO
0831 PUSH2 0347
0834 JUMPI
- 现在这是调用哪个函数呢?
.....省略
..........
// stack: [0x31fd725a]
0076 DUP1
0077 PUSH4 31fd725a
0082 EQ
0083 PUSH2 00c4
0086 JUMPI
// 匹配函数0x31fd725a, 是谁???
// Keccak-256("PassHasBeenSet(bytes32)")[0:4] == 0x31fd725a!!!!
//
........接下来的时候就是一些判断
.......然后设置closed为true
结论
- N_B从没有让passHash的原像出现在区块链上,除非你跟钓鱼黑客心心相印,知道Hash的原像是什么,否则是很难拿走所谓的gift.
- 调试0x6290f0dfb9673f56a3da48884414e3c99a59021d99fc5c4a866b89180b4a64e1这个Tx,也就是N_B是怎么转走C_A的eth的交易,我们可以知道,N_B是通过C_B,调用C_A的Revoce()来转走eth的,而不是GetGift,没有出现bytes参数。
最后要打赏的客官这边: 0x003be5df5fef651ef0c59cd175c73ca1415f53ea