S3是小米最新发布的智能手表,搭载小米基于NUTTX系统二次开发的Vela系统。Vela是RTOS系统,与传统的安卓系统穿戴设备有一定区别。本文通过Hack小米S3手表,成功实现原神启动。
小米手表S3搭载了小米基于NUTTX系统二次开发的Vela系统,这是一款实时操作系统,与传统的安卓穿戴设备有一定区别。S3的应用自然也不是安卓APK应用,而是快应用(QuickAPP),一个由国内几家手机厂商联合推动的一个类似微信小程序的应用标准,这种应用使用JS开发,使用了与安卓APK类似的签名机制。本文的研究目标就是破解S3手表,让它安装我们修改后的应用,实现原神启动。
要实现这一目标,我们需要解决以下两个挑战:
前文提到过,S3手表不具备网络连接能力,那么它是如何完成应用安装功能的呢?小米提供了一个小米运动健康APP,通过这个APP与手表配对后,在APP中的设备页面,有应用商店选项,点击进入后可以安装手表应用。在安卓手机上,这个过程是通过蓝牙串口通信协议完成的,用户先在小米健康APP上下载要安装的手表应用,然后通过蓝牙串口通信协议把下载的应用传输进入手表安装。
要实现任意应用安装,传统的方法是分析蓝牙通信协议,蓝牙通信协议相关代码在com.xiaomi.wearable.wear.api.SppConnection,com.xiaomi.wearable.wear.queue.BleTaskQueueV2中。但是其实可以使用一个TRICK绕过协议分析的问题,手表应用安装的过程首先需要下载应用到安卓本机,那么只需要在下载完成后替换下载的应用为我们修改的应用,就可以实现任意应用安装。
要实现替换应用,需要访问本机文件,Frida JS API没有文件访问API。我将演示如何使用我开发的pyfrida框架非常自然地完成替换过程。pyfrida框架支持直接使用Python编写Frida HOOK脚本,详细可以见文章末尾往期推送。
快应用下载相关代码在com.xiaomi.xms.wearable.ui.appshop.manager.AppDownloadManager类中,该类的getAppFilePath方法是我们需要HOOK的方法。因为在下载快应用后,会调用这个方法获取快应用在安卓本机上的路径,参数就是快应用包名。
接下来,我演示如何通过HOOK这个方法,把下载的快应用拉取到电脑。首先使用pyfrida编写HOOK脚本,然后在安卓手机上安装一个手表应用。如下图所示,get_filepath方法是我们的替换方法,在其中通过调用adb pull拉取下载的快应用到电脑。getAppFilePath方法被调用了两次,第一次调用是下载前被调用,作为下载目标路径,第二次调用是下载后调用,因为要把下载的快应用推送到S3手表。使用pyfrida框架可以很流畅地完成这个过程,因为可以直接使用Python编写Frida HOOK脚本,而如果使用传统的Frida HOOK脚本工作流程,则需要使用Socket通信实现类似功能,较为繁琐。


完整代码如下
基于pyfrida框架,如果要实现替换应用也很简单,只需要在触发HOOK时,执行adb push上传应用即可。
刚刚下载到的RPK文件是一个ZIP包,与安卓APK类似,它也有两处签名。一处是简单的完整性校验,有一个文件记录了ZIP包内其它条目的SHA256摘要,类似安卓V1签名,一处是修改校验,它在ZIP包中添加了签名快,类似安卓V2签名。接下来依次介绍这些签名。
首先把RPK文件解压,在META-INF目录中存在一个CERT文件,这也是一个ZIP包,解压后,得到hash.json文件。hash.json文件内容如下图所示,保存有ZIP包中除CERT文件以外的所有文件的SHA-256值。

在CERT文件,与RPK文件中,都存在与安卓APK V2签名类似的签名块,如下图所示,只是将Magic从APK Sig Block 42修改为RPK Sig Block 42。

