海底掘金自动化挖矿实现

前言

最近掘金出了一款小游戏叫做海底掘金, 挖取矿石可以兑换周边礼品

我眼馋那款游戏机已久, 于是乎就玩起了这款游戏, 不过每天手动操作实在太过劳神, 于是, 我打算将其流程自动化

所有的数据都是离不开接口, 我们只需要模拟接口请求即可

那么首先要做的就是分析游戏中每一个动作都发送了什么数据

动作分析

  1. 上移

    1
    {"command":["U"]}

    image-20211129115453501

  2. 下移

    1
    {"command":["D"]}

    image-20211129115518866

  3. 左移

    1
    {"command":["L"]}

    image-20211129115538071

  4. 右移

    1
    {"command":["R"]}

    image-20211129115556431

  5. 跳跃

    1
    2
    3
    4
    5
    6
    7
    8
    //上跳跃
    {command: ["8"]}
    //下跳跃
    {command: ["2"]}
    //左跳跃
    {command: ["4"]}
    //右跳跃
    {command: ["6"]}

    image-20211129120520331

  6. 一层循环2次上移

    1
    {command: [{times: 2, command: ["U"]}]}
    • times: 表示循环次数

    image-20211129115642187

  7. 两层循环

    1
    {command: [{times: 2, command: [{times: 3, command: ["D"]}]}]}

    image-20211129115708099

    多层循环则多层嵌套

接口分析

  1. 动作接口

    1
    https://juejin-game.bytedance.com/game/sea-gold/game/command?uid=xxx&time=xxx
  2. 游戏结束接口

    1
    https://juejin-game.bytedance.com/game/sea-gold/game/over?uid=xxx&time=xxx
  3. 游戏开始接口

    1
    https://juejin-game.bytedance.com/game/sea-gold/game/start?uid=xxx&time=xxx
  4. Token获取接口

    1
    https://juejin.cn/get/token

    这个接口获取到的值其实就是其他接口中请求头authorization的值, 具体往下看

  5. 获取矿石数量相关信息

    1
    https://juejin-game.bytedance.com/game/sea-gold/home/info?uid=xxx&time=xxx

浏览器控台自动化实现

我们跳过登录验证操作, 先从简单的控台自动化入手, 实现动作的自动化, 有效避免作弊检测

比如我们想要实现以下动作的反复执行

image-20211129121755381

那么发送的数据如下:

1
{"command":[{"times":10,"command":["L","2","R",{"times":10,"command":["L","4","D",{"times":10,"command":["R","D","2"]}]}]}]}

这一步我们可以直接从浏览器控制台里拷贝出来, 不用自己计算, 方便快捷准确

然后结合动作接口和请求头进行模拟请求即可

如下请求头

  • Content-type
  • authorization (关键)
  • x-tt-gameid (关键)
  • accept

以上请求头数据直接从控制台拷贝

代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
await (async function autoRun(){
//以下三个字段值替换成你自己的就行
let uid = 'xxx';// 你的uid
let authorization = 'xxx'; // request headers中的authorization
let gameid = 'xxx'; // request headers中的x-tt-gameid


let params = {"command":[{"times":10,"command":["L","2","R",{"times":10,"command":["L","4","D",{"times":10,"command":["R","D","2"]}]}]}]};

let datarus = await fetch('https://juejin-game.bytedance.com/game/sea-gold/game/command?uid=' + uid + '&time=' + Date.parse(new Date()), {
method: 'POST',
credentials: "include",
headers: {
'Content-type': 'application/json; charset=UTF-8',
'authorization': authorization,
'accept': 'application/json, text/plain, */*',
'content-length': JSON.stringify(params).length,
'x-tt-gameid': gameid,
},
body: JSON.stringify(params)
}).then(async (res) => {
return res.json();
});

return datarus;

})();

注意, 每一局的gameId是不一样的, 使用相同的gameId会报游戏异常, 这种操作大家要尽量避免, 谁知道官方有没有利用这个来判定玩家是否作弊呢, 谨慎为上

请求成功后响应数据如下:

