Coinhive挖矿脚本分析与Pool改造自建(二)


自己动手丰衣足食

本着一探究竟开源共享的精神,朝着拿回我的30%payback目标,我们已经详细分析了Coinhive挖矿脚本的构成、由来、运作方式,暂不提用户交互和兼容处理方式,我们先实现最核心的功能,构造一个属于自己的WebMoneroPool!

转化器思路构想

1. 从现有开源的矿池项目直接二次构造,搭建兼容WebSocket通信方式的完整Pool。

优点:从矿池整体可控,全面覆盖各项设置,100%赚取算力价值,矿池直接收取价值。(一般Pool由中心矿池打款到矿工需满足至少有0.1XMR,其中自动“税收”扣去矿池运营捐赠0.5-2%,平台开发捐赠0.1%)

缺点:服务器配置需求较高(≥2C4G),对于小流量站点或者前端产品变现转换率较低,运营赤字风险大。其次矿池更新迭代需从原项目升级并重新修改,容易出现不必要的烦恼……

2. 从头构造流量转换,用 Pool_Proxy 形式,对接转化WebSocket与PoolSocket,以中间件形式介入。

优点:只需构造中间件,便于维护。可以单独形成Log,对搭建平台要求无过高要求。

缺点:无法从头操控,无法避免部分定量捐赠和Pool平台“税收”。

从优缺点看,我们首要选择从 Pool_Proxy 中间件方式来构造前端挖矿的服务端,而后端Pool的选择空间就更大了,可以选择现有的公开矿池,也可以另外结合再自己搭建矿池。

稍安勿躁,本文将分别讲解自建中间件的过程以及标准矿池的搭建方式。

中间件deepMiner构造

为了简化开发流程,我使用比较熟悉的nodejs举例实现(其他语言按需实现均可)。

中间件制作,用于转化WebSocket流量与PoolSocket(TCP)流量,给双方充当“翻译”角色。

所以基础框架,先获得两边的接口,并使其能够正常对接,所以我们需要先写入如下内容到一个新建的 `server.js` 里:

  1. var http = require('http'),    //web承载
  2.     WebSocket = require("ws"), //WebSocket实现
  3.     net = require('net'),      //PoolSocket(TCP)实现
  4.     fs = require('fs');        //访问本地文件系统

我们先构造一个 `config.json` 文件用来设置构造所需的参数设定,比如域名地址,矿池地址,钱包地址,监听端口等:

  1. {
  2.     "lhost": "127.0.0.1",
  3.     "lport": 7777,
  4.     "domain": "miner.deepwn.com",
  5.     "pool": "pool.usxmrpool.com:3333",
  6.     "addr": "41ynfGBUDbGJYYzz2jgS***************************************************",
  7.     "pass": ""
  8. }

再继续往 `server.js` 里加入代码,读取配置文件并构造出大体框架。

先来一个web,确保外部访问正常:

  1. var conf = fs.readFileSync(__dirname + '/config.json', 'utf8');
  2. conf = JSON.parse(conf);
  3. // Http web
  4. var web = http.createServer((req, res) => {
  5.     res.setHeader('Access-Control-Allow-Origin', '*');
  6.     res.end('Pool Worked!'); // Change at Next...
  7.     }).listen(conf.lport, conf.lhost);
  8. // next codes here...

打开浏览器,从自己的127.0.0.1:7777已经能访问看到`Pool Worked!`页面。

接下来完成复用构造WebSocket服务:

为了方便书写,我们先声明一个叫做 `conn` 的对象来接管所有设置内容,为了方便后期调用。

其中囊括了`ws`服务,以及每次`ws`新连接所触发的`net.Socket()`,同时声明每个连接的 `pid` 来解决一个关于`Miner_banned`的坑……

