虽然“前端挖矿脚本”是近一个月才从业内报告中进入公众视野的词汇,但在“一棒子打死”型各大媒体站的“详细分析”下,我看到满是“黑客恶意植入”、“占用CPU”、“非法牟利”等负面评价的所谓“分析”和传说中的可笑“查杀检查方法”,鲜有对此技术从各层面分析和冷静判断的文章或者新闻,这么好一个转换流量变现,或者说用来进行机器人验证的“硬算力”方式,却被直接贴上“非法”标签,对于做技术和饱含极客精神的人们,真是不友好。。
于是你现在看到的,就是我这个“为前端技术和匿名算法叫冤”的“和事老”,从一个更有趣的角度,以较为通俗的语言、尽量全面的解析、外加合理改造和白利用姿势,用来“科普”的一篇赚稿费的“水文”。
区块链:分布式数据存储、P2P传输、共识机制、加密算法等计算机技术的新型应用模式。其中共识机制指区块链系统中实现不同节点之间建立信任、获取权益的数学算法。(理解为你工作的绩效评定方法)
钱包:用来记录个人用户电子货币资产、操作交易、用于收发货币的一个"hash"地址。(理解为汇款地址或者你的工资卡吧)
矿池:相关电子货币根据区块链,同一分配“生产”任务的发布平台,同时也是算力效验的对接平台。(理解为你的工作单位)
挖矿:此处指利用设备算力按特定电子货币生产计算方法和矿池分配方式产生的经济价值。(理解为努力工作的这个行为)
前端挖矿:从挖矿技术来说,是没有前后端的,此处前端也是指的浏览器前端,而本文前端挖矿,就是说“浏览器内实现、通过用户访问实施挖矿”的一种方式。(理解成工作偷懒让别人干活的方式吧 ⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄ )
(抱歉如果文章里还有不懂却未提到的请自行网上查询)
在进入正题的时候我们必须理解一个概念,那就是正常挖矿和前端挖矿的区别,技术,以及意义。
金镐子银镐子,挖矿方便才是好镐子! :)
区别:
一般挖矿:用户以主动方式,直接使用C或者其他语言构造的miner客户端进行CPU或GPU计算,得到算力价值。
前端挖矿:用户以被动(或主动)方式,在不知情(或知情)情况下,由浏览者产生的CPU或GPU计算,得到算力价值。
技术:
一般挖矿:利用CPU或GPU本身的高速浮点计算能力,进行挖矿工作。而一般挖矿选取有更优秀浮点计算能力的GPU进行操作。
前端挖矿:利用asm.js或webAssembly前端解析器中介,在浏览器端被动使用用户的CPU完成挖矿,或者利用Html5新规范WebGL利用浏览器完成GPU挖矿操作。
意义:
一般挖矿:从行为和实现方式看,是一种主动在自己设备完成的挖矿行为(排除非法控制他人设备情况),c实现的工具直接使用OS的底层驱动和接口,无论操作内存、CPU或GPU,流程损耗低计算速度更快。
前端挖矿:从行为和实现方式看,可以是一种以被动方式在其他用户浏览到某网页情况下,在非己设备上发生的挖矿行为,通过将算力任务分担给其他浏览用户,来实现“分布式”计算(其实更应该叫,利用多设备优势,提高单一时间内,单目标多任务并发能力)
关键字: WebSocket
/ worker
/ asm.js
/ WebAssembly
传送门:coinhive.min.js
https://coinhive.com/lib/
.
|—— coinhive.min.js // wss矿池通信,分配worker,交互实现
| :.. (CoinHive.CONFIG.WEBSOCKET_SHARDS) // WebSocket地址,连接pool进行任务发布等功能
| :.. (CoinHive.CRYPTONIGHT_WORKER_BLOB) // 以CreateURL形式动态生成Blob链接,对worker内容实现
|—— cryptonight.wasm // WebAssembly实现,预计源cryptonight.c & keccak.c 等
|—— cryptonight-asmjs.min.js // 兼容方案asmjs实现,内容worker除外从L4851算法开始
|__ cryptonight-asmjs.min.js.mem // 浏览器中asmjs所需模拟内存实现
通过阅读脚本,可以在Blob构造的worker实例(下文中称worker.js)看到如下对运行平台的判断:
worker.js ( L21 .. L50 )
var Module;
if (!Module) Module = (typeof Module !== "undefined" ? Module : null) || {};
var moduleOverrides = {};
for (var key in Module) {
if (Module.hasOwnProperty(key)) {
moduleOverrides[key] = Module[key]
}
}
var ENVIRONMENT_IS_WEB = false;
var ENVIRONMENT_IS_WORKER = false;
var ENVIRONMENT_IS_NODE = false;
var ENVIRONMENT_IS_SHELL = false;
if (Module["ENVIRONMENT"]) {
if (Module["ENVIRONMENT"] === "WEB") {
ENVIRONMENT_IS_WEB = true
} else if (Module["ENVIRONMENT"] === "WORKER") {
ENVIRONMENT_IS_WORKER = true
} else if (Module["ENVIRONMENT"] === "NODE") {
ENVIRONMENT_IS_NODE = true
} else if (Module["ENVIRONMENT"] === "SHELL") {
ENVIRONMENT_IS_SHELL = true
} else {
throw new Error("The provided Module['ENVIRONMENT'] value is not valid. It must be one of: WEB|WORKER|NODE|SHELL.")
}
} else {
ENVIRONMENT_IS_WEB = typeof window === "object";
ENVIRONMENT_IS_WORKER = typeof importScripts === "function";
ENVIRONMENT_IS_NODE = typeof process === "object" && typeof require === "function" && !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_WORKER;
ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER
}
看来其实coinhive支持包括本地挖矿的其他兼容方式,野心不小呀。
再来看实现过程对兼容性的处理:
worker.js ( L1169 .. L1179 )
function integrateWasmJS(Module) {
var method = Module["wasmJSMethod"] || "native-wasm";
Module["wasmJSMethod"] = method;
var wasmTextFile = Module["wasmTextFile"] || "cryptonight.wast";
var wasmBinaryFile = Module["wasmBinaryFile"] || "cryptonight.wasm";
var asmjsCodeFile = Module["asmjsCodeFile"] || "cryptonight.temp.asm.js";
if (typeof Module["locateFile"] === "function") {
wasmTextFile = Module["locateFile"](wasmTextFile);
wasmBinaryFile = Module["locateFile"](wasmBinaryFile);
asmjsCodeFile = Module["locateFile"](asmjsCodeFile)
}
从容地概括本地挖矿和浏览器内前端挖矿,并对不同浏览器对WebAssembly的支持情况进行兼容。
cryptonight.wasm & cryptonight-asmjs.min.js
我知道怎么从file.c转化成file.wasm或者file.asm.js,但是你让我挑出来给你分析这俩文件的内容?
你自己去下下来看看你就知道为什么本文不做分析了…… :)
但是如果非要问,我可以告诉你文件主要就是cryptonight算法的实现,和一般Miner并无两样,只是参数传入传出方式由前端方式二次构造。
目测来源项目:ccminer 或 stak CPU miner
example1: https://github.com/tsiv/ccminer-cryptonight/tree/master/crypto
example2: https://github.com/jquesnelle/xmr-stak-cpu/tree/master/crypto
对于worker队列内的一些实现,你也可以跟我一起过一遍他们的代码,目前看来虽然并未提起开源,但也还不是webpack打包的js,函数名没有替换流程也相对清晰,一步步看下来你也能说出个大概。
听人千言不如亲眼所见,一定要养成强迫自己阅读别人代码,并记录他人书写中优美片段的好习惯。
如果你还非得要我分析算法构成?真巧,我研究的时候顺便翻译了一下CryptoNight的白皮书,Mark在Freebuf今后自己回头来学习。
CryptoNote Standards 介绍了一种点对点的匿名支付系统,本文档是 CryptoNote Standards 的一部分,定义了 CryptoNote 的缺省工作量证明散列函数,CryptoNight。
版权所有(c)2013 CryptoNote。 本文档可在知识共享署名3.0许可证(国际)许可权限范围内查询。
许可副本 :http://creativecommons.org/licenses/by/3.0/
1. 算法介绍
2. 名词解释
3. 初始化流程
4. 内存处理流程
5. 结果计算流程
6. 参考内容
CryptoNight是一个使用物理内存的高强度hash算法函数。 它的设计适用于GPU,FPGA和ASIC架构上的有效性算力。流程的第一步是初始化大型暂存器与伪随机数据;下一步是算法对暂存器中包含伪随机地址的大量的读/写计算操作;最后一步是将整个暂存器的hash值进行hash效验,验证本次计算产生的价值。
hash函数:映射数据的有效计算函数,对于固定大小的数据、构造特定算法行为,产生类似随机数结果
暂存器:在算法过程中,申请用于存储计算时中间值的部分内存
首先,使用参数 b = 1600
和 c = 512
.对输入内容进行Keccak计算 [ KECCAK ]。计算结果的0..31字节用作AES-256密钥[AES],并扩展为10个循环密钥;申请一个分配了2097152字节(2 MB)空间的暂存器;从计算结果的64..191字节处,提取出来数据并分割成8个块,每个块16字节。使用以下步骤对每个块进行加密:
for i = 0..9 do:
block = aes_round(block, round_keys[i])
aes_round()
函数执行一轮AES加密,对本块执行 SubBytes
,ShiftRows
和 MixColumns
步骤,其结果与round_key进行异或运算。但这不同于AES加密算法,第一轮计算和最后一轮计算没什么不同。
一轮下来得到的计算结果,写入暂存器的前128个字节,然后这些结果再次代入加密循环,再把这次循环结果写入暂存器的第二个128字节里。这里每次往暂存器里写入下一个128字节,都代表对先前写入的128字节内容在新一轮加密的结果。流程一直循环,直到暂存器写满。至此,一次算法的初始化就完成了。
该图表示暂存器初始化:
+-----+
|Input|
+-----+
|
V
+--------+
| Keccak |
+--------+
|
V
+-------------------------------------------------------------+
| Final state |
+-------------+--------------+---------------+----------------+
| Bytes 0..31 | Bytes 32..63 | Bytes 64..191 | Bytes 192..199 |
+-------------+--------------+---------------+----------------+
| |
V |
+-------------+ V
| Round key 0 |------------+---+->+-----+
+-------------+ | | | |
| . | | | | |
| . | | | | AES |
| . | | | | |
+-------------+ | | | |
| Round key 9 |----------+-|-+-|->+-----+ +---+
+-------------+ | | | | | | |
| | | | +------------------->| |
| | | | | | |
| | | | V | |
| | | +->+-----+ | |
| | | | | | S |
| | | | | | |
| | | | AES | | c |
| | | | | | |
| | | | | | r |
| | +--->+-----+ | |
| | | | a |
| | +------------------->| |
| | . | t |
| | . | |
| | . | c |
| | +------------------->| |
| | | | h |
| | V | |
| +----->+-----+ | p |
| | | | |
| | | | a |
| | AES | | |
| | | | d |
| | | | |
+------->+-----+ | |
| | |
+------------------->| |
| |
+---+
在主循环之前,对输入内容进行Keccak计算后取0..31字节和32..63字节进行异或,所得到的32字节结果用于初始化,变量a和b分别各占16字节,这两个变量将用于主循环,主循环进行524,288次迭代,当一个16字节值需要转换成暂存器中的一个地址,将以低字节顺序压入内存,21位低字节用作索引,但是索引中的4个低字节将被清除,以确保地址索引统一16字节对齐。 数据从16字节块中读取并写入暂存器。
迭代流程伪代码:
scratchpad_address = to_scratchpad_address(a)
scratchpad[scratchpad_address] = aes_round(scratchpad [scratchpad_address], a)
b, scratchpad[scratchpad_address] = scratchpad[scratchpad_address],
b xor scratchpad[scratchpad_address]
scratchpad_address = to_scratchpad_address(b)
a = 8byte_add(a, 8byte_mul(b, scratchpad[scratchpad_address]))
a, scratchpad[scratchpad_address] = a xor scratchpad[scratchpad_address], a
8byte_add()
函数将每个参数表示为一对64位低位值,并将它们组合在一起,以分量形式进行快速模除 2 ^ 64。 其结果返回16字节。
8byte_mul()
函数仅使用每个参数的前8个字节,并分别解析为无符号64位低位字节整数,并相乘。 其结果返回16字节,最后,结果分半,两边的8字节相互交换。
内存处理流程图:
+-------------------------------------------------------------+
| Final state |
+-------------+--------------+---------------+----------------+
| Bytes 0..31 | Bytes 32..63 | Bytes 64..191 | Bytes 192..199 |
+-------------+--------------+---------------+----------------+
| |
| +-----+ |
+-->| XOR |<--+
+-----+
| |
+----+ +----+
| |
V V
+---+ +---+
| a | | b |
+---+ +---+
| |
--------------------- REPEAT 524288 TIMES ---------------------
| | address +---+
+-------------|----------------------------------->| |
| +-----+ | read | |
+-->| AES |<--|------------------------------------| |
| +-----+ V | |
| | +-----+ | S |
| +-->| XOR | | |
| | +-----+ write | c |
| | | +------------------------------>| |
| | +----+ address | r |
| +------------------------------------------>| |
| | +-----------+ read | a |
| +->| 8byte_mul |<--+------------------------| |
| | +-----------+ | | t |
| | | | | |
| | V | | c |
| | +-----------+ | | |
+------|->| 8byte_add | | | h |
| +-----------+ | | |
| | | write | p |
| +---------|----------------------->| |
| | | | a |
| V | | |
| +-----+ | | d |
| | XOR |<-----+ | |
| +-----+ | |
+------+ | | |
+-------------|-+ | |
| | +---+
-------------------------- END REPEAT -------------------------
| |
在内存操作完成之后,使用与第一步骤(初始化步骤)分相同的方式,进行Keccak计算,最终结果的32..63字节被扩展成10个AES循环密钥。
从Keccak结果里提取64..191字节,并与暂存器里前128个字节进行异或运算。然后结果以与第一步(初始化步骤)使用相同的方式进行加密,但是使用新的密钥。结果继续与暂存器中的第二个128个字节进行异或运算,依次循环加密迭代。
在暂存器的最后128个字节进行异或运算后,就是流程的最后一次加密,完成后将原本Keccak结果的64..191字节内容,替换为本次加密内容。然后,以b = 1600对整个块内容进行Keccak-f(Keccak排列)。
然后,结果中第一个字节的2个低位比特,用于进行散列函数运算:0 = BLAKE-256 [BLAKE],1 = Groestl-256 [GROESTL],2 = JH-256 [JH] 3 = Skein-256 [SKEIN]
。最后将所选的散列函数应用于Keccak最终结果,生成的散列就是CryptoNight算法的计算输出。
结果计算流程图:
+-------------------------------------------------------------+
| Final state |
+-------------+--------------+---------------+----------------+
| Bytes 0..31 | Bytes 32..63 | Bytes 64..191 | Bytes 192..199 |
+-------------+--------------+---------------+----------------+
| | | |
| +--------+ | |
| V | | |
|+-------------+ | | |
|| Round key 0 |-|---+---+ | |
|+-------------+ | | | | |
|| . | | | | | |
|| . | | | | | |
|| . | | | | | |
|+-------------+ | | | | |
+---+ || Round key 9 |-|-+-|-+ | V |
| | |+-------------+ | | | | | +-----+ |
| |-|----------------|-|-|-|-|->| XOR | |
| | | | | | | | +-----+ |
| S | | | | | | | | |
| | | | | | | | V |
| c | | | | | | +->+-----+ |
| | | | | | | | | |
| r | | | | | | | | |
| | | | | | | | AES | |
| a | | | | | | | | |
| | | | | | | | | |
| t | | | | | +--->+-----+ |
| | | | | | | |
| c | | | | | V |
| | | | | | +-----+ |
| h |-|----------------|-|-|----->| XOR | |
| | | | | | +-----+ |
| p | | | | | | |
| | | | | | . |
| a | | | | | . |
| | | | | | . |
| d | | | | | | |
| | | | | | V |
| | | | | | +-----+ |
| |-|----------------|-|-|----->| XOR | |
| | | | | | +-----+ |
+---+ | | | | | |
| | | | V |
| | | +----->+-----+ |
| | | | | |
| | | | | |
| | | | AES | |
| | | | | |
| | | | | |
| | +------->+-----+ |
| | | |
V V V V
+-------------+--------------+---------------+----------------+
| Bytes 0..31 | Bytes 32..63 | Bytes 64..191 | Bytes 192..199 |
+-------------+--------------+---------------+----------------+
| Modified state |
+-------------------------------------------------------------+
|
V
+----------+
| Keccak-f |
+----------+
| |
+-----------+ |
| |
V V
+-------------+ +-------------+
| Select hash |->| Chosen hash |
+-------------+ +-------------+
|
V
+--------------+
| Final result |
+--------------+
计算结果举例:
Empty string:
eb14e8a833fac6fe9a43b57b336789c46ffe93f2868452240720607b14387e11.
"This is a test":
a084f01d1437a09c6985401b60d43554ae105802c5f5d8a9b3253649c0be6605.
白皮书里头涉及的算法在example里两个Miner项目中都有c实现。
可见此Coinhive脚本中XMR挖矿算法(cryptonight)就是靠一下任意一个形式实现的对C书写的算法的封装了。
wasmBinaryFile => (cryptonight.wasm)
asmjsCodeFile => (cryptonight-asmjs.min.js)
asm.js
和 WebAssembly
如何将C代码转为可执行的javascript文件,抱歉这一下子根本就讲不清楚,所以大家只需要知道这两项技术旨在提升浏览器内执行效率的类似“汇编”方式的实现快速运算的解决方案就可以了。
脚本里使用它,其一,目的是解决javascript内浮点运算等在弱类语言难以高效率执行的问题。其二,两个框架将已有的成型miner所用算法实例c语言脚本直接转换成js模拟的脚本,大大缩短了开发周期。可谓两全其美。
可供转换使用的工具有 Emscripten 、 binaryen 、 WABT 等。
两个框架的介绍就不详细谢了,具体DOC和介绍按需去他们官网搜吧 :)
简评一下两者区别:
asm.js 框架
可以由手写方式字符串书写,拥有极大的可读性,用于计算类的实现,性能也达到同样C脚本2/3效率。
WebAssembly 框架
而是以由c语言脚本书写,再通过编译成中间件file.wasm的Buffer形式实现,因此执行更加高效。
要我去翻看Miner的源码,我是懒得一段段跟进去看了……所以我们直接抓包看看client与server到底在说些什么吧。
PoolSocket:
client >>
{
"method": "login",
"params": {
"login": "********** [ Wallet Addr ] **********",
"pass": "",
"agent": "xmr-stak-cpu/1.3.0-1.5.0"
},
"id": 1
}
server <<
{
"id": 1,
"jsonrpc": "2.0",
"error": null,
"result": {
"id": "811233385116793",
"job": {
"blob": "0606e498c5ce057326423f235dcd67dec07d9cb79e3506da8b35198e7debb40be3cbc2326c1999000000008bad7c9d5b78e9c9693903e817d20c09befe2c72ee6d20f297c0026d9a6e492406",
"job_id": "664084446453489",
"target": "711b0d00"
},
"status": "OK"
}
}
client >>
{
"method": "submit",
"params": {
"id": "811233385116793",
"job_id": "664084446453489",
"nonce": "11018000",
"result": "0e9b264a2a225cbbdc520b1fbef207a12e092c23325b7667c27bd0d95a590c00"
},
"id": 1
}
server <<
{
"id": 1,
"jsonrpc": "2.0",
"error": null,
"result": {
"status": "OK"
}
}
server <<
{
"jsonrpc": "2.0",
"method": "job",
"params": {
"blob": "0606e498c5ce057326423f235dcd67dec07d9cb79e3506da8b35198e7debb40be3cbc2326c199900000000253addee86a405a4553dee713dabb28bd5cbb5a6556ddb332ad46ffdcf0c073a06",
"job_id": "183626817283220",
"target": "58dd0700"
}
}
.
.
.
WebSocket:
web >>
{
"type": "auth",
"params": {
"site_key": "******* [ siteKey ] *******",
"type": "anonymous",
"user": null,
"goal": 0
}
}
srv <<
{
"type": "authed",
"params": {
"token": "",
"hashes": 0
}
}
{
"type": "job",
"params": {
"job_id": "931350207026116",
"blob": "0606ffbddece05a1e21456e45a3ca88d8e0ff2f299568aecc5db053f08e04774cdfba60d0ae4db000000003d710b3b5444505391ba01b634f94d72915bda5a7999499f40f62e3046b254b403",
"target": "ffffff00"
}
}
web >>
{
"type": "submit",
"params": {
"job_id": "931350207026116",
"nonce": "197aae72",
"result": "6d9a8c2da95bcd498a4056631f1a03b51f2d6806b1c6d44e4f45fd9878b6eb00"
}
}
srv <<
{
"type": "hash_accepted",
"params": {
"hashes": 256
}
}
web >>
{
"type": "submit",
"params": {
"job_id": "931350207026116",
"nonce": "edd815a0",
"result": "ca182b3086f441510397d67b2323014d201aa14b89b6c1fcf59759794540a900"
}
}
srv <<
{
"type": "hash_accepted",
"params": {
"hashes": 512
}
}
web >>
{
"type": "submit",
"params": {
"job_id": "931350207026116",
"nonce": "6e791848",
"result": "9fec44bc7d338b5e114e138ee902066ba8d6a2a0e4c0b6090fa94571eca54900"
}
}
srv <<
{
"type": "hash_accepted",
"params": {
"hashes": 768
}
}
.
.
.
通过 Socket_dump
内容不难看出:
其实输入输出都是那几个量,验证登录 Wallet(siteKey),获取jobs,开始计算,返回计算值,由Pool确认算力价值。
所有的
method
变成了type
,params
内容基本原样传递,result
根据流程有所变化。
看过 coinhive.min.js
我们目前大致知道了:
脚本通过浏览器端asm实现方式,以尽可能高的效率完成复杂的CPU计算,实现XMR挖矿。
因为web端无法直接进行Socket通信,所以需要用
WebSocket
与矿池通信。
由以上内容和对wss流量的解析,看出Pool连接流量中最大的变化主要在json内容构造上,而核心的领取jobs、计算hash、提交到pool、确认hash有效四个步骤,实际内容与一般Miner并无根本差异,只是由JsonRPC2.0
变更为适应前端的长连接类型WebSocket
而已。
既然浏览器里的CryptoNight算法已经实现,那么改造起来就hin方便辣!我们只需要自己构造Pool或者构造PoolProxy接管所有的操作,就能完全从Coinhive平台跳出来,直接为自己的Wallet挖矿,免去使用平台而损失的30%手续费。
而用算力替代网站内的广告,在控制对CPU占用情况下不对用户造成卡顿,同时避免在自己网站停放广告导致用户厌烦的情况下,赚取用来维护网站的“零花钱”,感觉也是一个更好的发展方向。
Coinhive手续就是高!广告联盟弹框就是飘!
要不是30%手续费让我心如刀割,说不定我就老老实实用Coinhive现成的脚本了,不会有这篇文章了吧!
开个玩笑……
黑客意识哪来?极客情怀何在?众矢之的,打倒虚拟货币资本主义构造属于自己的前端矿池服务器,刻不容缓!
但是!都写了这么长了,各位看官或许也审美疲劳了,那么在下一篇文章中我们再详细来聊聊,如何构造属于自己的前端挖矿PoolProxy~
(Emmmm……)
(好像那篇说Webshell的第二篇文章……也忘了写了……下回一同补档……)
(预习班:来自学一下demo,再养成阅读无注释源码的习惯吧!)