1
{"data":{"appendMapData":[0,0,133,0,0,0,27,122,6,0,0,22,6,0,0,6,135,6,0,21,21,0,0,122,0,0,0,0,23,0,6,6,0,0,6,0,25,0,0,121,0,6,6,0,29,0,6,6,0,151,6,0,0,6,27,0,0,6,0,0,0,0,125,6,0,0,0,5,0,135,113,143,0,6,131,0,0,102,101,151,6,6,0,0,0,0,3,0,0,6,0,6,6,6,122,0,6,6,0,6,6,0,0,0,29,0,6,0,6,0,0,101,6,0,24,6,6,0,0,0,6,0,6,6,6,132,0,6,0,0,0,0,6,0,5,6,6,131,114,0,0,0,0,0,0,6,6,0,6,0,6,6,6,0,104,0,6,0,4,0,0,0,6,125,6,113,0,0,0,6,0,6,6,0,0,0,6,6,0,0,6,0,5,6,5,121,28,0,0,24,0,0,6,151,0,0,141,6,0,24,0,6,0,0,6,6,0,0,131,6,105,6,0,0,27,113,6,0,6,0,0,143,0,0,101,0,29,0,0,6,0,0,0,133,6,6,25,24,151,29,6,104,6,0,6,6,5,0,0,131,6,131,6,6,0,0,0,6,0,28,133,0,124,6,6,0,0,0,131,0,22,0,0,0,0,0,0,0,22,131,6,6,0,6,6,24,124,6,0,0,101,6,6,0,0,0,103,0,6,26,0,0,0,0,103,0,6,0,6,131,0,6,0,6,6,6,0,141,6,29,0,0,0,6,0,3,6,0,6,6,6,0,0,114,0,0,6,0,0,6,0,6,0,6,6,6,0,0,28,23,6,102,0,115,4,6,6,0,6,27,6,6,0,6,6,0,0,24,0,3,6,0,0,143,0,29,0,26,0,4,6,105,0,0,0,6,0,21,132,21,0,0,0,6,4,103,6,0,27,6,0,24,24,0,6,104,0,104,0,0,6,6,152,6,0,6,0,0,0,0,0,0,0,6,24,6,6,23,143,0,0,24,0,6,0,6,0,0,0,151,0,26,0,6,0,0,0,0,6,0,0,0,0,6,135,0,6,6,0,0,0,0,29,6,0,0,6,0,24,25,6,0,0,0,0,0,6,0,0,6,0,6,152,28,6,25,6,0,6,0,6,6,0,0,0,25,0,0,6,122,6,0,0,6,112,6,0,0,124,0,0,0,0,25,0,6,6,6,6,27,112,24,121,6,6,0,6,0,6,0,6,115,0,22,6,6,6,115,6,6,101,0,27,5,105,0,0,0,0,0,0,0,6,0,0,0,3,6,6,0,0,0,0,6,0,0,0,0,26,6,6,0,0,6,6,6,6,6,0,6,0,6,6,0,0,0,0,6,0,6,6,0,0,3,5,104,0,0,122,27,0,0,0,0,23,0,0,0,0,0,4,23,25,6,0,153,0,0,0,0,0,6,0,0,0,6,6,6,0,6,0,4,0,6,6,0,3,0,112,0,0,0,0,0,0,6,0,113,0,0,0,6,6,21,0,6,124,0,6,115,0,0,23,132,0,0,6,0,6,6,0,6,6,6,0,6,0,6,0,0,6,6,0,6,6,26,0,134,0,6,6,6,0,0,0,0,0,0,0,0,0,6,6,6,113,111,0,6,0,6,0,0,0,0,0,124,0,0,6,0,22,6,6,24,22,6,104,0,125,0,0,6,6,0,6,6,0,0,121,6,0,6,0,0,6,0,6,6,0,0,0,0,0,25,0,0,6,0,6,6,6,111,0,0,6,0,5,0,0,0,6,6,6,0,6,0,123,6,0,6,25,0,0,6,113,0,0,6,0,0,0,0,0,6,0,0,6,122,6,134,0,0,6,0,134,0,6,0,0,0,6,103,0,24,0,6,0,6,0,0,0,0,0,6,6,0,132,6,0,6,103,6,0,6,28,26,6,0,111,6,131,0,27,27,0,6,6,0,102,0,0,6,0,6,0,0,6,21,6,0,6,0,6,121,26,0,6,0,6,0,0,0,0,0,0,6,0,24,0,0,0,6,0,4,0,22,23,28,0,6,6,111,153,131,0,0,0,0,0,6,27,0,0,23,0,0,0,0,6,0,0,104,0,0,0,104,6,0,0,0,6,6,0,0,6,29,6,0,0,0,0,6,6,0,0,6,6,0,0,0,23,6,0,0,0,6,0,113,6,0,6,6,6,0,0,0,29,152,0,0,0,153,0,6,6,0,6,6,0,26,6,6,6,151,21,0,21,6,0,21,6,6,6,0,6,0,0,0,22,6,6,6,0,6,0,21,6,6,0,6,6,22,0,5,0,6,0,0,0,0,0,28,6,0,6,114,0,6,0,0,6,0,23,0,0,6,0,0,114,6,6,0,0,0,6,0,0,0,0,6,0,6,6,0,0,0,0,0,0,0,24,0,0,0,6,134,0,0,0,131,0,0,0,0,6,6,25,0,0,6,5,0,104,0,6,6,0,0,6,0,0,0,6,0,6,101,0,0,6,0,6,0,0,0,125,0,0,6,134,0,6,6,27,0,6,0,131,6,0,0,6,0,0,6,0,123,0,0,0,0,6,0,0,6,6,6,6,0,6,0,6,0,0,0,25,0,0,0,0,0,6,0,0,6,0,6,0,0,0,6,0,6,6,6,114,6,6,0,0,0,0,0,6,0,0,6,124,6,6,0,0,154,6,0,152,0,0,22,6,6,6,0,6,26,0,0,111,0,133,29,0,0,6,6,104,133,0,0,6,26],"curPos":{"x":5,"y":210},"blockData":{"moveUp":29,"moveDown":30,"moveLeft":14,"moveRight":27,"jump":7,"loop":15},"gameDiamond":112},"code":0,"message":"success","logId":"xxxx","serviceTime":1638159487113}