回顾一下:上篇稿件中提到的JsonRPC里,首次login验证中的id,其实是一个Miner的身份区分。

  1. client >>
  2. {
  3.     "method": "login",
  4.     "params": {
  5.         "login": "********** [ Wallet Addr ] **********",
  6.         "pass": "",
  7.         "agent": "xmr-stak-cpu/1.3.0-1.5.0"
  8.     },
  9.     "id": 1 // <= 这个id 可以重复使用重复登录,但是过多为完成jobs,或者同时间id=1并发登录,将造成Miner被ban。
  10. }
  11. server <<
  12. {
  13.     "id": 1, // <= 登录的MinerID,不用IP区分应该是担心存在DHCP下的Miner出口IP相同,能够理解……
  14.     "jsonrpc": "2.0",
  15.     "error": null,
  16.     "result": {
  17.         "id": "811233385116793",
  18.         "job": {
  19.             "blob": "0606e498c5ce057326423f235dcd67dec07d9cb79e3506da8b35198e7debb40be3cbc2326c1999000000008bad7c9d5b78e9c9693903e817d20c09befe2c72ee6d20f297c0026d9a6e492406",
  20.             "job_id": "664084446453489",
  21.             "target": "711b0d00"
  22.         },
  23.         "status": "OK"
  24.     }
  25. }

这是pool用来区分单个IP不同miner的MinerID,如果一个IP里同一个Miner多次违约不完成Job并且重复登录申请新Job,将会进入banned模式,10分钟内无法获取新Jobs。

因为只是个demo,所以所有内容,全写在一个文件了,并没有进行区分和不同Socket线程单独控制,就全权交给`http`来内部控制sessions开启和销毁吧!我们继续接着构造:

  1. // Websocket 成功连接后,流程内部发出TCP_Socket连接Pool不单独控制TCP销毁
  2. var srv = new WebSocket.Server({
  3.     server: web, // 这里从web接管ws的操作
  4.     path: "/proxy", //可以加上path区分
  5.     maxPayload: 256
  6. });
  7. srv.on('connection', (ws) => { //当连接成功时,我们开始构造conn
  8.     var conn = {
  9.         uid: null, //为后期框架管理面板区分不同站点的UID
  10.         pid: new Date().getTime(), //解决踩坑……区分MinerID
  11.         workerId: null, //来自PoolJobs内job_id
  12.         found: 0,
  13.         accepted: 0,
  14.         ws: ws, //this ws
  15.         pl: new net.Socket(), //TCP Socket
  16.     }
  17.     var pool = conf.pool.split(':');
  18.     conn.pl.connect(pool[1], pool[0]); //使用新conn.pl对象,TCPSocket介入Pool
  19.     // on.('event') & some func here...
  20. });

我们可以 `nc -lvvp 8888` 本地监听,修改 `config.json` 里pool的地址为本地监听的接口,再通过浏览器构造WebSocket访问 `ws://127.0.0.1:7777` 来验证代码是否可以执行。

在一切顺利的情况下我们开始下一步,处理不同事件,现在可以将 `// on.('event') & some func here...` 改为:

  1. // Trans func here...
  2. conn.ws.on('message', (data) => {
  3.     ws2pool(data); // Trans WS2TCP
  4.     console.log('[>] Request: ' + conn.uid + '\n\n' + data + '\n');
  5. });
  6. conn.ws.on('error', (data) => {
  7.     console.log('[!] ' + conn.uid + ' WebSocket ' + data + '\n');
  8.     conn.pl.destroy();
  9. });
  10. conn.ws.on('close', () => {
  11.     console.log('[!] ' + conn.uid + ' offline.\n');
  12.     conn.pl.destroy();
  13. });
  14. conn.pl.on('data', (data) => {
  15.     pool2ws(data); // Trans TCP2WS
  16.     console.log('[<] Response: ' + conn.uid + '\n\n' + data + '\n');
  17. });
  18. conn.pl.on('error', (data) => {
  19.     console.log('[!] PoolSocket ' + data + '\n');
  20.     if (conn.ws.readyState !== 3) {
  21.         conn.ws.close();
  22.     }
  23. });
  24. conn.pl.on('close', () => {
  25.     console.log('[!] PoolSocket Closed.\n');
  26.     if (conn.ws.readyState !== 3) {
  27.         conn.ws.close();
  28.     }
  29. });

1. `conn.ws.on('event', [function])` 接管了在不同情况下对WebSocket的处理方式。

