区块链安全

钓鱼合约骗局分析

学习了 以太坊钓鱼合约:因为了解,所以上当 一文,以下是我的思考:

parent contact 的分析

通过这个交易改变了钓鱼合约中的状态变量:

因为被改变 Storage 的钓鱼合约代码中,状态变量一共有如下3个:

所以很明显推断出,Storage 布局为:

  • slot 0 –> question 字符串的长度
  • slot 1 –> responseHash
  • slot keccak256(bytes32(0)) –> question 字符串的部分
  • slot keccak256(bytes32(0))+1 –> question 字符串的部分
  • 注:mapping admin 很明显没有被赋值,因为如果赋值,位置应该在 keccak256(bytes32(key),bytes32(2))

通过监视 Start 方法的调用,发现字符串的内容没有被改变:

但是发现 responseHash 的值变了:

查看这个交易的内部交易:可以看到当前合约调用了钓鱼合约,改变了钓鱼合约的状态变量:

而钓鱼合约的 Start 调用本身没有改变任何状态变量,当然也没有改变 responseHash 的值。

所以猜测完整的钓鱼流程是这样:

-->tx.origin(0xaecf37061ff1ecc7cdb103489ce14b55d3702877)
     --> 触发 phishing contract 的 parent contract(0xf4e96e2d3b4e27853b5eabc5b0ddcc664f2b1ed1) 
        --> 触发 parent contract 的 call0 函数,触发内部交易 
            --> 触发 phishing contract(0x2ecC69bBd14Bb2bF974a901dA37370ca256fe07A) 
                --> 修改并预设了 question 和 responseHash 
                    --> 虚假使用EOA(0x72777BB81681Df0e634D3349885D9C52ae67F4b7)的调用 phishing contract 的 Start 方法 
                        --> 等待鱼儿上钩 

所以回头看简单逆向后的 parent contract 的合约代码(当然没有 publish),功能包括:(以下全是猜测)

  • 硬编码 0xaecf37061ff1ecc7cdb103489ce14b55d3702877,这才是真正的 admin
def storage:
  unknownb1d131bfAddress is addr at storage 0   

def unknownb1d131bf(): # not payable
  return unknownb1d131bfAddress
  • 验证 admin,自毁、以及调用钓鱼合约的 stop,转出钓到的钱
def kill(): # not payable
  require caller == unknownb1d131bfAddress
  selfdestruct(unknownb1d131bfAddress)

def _fallback() payable: # default function
  stop
  • 未知函数
def unknown9278a35a(addr _param1, uint256 _param2, array _param3) payable: 
  require caller == unknownb1d131bfAddress
  call _param1 with:
     value _param2 wei
       gas gas_remaining wei
      args _param3[all]
  require ext_call.success