注意看返回的gameDiamond这个字段, 我们执行一次接口立马就获取了112个矿石, 也就是说我们掉完接口立马结束游戏, 那么这矿石就到手了, 省去了游戏执行的时间

在同一局中, 我们可以反复调接口, 直到道具不足为止, 如果道具不足就重开一局再过一遍之前的代码

到这里 问题就来了, 每一局的x-tt-gameid都不一样, 每新开一局都要拷贝一次太繁琐了,接下来我们解决x-tt-gameid的问题

签名破解

接下来全局翻找源码, 从js源码中, 我大致猜测这个x-tt-gameid是由ES256算法生成accessToken, 找到如下代码:

1
h.default.sign({gameId:this.gameId,time:t},"-----BEGIN EC PARAMETERS-----\nBggqhkjOPQMBBw==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIDB7KMVQd+eeKt7AwDMMUaT7DE3Sl0Mto3LEojnEkRiAoAoGCCqGSM49\nAwEHoUQDQgAEEkViJDU8lYJUenS6IxPlvFJtUCDNF0c/F/cX07KCweC4Q/nOKsoU\nnYJsb4O8lMqNXaI1j16OmXk9CkcQQXbzfg==\n-----END EC PRIVATE KEY-----\n",{algorithm:"ES256",expiresIn:2592e3,header:{alg:"ES256",typ:"JWT"}});return console.log("token",e)

当然也只是猜测, 需要进行验证

为了方便测试, 我将代码测试从控台转移到nodejs环境中

对于nodejs不太熟悉的朋友, 我这里简单介绍一下:

nodejs可以通俗理解为是模拟了一个浏览器的环境, 相当于java中的jre, 以前js代码只能在浏览器中执行, 有了nodejs环境, 我们就可以脱离浏览器随处运行js代码啦

Nodejs官网

好了 下载安装完nodejs之后, 安装jsonwebtoken模块用于jwt的生成

1
npm install jsonwebtoken

然后运行以下代码测试:

1
2
3
4
5
const jwt = require('jsonwebtoken');
const privatekey = "-----BEGIN EC PARAMETERS-----\nBggqhkjOPQMBBw==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIDB7KMVQd+eeKt7AwDMMUaT7DE3Sl0Mto3LEojnEkRiAoAoGCCqGSM49\nAwEHoUQDQgAEEkViJDU8lYJUenS6IxPlvFJtUCDNF0c/F/cX07KCweC4Q/nOKsoU\nnYJsb4O8lMqNXaI1j16OmXk9CkcQQXbzfg==\n-----END EC PRIVATE KEY-----\n"