2. `conn.pl.on('event', [function])` 接管了对接Pool的不同处理方式。

那么我们还少了什么? 对,如何转换Socket流量才是核心内容,我们替换刚才 `// Trans func here...` 为如下,开始勾画核心——Socket转换的Functions:

查看第一篇文章提到的 `coinhive.min.js` 我们可以看到WebSocket主要有 `auth` / `submit` / ( `banned` ) 三个不同内容。

而从Socket_Dump中我们看到,PoolSocket里只有标准化的 `JsonRPC` ,所以我们需要转换出当前脚本能接受的 `authed` / `job` / `hash_accepted` / ( `error` ) 四种类型返回如上。

接下来,我们来解决静态资源问题,也是为什么我们要设置 `config.json` 中的域名等,我们需要动态替换所有静态文件里的域名为自己的服务器地址或个人域名,并提供 `cryptonight.wasm` 等其他资源访问,所以我们来修改第一段代码,其中构造的 `web` 实例里需要替换那个 “撒fufu的”页面,将 `res.end('Pool Worked!'); // Change at Next...` 替换为如下:

  1. req.url = (req.url === '/') ? '/index.html' : req.url;
  2.     fs.readFile(__dirname + '/web' + req.url, (err, buf) => {
  3.         if (err) {
  4.             fs.readFile(__dirname + '/web/404.html', (err, buf) => {
  5.                 res.end(buf);
  6.             });
  7.         } else {
  8.             if (!req.url.match(/\.wasm$/) && !req.url.match(/\.mem$/)) {
  9.                 buf = buf.toString().replace(/%deepMiner_domain%/g, conf.domain);
  10.             } else {
  11.                 res.setHeader('Content-Type', 'application/octet-stream');
  12.             }
  13.             res.end(buf);
  14.         }
  15.     });

将所需的web文件,放入web文件夹,其中lib文件夹放入 `cryptonight-asmjs.min.js` / `cryptonight-asmjs.min.js.mem` / `cryptonight.wasm` ,最终我们拥有了一个200行代码写出来的 `Pool_Proxy` 中间件!

deepMIner 项目实例

Repo: https://github.com/deepwn/deepMiner

Example: https://deepc.cc/demo.html

  1. deepMiner.git
  2. .
  3. |-- README.md
  4. |-- banner
  5. |-- config.json
  6. |-- package-lock.json
  7. |-- package.json
  8. |-- server.js
  9. |__ web
  10.     |-- 404.html
  11.     |-- deepMiner.js
  12.     |-- demo.html
  13.     |-- index.html
  14.     |-- lib
  15.     |   |-- cryptonight-asmjs.min.js
  16.     |   |-- cryptonight-asmjs.min.js.mem
  17.     |   |__ cryptonight.wasm
  18.     |__ worker.js

构建属于自己的Pool

(以下内容,可以跳过或者选取阅读。你可以直接在 `config.json` 里使用对外开放的公共矿池,也可以继续跟着本文,搭建自己的矿池。因为这个200行的Pool_Proxy已经可以完美地独立运转了!)

既然中间件有了,按道理我们可以直接使用,但还是想自行控制全部权限。

所以,不如再来一起搭建一个完全属于自己的矿池吧!

Github: https://github.com/zone117x/node-cryptonote-pool

Monero: https://getmonero.org

搭建Monero

首先,保证我们的服务器有 `1C1G` 的标准,因为本矿池并不对外,可以只在localhost运行,所以我们不需要太大的规格来容纳那么多连接。

但是为了确保万一别用着用着就宕了……我们还是先设置一下虚拟内存吧……

  1. dd if=/dev/zero of=/mnt/myswap.swap bs=1M count=4000
  2. mkswap /mnt/myswap.swap
  3. swapon /mnt/myswap.swap

再把设置出来的内存,挂载到系统里 `vi /etc/fstab` 并加入如下(保存退出: `:wq` )

  1. /mnt/myswap.swap none swap sw 0 0

