作者:sunsama@知道创宇404区块链安全研究团队

背景介绍

为了迎合以太坊区块链[1]发展需求,Microsoft Azure[2]早在2016年9月九推出了以太坊节点走自动部署的模块。部署情况如下:

登陆Microsoft Azure:

部署Ethereum Proof-of-Work Consortium:

访问建立的“ADMIN-SITE”可以看到一个“Blockchain Admin”界面:

我们注意到这个管理接口提供了一个“转账”功能并且整个页面缺少鉴权机制任何人都可以访问,这样就导致恶意攻击者可以通过该接口提交钱包地址和转账数量进行转账。

Web3.js 是⼀个兼容了以太坊核心功能的JavaScript库[3],很多以太坊客户端及DApp都是通过调用Web3.js的API接⼝来实现。 以太坊客户端开发库主要是提供了两种类型的API接口:RPC(Remote Procedure Call)及IPC(Inter-process Communications),在以往的攻击事件里很多关注点都在RPC接口上,而很少关注IPC接口,在本文的涉及“Blockchain Admin”的问题就发生在IPC接口上,由此下面做了详细的代码分析:

代码分析

在分析之前我们先介绍下PRC及IPC接口区别:

IPC与RPC简介

IPC(Inter-process Communications)进程间通信,是指在不同进程之间传播或交换信息,IPC的方式通常有管道、消息队列、信号量、共享存储、Socket、Stream等。对于geth来说IPC的方式更为高效,在安装geth之后 IPC socket不会自动创建,并且他也不是一个永久的资源,只有在启动geth时才会创建一个IPC Socket。

有以下几个参数可以在启动geth时配置IPC相关服务,其他参数可以使用geth —help查看。

--ipcdisable                            Disable the IPC-RPC server
--ipcapi "admin,eth,debug,miner,net,shh,txpool,personal,web3"   API's offered over the IPC-RPC interface
--ipcpath "geth.ipc"                        Filename for IPC socket/pipe within the datadir (explicit paths escape it)

在geth启动时使用 --ipcpath来指定一个IPC路径,会有一段信息指明IPC的相关信息。例如

IPC endpoint opened: /Users/username/Library/Ethereum/geth.ipc

Web3.js中提供了使用IPC通信的方法。

// Using the IPC provider in node.js
var net = require('net');
var web3 = new Web3('/Users/myuser/Library/Ethereum/geth.ipc', net); // mac os path
// or
var web3 = new Web3(new Web3.providers.IpcProvider('/Users/myuser/Library/Ethereum/geth.ipc', net)); // mac os path
// on windows the path is: "\\\\.\\pipe\\geth.ipc"
// on linux the path is: "/users/myuser/.ethereum/geth.ipc"

node_modules/web3/lib/web3/ipcprovider.js

var IpcProvider = function (path, net) {
    var _this = this;
    this.responseCallbacks = {};
    this.path = path;

    this.connection = net.connect({path: this.path});
    ...............
};

https://github.com/ethereum/go-ethereum/wiki/Management-APIs中给出了在命令行使用IPC的例子

RPC(Remote Procedure Call)远程过程调用,指通过网络从远程计算机的程序上请求服务。geth为RPC提供了两种方法,分别是HTTP JSON RPC API(默认8545端口)和WebSocket JSON RPC API(默认8546端口)。

在命令行中可以使用以下参数配置RPC服务。

--rpc                       启用HTTP-RPC服务器
--rpcaddr value             HTTP-RPC服务器接口地址(默认值:“localhost”)
--rpcport value             HTTP-RPC服务器监听端口(默认值:8545)
--rpcapi value              基于HTTP-RPC接口提供的API
WebSocket
--ws                        启用WS-RPC服务器
--wsaddr value              WS-RPC服务器监听接口地址(默认值:“localhost”)
--wsport value              WS-RPC服务器监听端口(默认值:8546)
--wsapi  value              基于WS-RPC的接口提供的API
--wsorigins value           websockets请求允许的源

同样的在Web3.js中也提供了使用RPC的方法。

Http Api
var Web3 = require('web3');
var web3 = new Web3('http://localhost:8545');
// or
var web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'));

WebSocket Api
// change provider
web3.setProvider('ws://localhost:8546');
// or
web3.setProvider(new Web3.providers.WebsocketProvider('ws://localhost:8546'));
/**
 * HttpProvider should be used to send rpc calls over http
 */
var HttpProvider = function (host, timeout) {
    this.host = host || 'http://localhost:8545';
    this.timeout = timeout || 0;
};

以太坊黑色情人节事件中,攻击者就是利用了RPC接口进行恶意转账。

流程分析

我们在Blockchain Admin页面的两个输入框中输入转账地址和转账数量并提交。

/home/ethtest/etheradmin/app.js定义了提交后服务器处理的方法。

命令行中的参数
var listenPort = process.argv[2]
var gethIPCPath = process.argv[3];
var coinbase = process.argv[4];
var coinbasePw = process.argv[5];
var consortiumId = process.argv[6];
var registrarHostEndpoint = process.argv[7];
var registrarConnectionString = process.argv[8];
var registrarDatatbaseId = process.argv[9];
var registrarCollectionId = process.argv[10];

定义了使用IPC服务
var web3IPC = new Web3(new Web3.providers.IpcProvider(gethIPCPath, require('net')));

            ··············