console.log(jwt.decode("eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJnYW1lSWQiOiIyMDIxLTExLTI2IDA5OjUwOjIwIiwidGltZSI6IjE2Mzc5MTMyNjIwMTkiLCJpYXQiOjE2Mzc5MTMyNjIsImV4cCI6MTY0MDUwNTI2Mn0.2SWFcrvSMGzRIhsZe6ny0ugj5eoJnb4DlGGudFzUgvwPTqbeZaSa2FG5jYF5VPOXfv7Ye0TlYPt3VmviclOGKQ")
)

解码得到:

1
2
3
4
5
6
{
gameId: '2021-11-26 09:50:20',
time: '1637913262019',
iat: 1637913262,
exp: 1640505262
}

至此, 我们既验证了密钥的正确性, 同时得到了加密参数, 而这个time则是当前时间毫秒值

那接下来我们需要在游戏中实际验证一下才行, 验证步骤如下:

  1. 调用游戏开始接口获取返回的gameId
  2. gameId进行ES256加密得到签名
  3. 调用动作接口, 并将签名放入x-tt-gameid请求头中
  4. 请求成功, 验证ok

好了 我们的第一阶段的破解工作已完毕, 接下来就是对代码进行整理, 实现自动化运行

由于代码转移到nodejs上之后使用fetch进行网络请求代码一直报错, 因此请求方式改为axios

这里多一嘴, 虽然nodejs模拟了浏览器的环境, 但是浏览器内置的一些API, nodejs上不一定也内置了, 要用的话的npm手动安装, 比如fetch是浏览器内置的API, 控台直接可以使用, 如果要在node上用的话可以执行以下指令安装:

1
npm install node-fetch

然后代码中引用:

1
const fetch = require('node-fetch');

同样axios也是:

1
npm install axios

代码引用:

1
const axios = require('axios');

关于fetchaxios两者的实现原理和区别, 有兴趣的可自行百度, 这里就不展开介绍了

回过头来, 整理后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
const jwt = require('jsonwebtoken');
const axios = require('axios');


const uid = 'xxx';// 你的uid
let authorization = 'xxx'; // request headers中的authorization


let currentTime = Date.parse(new Date())
let gameId = "2021-11-30 10:36:12"
function getAccesssToken() {

const privatekey = "-----BEGIN EC PARAMETERS-----\nBggqhkjOPQMBBw==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIDB7KMVQd+eeKt7AwDMMUaT7DE3Sl0Mto3LEojnEkRiAoAoGCCqGSM49\nAwEHoUQDQgAEEkViJDU8lYJUenS6IxPlvFJtUCDNF0c/F/cX07KCweC4Q/nOKsoU\nnYJsb4O8lMqNXaI1j16OmXk9CkcQQXbzfg==\n-----END EC PRIVATE KEY-----\n"
currentTime = Date.parse(new Date())
const payload = { "gameId": gameId, "time": currentTime }

const accessToken = jwt.sign(payload, privatekey, {
expiresIn: "240h",
algorithm: "ES256",
});
//console.log(accessToken)
return accessToken
}