接下来解决Pool的依赖问题:

  1. apt-get install build-essential libtool autotools-dev autoconf pkg-config libssl-dev
  2. apt-get install libboost-all-dev git libminiupnpc-dev redis-server
  3. add-apt-repository ppa:bitcoin/bitcoin
  4. apt-get update
  5. apt-get install libdb4.8-dev libdb4.8++-dev

麻烦的事情总是要来的,我们需要得到完整版区块链信息来完成交易和任务发布,所以需要构建可信的 `monerod` 本地进程。

具体Monero版本如果更新了,可以去官网下载布置,本文目前以0.11.0.0版本介绍。(见上方链接)

  1. cd
  2. mkdir monero
  3. cd monero
  4. wget https://downloads.getmonero.org/cli/monero-linux-x64-v0.11.0.0.tar.bz2
  5. tar -xjvf monero-linux-x64-v0.11.0.0.tar.bz2

然后运行 `./monerod` 开始长达3-6小时的下载区块链信息和验证完整性……

当然,官网也介绍了一个更方便的方式,直接手动下载 `raw` 文件并导入验证。

  1. // 下载并验证导入
  2. wget -c --progress=bar https://downloads.getmonero.org/blockchain.raw
  3. ./monero-blockchain-import --verify 0 --input-file ./blockchain.raw
  4. // 验证完之后就可以移除辣~
  5. rm -rf ./blockchain.raw
  6. // 继续开启demon并后台运行
  7. ./monerod --detach

搭建Pool

可以参见官方文档:https://github.com/zone117x/node-cryptonote-pool#1-downloading--installing

之前已经下载了 `nodejs` 所以不做重复下载。我们还需要 `Redis` 解决Pool的数据库问题。

关于Redis安装,一搜一大堆,不再啰嗦。

请注意:切记设置Redis为本地服务,不要对外开放,可以自行设置密码。
参见:https://redis.io/topics/security

搭建完Redis数据库,我们来从github下载最新的源码,着手布置Pool。

  1. cd /srv
  2. git clone https://github.com/zone117x/node-cryptonote-pool.git pool
  3. cd pool
  4. npm update

等npm更新下载完毕,我们来进行配置

  1. cp config_example.json config.json
  2. vi config.json

配置信息如下:

最后在终端键入 `node init.js` 让Pool开始工作, 也可以用比如 `node init.js -module=api` 只开启单独项目。

当然你也可以用 `forever start /srv/pool/init.js` 确保出错了还能在线,也可以将其写入开机启动项里。不过值得注意的是:切记要把Pool的开启,放在Pool_Proxy开启之前哦!

如此一来,Pool + Pool_Proxy 就完成了,请开始你的表演吧~

Pool 相关项目

https://github.com/zone117x/node-cryptonote-pool

https://github.com/CanadianRepublican/monero-universal-pool

https://github.com/search?utf8=%E2%9C%93&q=monero+pool

发展构想

其实前端算力,不仅仅可以用来挖矿,更可以用来做机器人验证,构造一种算法,或者换一种token方式,利用硬算力来增加批量化成本,同时通过算力难度,来增加单次的硬计算时间成本,我想在验证码应用上将有更好的发展。

同时 `asmjs` 和 `WebAssembly` 的出现,也将前端的处理能力提升到一个新的台阶,今后通过浏览器构造本地应用?创建新形式的3D页游?甚至加入P2P将应用分发到用户?新的加密传输?大众化的自定义加密算法?

正如Coinhive现在的验证码雏形,市场前景十分美好,通过基本判断,逐步增加重复提交表单的计算成本,来杜绝撸羊毛,通过不断的更新与进步,前端的魅力,正在逐步散发。

比如EtherDream的文章推荐给大家:

使用浏览器的计算力,对抗密码破解:https://www.cnblogs.com/index-html/p/frontend_kdf.html

无形验证码 —— PoW 算力验证:https://www.cnblogs.com/index-html/p/web-pow-research.html

怎样的 Hash 算法能对抗硬件破解:https://www.cnblogs.com/index-html/p/hardware-resistant-hash-algorithm.html

:) 哈哈哈

一入前端深似海,猥琐不枉此生学。

熊迪,跟我来学做菜吧2333……