app.post('/', function(req, res) {
  var address = req.body.etherAddress;//转账地址
  var amount = req.body.amount;//转账数量

  if(web3IPC.isAddress(address)) {
      //如果提交的地址是以太坊地址则解锁账号
    web3IPC.personal.unlockAccount(coinbase, coinbasePw, function(err, res) {
      console.log(res);
        //通过ipc方法发送一笔交易
      web3IPC.eth.sendTransaction({from: coinbase, to: address, value: web3IPC.toWei(amount, 'ether')}, function(err, res){ console.log(address)});
    });

    req.session.isSent = true;
  } else {
    req.session.error = "Not a valid Ethereum address";
  }

  res.redirect('/');
});

使用POST方法提交后,会判断我们输入的地址是否是合法的以太坊地址。默认情况下我们的账号是处于锁定状态的,这里判断地址正确后使用personl.unlockAccount()方法解锁账号。该方法需要的参数coinbase和coinbasePw在启动服务时已经在命令行中作为参数传递过来了,使用ps命令查看该服务的进程。

其中f9cdc590071d9993b198b08694e5edf376979ce6是我们的钱包地址,123qweasdZXC是解锁钱包需要的密码,/home/ethtest/.ethereum/geth.ipcgetIPCPath参数的内容。

personal.js中的unlockAccount方法。

    var unlockAccount = new Method({
        name: 'unlockAccount',
        call: 'personal_unlockAccount',
        params: 3,
        inputFormatter: [formatters.inputAddressFormatter, null, null]
    });

IpcProvider.js中对发送方法的定义。

IpcProvider.prototype.send = function (payload) {

    if(this.connection.writeSync) {
        var result;

        // try reconnect, when connection is gone
        if(!this.connection.writable)
            this.connection.connect({path: this.path});

        var data = this.connection.writeSync(JSON.stringify(payload));

        try {
            result = JSON.parse(data);
        } catch(e) {
            throw errors.InvalidResponse(data);                
        }

        return result;

    } else {
        throw new Error('You tried to send "'+ payload.method +'" synchronously. Synchronous requests are not supported by the IPC provider.');
    }
};

ipcprovider会调用JSONRPC.js将unlockAccount方法中的参数格式化为JSON格式。

在node_modules/web3/lib/web3/ipcprovider.js中下断点跟踪一下数据流。

然后将数据通过socket写入。

接下来geth通过IPC接收到了请求的方法和参数,然后使用UnlockAccount函数进行账户解锁,解锁账户后使⽤eth.sendTransaction⽅法发送交易。

sendTransaction方法会使用已经解锁后的本地账户的私钥进行签名,并使用SignedTransaction方法进行发送签名后的交易。

我们通过geth日志获取交易hash,在console中查看详细信息。

值得一提的是:在我们分析过程发现通过Microsoft Azure提供的以太坊节点自动化部署方案仍然使用的1.7.3版本的geth ⽽这个版本里UnlockAccount函数:

func (s *PrivateAccountAPI) UnlockAccount(addr common.Address, password string, duration *uint64) (bool, error) {

    const max = uint64(time.Duration(math.MaxInt64) / time.Second)

    var d time.Duration

    if duration == nil {

        d = 300 * time.Second

    } else if *duration > max {

        return false, errors.New("unlock duration too large")

    } else {

        d = time.Duration(*duration) * time.Second

    }

    err := fetchKeystore(s.am).TimedUnlock(accounts.Account{Address: addr}, password, d)

    return err == nil, err

}

wiki中对personal_unlockAccount方法的定义:

从keystore中解锁账户并获得私钥,并把已经解锁的私钥放到内存中。解锁账户的api允许传入超时时间,默认超时为300秒,如果传⼊入的超时时间为0,则是永久不不会超时,账户⼀直处于解锁状态,直到节点进程退出。这也是“以太坊【偷渡】漏洞事件[5]”发生的主要原因。

风险评估

在以往的关于以太坊攻击案例里更多的是发生在暴露在互联网的RPC接口上,⽽基于本地进程通讯的IPC接口 被认为是相对安全可靠的,但是如果类似于Microsoft Azure提供的以太坊节点⾃动化部署⽅案里 的“Blockchain Admin”基于IPC调⽤程序,本身没有任何认证直接暴露在互联网上无疑是巨大的安全风险。(注:通过ZoomEye⽹路空间搜索引擎[7]可以看到曾经暴露在互联网上的目标。)

在实际测试分析过程发现使用Microsoft Azure提供的以太坊节点自动化部署方案更多的是联盟链或私有链,部署共有链的情况较少,所以这个安全事件实际可能给共有链的带来的影响相对不大。对于联盟链或私有链的影响需要根据其本身的情况去衡量量评估。

报告流程

针对以上问题我们第一时间联系了微软:

总结

区块链虚拟货币安全事件频发,安全刻不不容。通过这次的案例可以得几点建议:


针对目前主流的以太坊应用,知道创宇提供专业权威的智能合约审计服务,规避因合约安全问题导致的财产损失,为各类以太坊应用安全保驾护航。

知道创宇404智能合约安全审计团队: https://www.scanv.com/lca/index.html
联系电话:(086) 136 8133 5016(沈经理,工作日:10:00-18:00)

欢迎扫码咨询:

参考

[1] https://baike.baidu.com/item/%E4%BB%A5%E5%A4%AA%E5%9D%8A/20865117?fr=aladdin
[2] https://azure.microsoft.com/en-us/
[3] https://github.com/ethereum/web3.js/
[4] https://github.com/ethereum/go-ethereum/wiki/Management-APIs
[5] https://paper.seebug.org/547/
[6] https://mp.weixin.qq.com/s/Kk2lsoQ1679Gda56Ec-zJg
[7] https://www.zoomeye.org/searchResult?q=%22Blockchain%20Admin%22


源链接

Hacking more

...