快应用的开发工具链是hap-toolkit^1,属于NodeJS生态系,可以直接使用npm install -g hap-toolkit安装。
开发工具链里面肯定有签名算法,并且hap-toolkit直接将签名过程暴露出来了,运行hap,输出中有resign命令,很显然这个命令就是用来对快应用再签名的。找到hap-toolkit的安装目录,bin/index.js文件中有该命令的描述,如下所示。这个签名算法也可以在hap-toolkit的源代码^2里面找到,见signZipBufferForPackage函数。
简单描述一下这个命令的用法,它是为未签名的应用添加签名的。未签名的应用指的是只进行了V1签名,还没有进行V2签名的应用。那么再签名的过程也很简单,简单来说就是根据修改的文件,生成hash.json,并打包成ZIP改名为CERT替换原CERT,而后将目录打包ZIP,改后缀名为RPK,使用刚刚提到的resign命令重新签名即可通过校验。这个过程比较繁琐并且可以自动化,我编写了一个快应用重打包工具,该工具已开源至GitHub^3。简单介绍一下这个工具的使用流程。
首先解包快应用,得到如下目录

直接修改该目录中的文件
本例中需要修改两处,一处是替换 common/logo.png,这是应用启动LOGO,还有一处是 pages/index/index/index.js,修改调用setTimeout时的参数,后面的参数控制了启动LOGO的显示时间,修改为5000。Patch点如下图所示。

完成这3步之后,我们就获得了一个再签名过的RPK。而后使用上一节提到的替换下载应用的方法,我们就可以在S3手表上安装我们修改过的应用。最后原神启动效果如下图所示:

[1] hap-toolkit d46K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2F1M7r3#2B7M7#2)9J5k6h3y4G2L8g2)9J5c8Y4m8S2j5$3E0S2k6$3g2Q4x3V1k6Z5j5i4m8Q4x3X3c8@1L8$3!0D9K9$3W2@1
[2] 签名算法实现函数 96eK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Z5j5i4m8B7M7#2)9J5k6s2m8D9j5i4c8X3L8%4u0E0i4K6u0r3K9r3q4H3i4K6u0V1N6r3!0G2L8r3E0A6N6q4)9J5c8X3u0D9L8$3u0Q4x3V1j5#2x3o6y4W2x3X3q4T1y4h3u0X3x3$3j5&6x3K6y4X3j5K6k6X3y4$3x3$3k6o6g2X3j5X3j5$3y4h3f1J5y4$3u0V1j5e0S2U0j5h3t1$3i4K6u0r3M7r3q4U0K9$3q4Y4k6i4y4Q4x3V1k6Z5j5i4m8Q4x3X3c8H3j5h3y4C8j5h3N6W2M7W2)9J5c8Y4y4J5j5#2)9J5c8Y4y4A6k6$3&6S2N6s2g2J5k6g2)9J5c8X3W2F1k6r3g2^5i4K6u0W2K9Y4y4Q4x3U0y4x3x3e0l9&6
[3] 快应用重打包工具 ad7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6V1k6r3c8V1K9r3@1I4x3U0x3@1i4K6u0r3M7i4g2A6j5$3E0S2M7s2m8J5k6i4y4A6k6$3^5`.
from
pyfrida
import
pyfrida
from
pyfrida.pyfrida
import
JSObject, JSContext
import
frida
import
os
def
setup_hook(ctx: JSContext):
clz
=
ctx.Java.use(
"com.xiaomi.xms.wearable.ui.appshop.manager.AppDownloadManager"
)
clz.getAppFilePath.implementation
=
js_getfilepath
def
get_filepath(ctx: JSContext, a1: JSObject):
r
=
ctx.this.getAppFilePath(a1)
print
(
"参数: "
, a1.get_val(),
"返回值: "
, r.get_val())
os.system(
"adb pull %s 1.rpk"
%
(r.get_val(), ))
return
r
device
=
frida.get_usb_device()
fs
=
pyfrida.FridaScript()
js_setup_hook
=
fs.add_js_function(setup_hook)
js_getfilepath
=
fs.add_js_function(get_filepath)
ps
=
device.get_process(
"小米运动健康"
)
fs.attach(device, ps.pid)
fs.exec_func_in_java(js_setup_hook)
input
()
from
pyfrida
import
pyfrida
from
pyfrida.pyfrida
import
JSObject, JSContext
import
frida
import
os
def
setup_hook(ctx: JSContext):
clz
=
ctx.Java.use(
"com.xiaomi.xms.wearable.ui.appshop.manager.AppDownloadManager"
)
clz.getAppFilePath.implementation
=
js_getfilepath
def
get_filepath(ctx: JSContext, a1: JSObject):
r
=
ctx.this.getAppFilePath(a1)
print
(
"参数: "
, a1.get_val(),
"返回值: "
, r.get_val())
[培训]科锐逆向工程师培训第53期2025年7月8日开班!