有道词典笔S6获取adb权限的详细教程 #277
Replies: 10 comments 24 replies
-
非常好详细教程(doge) |
Beta Was this translation helpful? Give feedback.
-
对力,如果懒(bushi)可以直接使用Kali Linux 的Vmware/VirtualBox整合包,工具都是齐的 |
Beta Was this translation helpful? Give feedback.
-
有空试试,不过希望有大佬可以解答下,有adb后是否可以: |
Beta Was this translation helpful? Give feedback.
-
你好大佬可以将你的文章的 md 文件的格式放在文章末尾吗?我想移植到我的博客上。如果大佬想入住博客随时欢迎 |
Beta Was this translation helpful? Give feedback.
-
正准备买有道笔 正好搜到热乎的教程了 必须支持 |
Beta Was this translation helpful? Give feedback.
-
貌似修改了返回方式? 能不能分享一下全量包链接? |
Beta Was this translation helpful? Give feedback.
-
已确认教程中的设备实际上是 S6, S6 pro 测试已成功,我补充几点:
附上 http 文件服务器(支持断点续传): from http.server import HTTPServer, SimpleHTTPRequestHandler
import os
class RangeRequestHandler(SimpleHTTPRequestHandler):
def do_GET(self):
"""处理 GET 请求,支持 Range 请求头"""
path = self.translate_path(self.path)
if not os.path.exists(path):
return super().do_GET() # 如果文件不存在,调用父类方法返回 404
self.range_request(path)
def range_request(self, path):
"""处理 Range 请求"""
file_size = os.path.getsize(path)
range_header = self.headers.get("Range")
if range_header:
# 解析 Range 请求头,例如 "bytes=0-1023"
start, end = self.parse_range_header(range_header, file_size)
if start is None or end is None:
self.send_error(416, "Requested Range Not Satisfiable")
return
self.send_partial_response(path, start, end, file_size)
else:
self.send_full_response(path, file_size)
def parse_range_header(self, range_header, file_size):
"""解析 Range 请求头"""
if not range_header.startswith("bytes="):
return None, None
range_values = range_header[6:].split("-")
if len(range_values) != 2:
return None, None
start = int(range_values[0]) if range_values[0] else 0
end = int(range_values[1]) if range_values[1] else file_size - 1
if start < 0 or end >= file_size or start > end:
return None, None
return start, end
def send_partial_response(self, path, start, end, file_size):
"""发送部分响应"""
self.send_response(206) # Partial Content
self.send_header("Content-Type", self.guess_type(path))
self.send_header("Content-Range", f"bytes {start}-{end}/{file_size}")
self.send_header("Content-Length", end - start + 1)
self.send_header("Accept-Ranges", "bytes")
self.end_headers()
with open(path, "rb") as file:
file.seek(start)
self.wfile.write(file.read(end - start + 1))
def send_full_response(self, path, file_size):
"""发送完整响应"""
self.send_response(200)
self.send_header("Content-Type", self.guess_type(path))
self.send_header("Content-Length", file_size)
self.send_header("Accept-Ranges", "bytes")
self.end_headers()
with open(path, "rb") as file:
self.copyfile(file, self.wfile)
if __name__ == "__main__":
port = 8000
print(f"Starting HTTP server on port {port}...")
server_address = ("0.0.0.0", port)
httpd = HTTPServer(server_address, RangeRequestHandler)
httpd.serve_forever() 简单优化后的更新服务器: const http = require('http');
const fs = require('fs');
const path = require('path');
const port = 80;
// 假设这是你的自定义 JSON 数据
const JsonData = {
"releaseNotes": {
"publishDate": "2024-02-29",
"version": "4.7.8",
"content": "[{\"country\":\"zh_CN\",\"content\":\"1、修复部分问题,优化系统稳定性\"}]"
},
"sha256": "36d83659ffd041ac87edc0721eea3c0f35f2c86fc18d6bd65c7663615c4b84e9",
"safe": {
"encKey": null,
"isEncrypt": 0
},
"version": {
"segmentMd5": "[{\"num\": 0, \"startpos\": 0, \"md5\": \"431942e60e10bec58b391011551b35e7\", \"endpos\": 52428800}, {\"num\": 1, \"startpos\": 52428800, \"md5\": \"684496532ad9297505540d993a8f915e\", \"endpos\": 104857600}, {\"num\": 2, \"startpos\": 104857600, \"md5\": \"6d48da4f4c2061359a96bcc4f9205ecf\", \"endpos\": 157286400}, {\"num\": 3, \"startpos\": 157286400, \"md5\": \"d84d0ff0e09965fa955cfbf4daf8c79f\", \"endpos\": 209715200}, {\"num\": 4, \"startpos\": 209715200, \"md5\": \"de209fa6544e8330407f1b09478b8720\", \"endpos\": 262144000}, {\"num\": 5, \"startpos\": 262144000, \"md5\": \"d689a6b27a28cee199a4ac3bcb31ebd5\", \"endpos\": 314572800}, {\"num\": 6, \"startpos\": 314572800, \"md5\": \"457ff0d7ee31da83d0612d22f11a994d\", \"endpos\": 367001600}, {\"num\": 7, \"startpos\": 367001600, \"md5\": \"64d52bafee9eb870e72b9de334d8ce8b\", \"endpos\": 419430400}, {\"num\": 8, \"startpos\": 419430400, \"md5\": \"7a099536fccf4725caa42efef06853d8\", \"endpos\": 471859200}, {\"num\": 9, \"startpos\": 471859200, \"md5\": \"87ed2b247f84b4ab17d78a0fea25bbda\", \"endpos\": 524288000}, {\"num\": 10, \"startpos\": 524288000, \"md5\": \"10a030701b749e58734b5f05dd5ebaa8\", \"endpos\": 576716800}, {\"num\": 11, \"startpos\": 576716800, \"md5\": \"217ebe79e59d135369471c90d06390e4\", \"endpos\": 629145600}, {\"num\": 12, \"startpos\": 629145600, \"md5\": \"cd363d3fd605fd7b6e79139c2e27999b\", \"endpos\": 681574400}, {\"num\": 13, \"startpos\": 681574400, \"md5\": \"17616fc4c5a61052bf76304d987af23a\", \"endpos\": 734003200}, {\"num\": 14, \"startpos\": 734003200, \"md5\": \"9104f0113147f89375718dec80ae453d\", \"endpos\": 786432000}, {\"num\": 15, \"startpos\": 786432000, \"md5\": \"35c8d474218063c50b4a1ff4e6cccbee\", \"endpos\": 787582988}]",
"bakUrl": "http://10.42.0.1:8000/50415c49-338c-4d6b-8154-12d62d344ec6.img",
"versionAlias": "",
"deltaUrl": "http://10.42.0.1:8000/50415c49-338c-4d6b-8154-12d62d344ec6.img",
"deltaID": "16681889",
"fileSize": 787582988,
"md5sum": "78abbf91b8afca063bef1def6f48536b",
"versionName": "99.99.91",
"sha": "e7ae841a3f91e8b17059e2c26e3b9cfd057f2eee"
},
"policy": {
"download": [
{
"key_name": "wifi",
"key_message": "仅wifi下载",
"key_value": "optional"
},
{
"key_name": "storageSize",
"key_message": "存储空间不足",
"key_value": "76944507"
},
{
"key_name": "forceDownload",
"key_message": "",
"key_value": "false"
}
],
"install": [
{
"key_name": "battery",
"key_message": "电量不足,请充电后重试!",
"key_value": "30"
},
{
"key_name": "rebootUpgrade",
"key_message": "",
"key_value": "false"
},
{
"key_name": "force",
"key_message": "",
"key_value": "{\"from\": \"00:00\", \"to\": \"00:00\",\"gap\": \"00:00\"}"
}
],
// "check": [
// {
// "key_name": "cycle",
// "key_message": "",
// "key_value": "1500"
// }
// ]
}
};
// 获取当前日期,并格式化为 HTTP 头所需的格式
const getFormattedDate = () => {
const now = new Date();
return now.toUTCString(); // 转换为 UTC 格式
};
// 创建 HTTP 服务器
const server = http.createServer((req, res) => {
console.log(`${req.method} request for ${req.url} at ${new Date().toISOString()}`);
if (req.method === 'GET' && req.url === '/product/1708583443/b1be8c8c394409d9/ota/checkVersion') {
// 设置自定义头部
res.writeHead(200, {
'Server': 'nginx/1.20.1',
'Content-Type': 'application/json;charset=UTF-8',
'Date': getFormattedDate(), // 使用当前日期
'Transfer-Encoding': 'chunked',
'Connection': 'close'
});
// 发送 JSON 数据
res.write(JSON.stringify({
status: 1000,
msg: 'success',
data: JsonData
}));
res.end(); // 结束响应
} else if (req.method === 'POST' && req.url === '/product/1708583443/b1be8c8c394409d9/ota/checkVersion') {
// 设置自定义头部
res.writeHead(200, {
'Server': 'nginx/1.20.1',
'Content-Type': 'application/json;charset=UTF-8',
'Date': getFormattedDate(), // 使用当前日期
'Transfer-Encoding': 'chunked',
'Connection': 'close'
});
// 发送 JSON 数据
res.write(JSON.stringify({
status: 1000,
msg: 'success',
data: JsonData
}));
res.end(); // 结束响应
}
else {
if (req.method === 'POST') {
let body = ''; // 用于存储请求体的内容
// 监听 data 事件,逐步接收数据
req.on('data', chunk => {
body += chunk.toString(); // 将数据块转换为字符串并拼接
});
// 监听 end 事件,处理完整请求体
req.on('end', () => {
console.log('Received body:', body); // 打印接收到的文本
});
}
// 处理其他请求
// 设置自定义头部
res.writeHead(200, {
'Server': 'nginx/1.20.1',
'Content-Type': 'application/json;charset=UTF-8',
'Date': getFormattedDate(), // 使用当前日期
'Transfer-Encoding': 'chunked',
'Connection': 'close'
});
res.write(JSON.stringify({
status: 1000,
msg: 'success',
data: null
}));
res.end();
}
});
// 启动服务器
server.listen(port, () => {
console.log(`Server is running at http://localhost:${port}`);
}); 这里还有可以直接生成所需字符串的分段 md5 计算程序: import sys
import hashlib
import json
data = "[{\"num\":0,\"startpos\":0,\"md5\":\"431942e60e10bec58b391011551b35e7\",\"endpos\":52428800},{\"num\":1,\"startpos\":52428800,\"md5\":\"684496532ad9297505540d993a8f915e\",\"endpos\":104857600},{\"num\":2,\"startpos\":104857600,\"md5\":\"6d48da4f4c2061359a96bcc4f9205ecf\",\"endpos\":157286400},{\"num\":3,\"startpos\":157286400,\"md5\":\"d84d0ff0e09965fa955cfbf4daf8c79f\",\"endpos\":209715200},{\"num\":4,\"startpos\":209715200,\"md5\":\"de209fa6544e8330407f1b09478b8720\",\"endpos\":262144000},{\"num\":5,\"startpos\":262144000,\"md5\":\"d689a6b27a28cee199a4ac3bcb31ebd5\",\"endpos\":314572800},{\"num\":6,\"startpos\":314572800,\"md5\":\"457ff0d7ee31da83d0612d22f11a994d\",\"endpos\":367001600},{\"num\":7,\"startpos\":367001600,\"md5\":\"64d52bafee9eb870e72b9de334d8ce8b\",\"endpos\":419430400},{\"num\":8,\"startpos\":419430400,\"md5\":\"7a099536fccf4725caa42efef06853d8\",\"endpos\":471859200},{\"num\":9,\"startpos\":471859200,\"md5\":\"87ed2b247f84b4ab17d78a0fea25bbda\",\"endpos\":524288000},{\"num\":10,\"startpos\":524288000,\"md5\":\"10a030701b749e58734b5f05dd5ebaa8\",\"endpos\":576716800},{\"num\":11,\"startpos\":576716800,\"md5\":\"217ebe79e59d135369471c90d06390e4\",\"endpos\":629145600},{\"num\":12,\"startpos\":629145600,\"md5\":\"cd363d3fd605fd7b6e79139c2e27999b\",\"endpos\":681574400},{\"num\":13,\"startpos\":681574400,\"md5\":\"584eb8c60df2950dbef8264ce0b0dced\",\"endpos\":734003200},{\"num\":14,\"startpos\":734003200,\"md5\":\"9104f0113147f89375718dec80ae453d\",\"endpos\":786432000},{\"num\":15,\"startpos\":786432000,\"md5\":\"35c8d474218063c50b4a1ff4e6cccbee\",\"endpos\":787582988}]"
parsed_data = json.loads(data)
def calculate_md5(file_path, start, end):
hasher = hashlib.md5()
with open(file_path, 'rb') as f:
f.seek(start)
while start < end:
buffer = f.read(min(1024 * 1024, end - start)) # 每次读取1MB,避免内存占用过高
if not buffer:
break
hasher.update(buffer)
start += len(buffer)
return hasher.hexdigest()
def main():
if len(sys.argv) != 2:
print("Usage: ./md5_splitter.py <file_path>")
sys.exit(1)
file_path = sys.argv[1]
segment_sizes = [52428800, 104857600, 157286400, 209715200, 262144000, 314572800, 367001600, 419430400, 471859200, 524288000, 576716800, 629145600, 681574400, 734003200, 786432000]
segment_md5s = []
start = 0
for end in segment_sizes:
md5_value = calculate_md5(file_path, start, end)
segment_md5s.append(md5_value)
start = end
# 输出14个MD5值
for i, md5 in enumerate(segment_md5s, 1):
print(f"Segment {i}: {md5}")
parsed_data[i-1]['md5'] = md5
print(json.dumps(parsed_data))
if __name__ == "__main__":
main() |
Beta Was this translation helpful? Give feedback.
-
可不可以通过拦截下载词典笔内的软件的链接把其他软件下载进去(我意思是比如下载写作指导它会从网络上获取链接,这时对其进行抓包然后把其他软件的下载链接伪装成写作指导的链接让它下载) |
Beta Was this translation helpful? Give feedback.
-
可以目前我在寻找制作软件的办法详情加群
我应该评论过了
…---原始邮件---
发件人: ***@***.***>
发送时间: 2025年3月12日(周三) 中午1:02
收件人: ***@***.***>;
抄送: ***@***.******@***.***>;
主题: Re: [PenUniverse/PenMods-release] 有道词典笔S6获取adb权限的详细教程 (Discussion #277)
可不可以通过拦截下载词典笔内的软件的链接把其他软件下载进去(我意思是比如下载写作指导它会从网络上获取链接,这时对其进行抓包然后把其他软件的下载链接伪装成写作指导的链接让它下载)
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you commented.Message ID: ***@***.***>
|
Beta Was this translation helpful? Give feedback.
-
改了md5升级成功还是显示incorrect的可以看看我这个教程 里面详细提到了部分固件存在的换行符问题导致md5出现偏差 |
Beta Was this translation helpful? Give feedback.
-
本文将PenUniverse Github Discussion上
SkySight-666
大佬的帖子进行了细化和补全,如果有建议和疑问,欢迎在下面留言!0. 前言
免责:使用本方法以及本方法获取的权限造成的一切后果,如远程施法、有道律师函、词典笔损坏、家长批评、第三次世界大战.. 与作者无关。
此教程仅针对有道词典笔S6,不同版本的词典笔操作可能完全不同,此教程可能不适用于其他词典笔。
到目前为止,获取到有道词典笔的ADB权限所能做的事情都很局限,因为有道的词典笔是
基于Linux的
,SoC是rics-v
架构的,甚至从3代开始,词典笔硬件迎来了史诗级削减,如:屏幕刷新率被砍,运行内存被砍到只剩300MB,这直接的导致了它没法安装任何Android应用,甚至完全可以说折腾它是没有任何实用意义的。为了上课摸鱼
或者拿词典笔干一些别的同款笔干不到的事以在其他人面前装x
的可以退出此教程
了,跟着这个教程做完你会发现什么也干不了
!!!此漏洞十分危险,无论你是否想要给词典笔提权,都请做到:不在陌生的网络环境更新系统、警惕莫名奇妙的系统更新、关闭更新包自动下载 , 以及不要利用此漏洞攻击别人的设备。
原理
通过抓包可以分析,有道的OTA更新使用的是不安全的HTTP,以及更新镜像的MD5是返回在POST请求中的。同时,通过逆向抓包下载到的全量固件,发现有道的词典笔OS并没有做必要的系统更新签名校验。故我们可以修改一个全量包,修改其中
adb_auth.sh
中的 sha256 值来实现修改密码,再通过规则转发来让词典笔更新我们自己的镜像。实现
binwalk
拆解全量包,得到原始adb密码的sha256值sed -i
命令替换sha256值nodejs
服务器,修改返回值iotapi.abupdate.com
重定向到你的欺骗服务器使用工具:
1. 抓取系统包
1.1 抓取更新请求
电脑开启热点并
使用词典笔连接
打开任务管理器,确定自己电脑网络热点的适配器名称(我这里是

本地连接* 10
)打开
WireShark
,找到刚才的适配器名称(我这里是本地连接* 10
),单击选中后,点击窗口上方绿色小鲨鱼启动抓包在词典笔上搜索更新,等待

WireShark
抓取更新请求,不出所料会有这么一条POST请求抓到后可以点击窗口上方红色小鲨鱼停止抓包
1.2 重新发送更新请求,获取全量包链接
找一个HTTP测试网站,这里我用的是SOJSON,将Header设置为

application/json;charset=UTF-8
,将你在WireShark
获得的数据填入,发送如下请求:JSON { "timestamp": "这里填你WireShark获得的timestamp", "sign": "这里填你WireShark获得的sign", "mid": "这里填你WireShark获得的mid", "productId": "这里填你WireShark获得的productID", "version": "99.99.90", "networkType": "WIFI" }
不出意外的话,获取到的数据差不多应该是这样的
我这里使用的是
有道词典笔S6
,其他版本的词典笔获取到的数据大概率会和这个不一样,但整体结构就差不多是这么个结构deltaUrl
(Line 19)部分后面的链接复制到浏览器中打开,下载下来的就是全量包啦,如果下载不成功可以试试换成bakUrl
(Line 17)中的链接2. 解包,修改adb密码
2.1 使用Binwalk解包
前言:某些版本听说binwalk没用,用DNA试试.
别问我,我没试过安装
binwalk
Linux环境:
Windows环境:
python setup.py install
解包,读取密码
cd到全量包的同一目录,使用Binwalk解包
解包后会获得一个文件夹
在文件夹路径
./_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.img.extracted/ext-root-0/usr/bin/adb_auth.sh
下有个adb_auth.sh
文件,其内容是这样的(其他版本的词典笔内容可能有所不同):那么可以很容易的注意到这个bash文件的作用是在adb连接的时候校验密码,而密码的校验方式竟然是读取文件的倒数第一行的sha256值并与输入值的sha256进行匹配...
原理知道了,那么想要修改密码就不难了
2.2 使用sed -i 修改密码
安装
sed
Linux环境:
Windows环境:
sed-x.x.x-setup.exe
,然后安装C:\Program Files (x86)\GnuWin32\bin
,没兴趣的也可以后续命令全部带绝对路径执行浏览器找一个
SHA256加密工具
,我用的是SHA256 在线加密工具,要加密的数据输入你想要替换的密码,我拿1145141919810
举例,得到的加密结果则为b7ab30a912521ac36e433a5cfc8b5c1037884487af45ae5311ced235ee77faef
记录原全量包大小,精确到
字节
(后面有用)用
sed -i
替换全量包中指定的字符串大小一致
3. 让词典笔更新修改密码后的全量包
现在已知词典笔检查更新是通过发送一条
POST请求
,随后冈易服务器返回是否有更新.那么想要让词典笔更新
修改密码后的全量包
,我们可以通过伪造一个冈易更新服务器
来实现.观察可以发现,冈易返回的数据大概有以下几个重要元素:
status
用于告诉词典笔是否有更新(2101
为无更新,1000
为有更新,5000
为出现错误)releaseNotes
用于告诉词典笔更新说明version
下为更新包的下载接口(deltaUrl
和bakUrl
)和校验数据(segmentMd5
为文件分段MD5,md5sum
为文件总MD5)那么我们只需要修改这些关键数据后重新发送给词典笔就好了
3.1 计算修改后全量包的MD5
md5_splitter.py
segment_sizes
中的数值修改为你自己的冈易更新请求里"segmentMd5"那一块每一个"endpos"的数值启动Python程序
python ./md5_splitter.py ./xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx.img # 输出如下 Segment 1: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Segment 2: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Segment 3: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Segment 4: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Segment 5: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Segment 6: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Segment 7: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Segment 8: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Segment 9: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Segment 10: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Segment 11: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Segment 12: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Segment 13: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Segment 14: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
记录所有
Segment
数据,待会要用计算修改后全量包的总MD5
记录输出的
MD5
值,待会要用3.2 创建一个更新文件直链
HTTP_Server.py
python ./HTTP_Server.py ./xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx.img # 输出如下 文件直链: http://xxx.xxx.xxx.xxx:14514/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx.img 按 Ctrl+C 停止服务器
3.3 创建一个欺骗服务器
YDPen.js
segmentMd5
,bakUrl
,deltaUrl
,md5sum
的值。segmentMd5
中修改startpos
和endpos
的值为你从冈易官方服务器获取到的值。md5
值修改为之前用脚本分段计算的MD5值bakUrl
和deltaUrl
修改为3.3创建的文件直链md5sum
修改为3.1计算的完整md5req.url
修改为你抓到的词典笔获取更新的链接后缀3.4 让词典笔从你的欺骗服务器安装更新
4. 尝试用修改后的adb密码连接词典笔
4.1 配置Adb环境
4.2 使用Adb连接词典笔
正常应该会有弹窗提示
ADB调试已打开
adb listdevices
查找设备正常应该是这样的
如果不是长这个样可以尝试
重新打开adb
,重新将词典笔连接电脑
或重新安装Android SDK
解决终端输入
adb shell
,提示要求输入密码,输入完你修改后的密码后回车,再次输入adb shell
即可进入ADB Shell至此,已经获取到词典笔的ADB权限。
疑难杂症
A: 尝试换一个http服务脚本解决
文章正在施工中,如果发现教程不完整,那么你先别急,等等会有的(咕咕咕咕咕
Beta Was this translation helpful? Give feedback.
All reactions