//游戏开始
async function gameStart() {



//选择 click这个角色 钻石有加成
let params = { "roleId": 2 };

const options = {
url: 'https://juejin-game.bytedance.com/game/sea-gold/game/start?uid=' + uid + '&time=' + Date.parse(new Date()),
method: "POST",
credentials: "include",
headers: {
'Content-type': 'application/json; charset=UTF-8',
'authorization': authorization,
'accept': 'application/json, text/plain, */*',
'content-length': JSON.stringify(params).length,
'x-tt-gameid': '',
'user-agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Mobile Safari/537.36'
},
data: params,
};

return await axios(options).then((response) => {
console.log(response.data);
if (response.data.code == 4007) {
//游戏正在执行 则结束游戏
// gameOver()

} else if (response.data.code == 0) {
//成功开始
gameId = response.data.data.gameId


}
});


}
//执行游戏动作
async function gameRun() {


let gameid = getAccesssToken(); // request headers中的x-tt-gameid



let paramsList = [{ "command": [{ "times": 10, "command": ["L", "2", "R", { "times": 10, "command": ["L", "4", "D", { "times": 10, "command": ["R", "D", "2"] }] }] }] },
{ "command": [{ "times": 10, "command": ["L", "D", "R", { "times": 10, "command": ["L", "4", "D", { "times": 10, "command": ["R", "D", "2"] }] }] }] },
{ "command": [{ "times": 10, "command": ["U", "4", "L", { "times": 10, "command": ["L", "4", "D", { "times": 10, "command": ["R", "D", "2"] }] }] }] }
];

let params = paramsList[Math.floor(Math.random() * paramsList.length)];

const options = {
url: 'https://juejin-game.bytedance.com/game/sea-gold/game/command?uid=' + uid + '&time=' + currentTime,
method: "POST",
credentials: "include",
headers: {
'Content-type': 'application/json; charset=UTF-8',
'authorization': authorization,
'accept': 'application/json, text/plain, */*',
'content-length': JSON.stringify(params).length,
'x-tt-gameid': gameid,
},
data: params,
};

return await axios(options).then((response) => {
console.log(response.data);

if (response.data.code == 0) {
//成功

return response.data.data.gameDiamond

} else {
//4009 代码块不足
return 0
}
});

}
//结束游戏
async function gameOver() {



let params = { "isButton": 1 };

const options = {
url: 'https://juejin-game.bytedance.com/game/sea-gold/game/over?uid=' + uid + '&time=' + Date.parse(new Date()),
method: "POST",
credentials: "include",
headers: {
'Content-type': 'application/json; charset=UTF-8',
'authorization': authorization,
'accept': 'application/json, text/plain, */*',
'content-length': JSON.stringify(params).length,
},
data: params,
};

return await axios(options).then((response) => {
console.log(response.data);
if (response.data.code == 0) {
//成功

return response.data.data.todayDiamond>=response.data.data.todayLimitDiamond

} else {

return true
}
});


}
async function delay(time) {

var start = Number(new Date());
while (start + time * 200 > Number(new Date())) { }
}
async function start() {
//开始游戏-->游戏执行-->游戏结束
for (let index = 0; index < 100; index++) {
console.log("倒计时:" + index)
await gameStart()
let time = await gameRun()


//同步延迟结束游戏 根据获取的金币而定 金币越多 延迟时间越长
await delay(time)
let info=await gameOver()
//金币达到上限 停止游戏
if(info){
return
}
}


}

start()

将以上代码拷贝到你的本地文件中, 填入你的游戏authorizationuid, 然后每天用node运行一遍即可

到这里, 第一阶段的自动化已然实现

程序定时执行

每天手动跑代码太费事, 稍不留神就有可能某天给漏了, 尤其是周六日不开电脑的时候岂不是没法执行脚本了

这时 我们可以考虑放到服务器中每天定时执行, 365天风雨无阻, 推荐大家使用github或者travis ci免费服务部署

我这里手上刚好有一台阿里云的服务器, 因此直接用服务器跑, 这里我使用的是crontab定时程序, 关于crontab的用法, 可参见《Linux Crontab 命令安装和使用教程:在 VPS 上设置定时任务

ubuntu为例具体操作步骤如下:

  1. 将本地脚本拷贝至服务器

    1
    scp 脚本文件 root@服务器IP:/root

    注意:冒号前后不能有空格, 否则会提示目录不存在

  2. 登录服务器配置crontab任务

    执行crontab -e进入cron编辑界面, 将以下定时代码写入

    1
    0 8 * * *  /usr/bin/node /root/jwt_test >>/root//log/jwt_test_$(date +\%Y-\%m-\%d-\%H).log 2>&

    表示每天8点执行该脚本

  3. 安装nodejs及相关依赖

    如果服务器已经安装nodejs可忽略该步操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //更新源
    sudo apt-get update
    //安装nodejs
    sudo apt-get install -y nodejs
    //安装npm包管理器
    sudo apt-get install npm
    //安装脚本依赖
    npm install jsonwebtoken
    npm install axios
  4. 配置完毕

本文为作者原创转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

0%