def unknown5fc27f96(array _param1): # not payable
  mem[96] = _param1.length
  mem[128 len _param1.length] = _param1[all]
  mem[ceil32(_param1.length) + 128] = _param1.length
  if _param1.length:
      mem[ceil32(_param1.length) + 160 len 32 * _param1.length] = code.data * _param1.length]
  idx = 0
  while idx < _param1.length:
      require idx < _param1.length
      require _param1.length + -idx - 1 < mem[ceil32(_param1.length) + 128]
      mem[ceil32(_param1.length) + _param1.length + -idx + 159 len 8] = Mask(8, -(('mask_shl', 8, 0, 245, ('add', 1, ('mem', ('range', ('add', 128, ('var', 0)), 1)), ('mul', -1, ('cd', ('add', 4, ('param', '_param1')))), ('var', 0), ('mul', -1, 'gasprice'))), 0) + 256, 0) << (('mask_shl', 8, 0, 245, ('add', 1, ('mem', ('range', ('add', 128, ('var', 0)), 1)), ('mul', -1, ('cd', ('add', 4, ('param', '_param1')))), ('var', 0), ('mul', -1, 'gasprice'))), 0) - 256
      idx = idx + 1
      continue 
  mem[(2 * ceil32(_param1.length)) + 160] = 32
  mem[(2 * ceil32(_param1.length)) + 192] = mem[ceil32(_param1.length) + 128]
  mem[(2 * ceil32(_param1.length)) + 224 len ceil32(mem[ceil32(_param1.length) + 128])] = mem[ceil32(_param1.length) + 160 len ceil32(mem[ceil32(_param1.length) + 128])]
  if not mem[ceil32(_param1.length) + 128] % 32:
      return 32, mem[(2 * ceil32(_param1.length)) + 192 len mem[ceil32(_param1.length) + 128] + 32]
  mem[floor32(mem[ceil32(_param1.length) + 128]) + (2 * ceil32(_param1.length)) + 224] = mem[floor32(mem[ceil32(_param1.length) + 128]) + (2 * ceil32(_param1.length)) + -(mem[ceil32(_param1.length) + 128] % 32) + 256 len mem[ceil32(_param1.length) + 128] % 32]
  return 32, mem[(2 * ceil32(_param1.length)) + 192 len floor32(mem[ceil32(_param1.length) + 128]) + 64]
  • 修改 phishing contact 的 slot 的函数(主要是根据形参个数类型确定)
def unknown79f94f2b(addr _param1, uint256 _param2, array _param3) payable: 
  require caller == unknownb1d131bfAddress
  mem[128 len _param3.length] = _param3[all]
  mem[ceil32(_param3.length) + 128] = _param3.length
  if _param3.length:
      mem[ceil32(_param3.length) + 160 len 32 * _param3.length] = code.data * _param3.length]
  idx = 0
  while idx < _param3.length:
      require idx < _param3.length
      require _param3.length + -idx - 1 < mem[ceil32(_param3.length) + 128]
      mem[ceil32(_param3.length) + _param3.length + -idx + 159 len 8] = Mask(8, -(('mask_shl', 8, 0, 245, ('add', 1, ('mem', ('range', ('add', 128, ('var', 0)), 1)), ('mul', -1, ('cd', ('add', 4, ('param', '_param3')))), ('var', 0), ('mul', -1, 'gasprice'))), 0) + 256, 0) << (('mask_shl', 8, 0, 245, ('add', 1, ('mem', ('range', ('add', 128, ('var', 0)), 1)), ('mul', -1, ('cd', ('add', 4, ('param', '_param3')))), ('var', 0), ('mul', -1, 'gasprice'))), 0) - 256
      idx = idx + 1
      continue 
  mem[(2 * ceil32(_param3.length)) + 160 len ceil32(mem[ceil32(_param3.length) + 128])] = mem[ceil32(_param3.length) + 160 len ceil32(mem[ceil32(_param3.length) + 128])]
  if not mem[ceil32(_param3.length) + 128] % 32:
      call _param1.mem[(2 * ceil32(_param3.length)) + 160 len 4] with:
         value _param2 wei
           gas gas_remaining wei
          args mem[(2 * ceil32(_param3.length)) + 164 len mem[ceil32(_param3.length) + 128] - 4]
  else:
      mem[floor32(mem[ceil32(_param3.length) + 128]) + (2 * ceil32(_param3.length)) + 160] = mem[floor32(mem[ceil32(_param3.length) + 128]) + (2 * ceil32(_param3.length)) + -(mem[ceil32(_param3.length) + 128] % 32) + 192 len mem[ceil32(_param3.length) + 128] % 32]
      call _param1.mem[(2 * ceil32(_param3.length)) + 160 len 4] with:
         value _param2 wei
           gas gas_remaining wei
          args mem[(2 * ceil32(_param3.length)) + 164 len floor32(mem[ceil32(_param3.length) + 128]) + 28]
  require ext_call.success

相信以上分析已经可以大概说清楚这个骗局的原理了。

总结

遇到疑似 phishing contact 要注意看 State 的变化。这样可以避免被骗: