魅族手环无法登陆可以说是魅族这个公司的正常操作,魅族很多产品貌似都是外包出去的,发布之后几乎没有更新,我买的魅族pop一代和魅族手环都是如此。 ——被耍猴多次的煤油

魅族手环的app很久都没有更新了,安装在现在的手机上发热严重几乎不可用,所以前一阵子我就换成了小米手环(非常讽刺,当时是用更贵的价格支持的魅族)。昨天拿出来魅族手环一看,好家伙app已经登录不上去了。询问了官方客服,客服表明已经停产两年,服务器数据已经清空关闭了。

于是我们只能自力更生!

安卓反编译流程

我们首先要有一份APK,然后通过apktool将他分别反编译为java版本和smali版本。java是用来看源码理思路的,smali是我们可以修改重新编译的,两个版本的代码一一对应,比如有一个MainActivity.java文件就有一个MainActivity.smali文件对应,其中的类、方法等内容也几乎是对应的。

我们看java代码是比较容易理解程序的逻辑流程的,但java代码几乎没有有效快捷的方法去重新编译,所以我们修改的时候只能先阅读java代码,理清要修改的java代码内容,再通过二者的对应关系修改smali代码。

针对smali简单科普一下:apk中代码被储存为dex格式,也就是安卓虚拟机的字节码格式,而smali就相当于是dex对应的汇编语言。虽然我们可能不太了解smali,但是不了解也没关系,这对精简来说影响不大。

0x00. 反编译

1
2
3
4
# 在命令行中先写好环境变量(windows下可能有些不同)
input=/Users/balabala/Downloads/meizushouhuan.apk
output=/Users/balabala/Downloads/meizushouhuan
output_java=/Users/balabala/Downloads/meizushouhuan_java

我们这一步需要两个软件 apktool、dex2jar和JD-gui,大家可以自行下载

1
2
3
4
5
6
7
8
9
# 反编译为smali项目
apktool d -f -o $output $input

# 反编译为dex项目
apktool d -s -f -o $output_java $input
# 再将dex转换为jar文件
sh d2j-dex2jar.sh $output_java/*.dex
# 打开JD-gui导入jar查看源码
# 或者下载jadx直接从apk反编译到源码(不过貌似没有前面的效果好)

下一步就是阅读java源码整理思路,实际大部分应用都会做“加壳”处理,轻则让我们不能很好的阅读理解代码,重则反编译失败。好在魅族手环并没有这样的操作,一切代码以近乎源码的程度向我们开放。

0x01. 修改代码1.0——手动写入用户数据

修改的过程只能说是具体情况具体分析,简单的修改可以是替换res文件夹下的语言文件(也就是我们听过的汉化),也可以修改layout文件夹下的界面布局文件,改改样式改改内容,这些都不涉及代码层面的修改,所以难度不大。

但是魅族手环这个情况涉及到了需要去除登录的问题,只是简单的跳转到首页没有作用,因为软件几乎每个操作都要读取修改数据库中的用户数据,思考再三我准备在第一次进入软件时模仿注册成功的行为向数据库中写入新用户的数据信息,这样就可以欺骗程序无需跳转到登录页面。

下列对代码的分析都是基于魅族手环app进行的,如果换一个app的话不能直接生搬硬套!

经过翻阅代码,发现程序在打开时会运行“gotoNext方法”,读取数据库判断存不存在本地用户,如果不存在本地用户则跳转到登录页面(LoginActivity)进行登录,然后重复运行类似的“gotoNext方法”,这时会监测到用户存在但是基本数据(年龄、身高……)为空,所以跳转到基本数据填写页面(RegistInfoActivity),填写好数据后再次运行“gotoNext方法”,这回一切条件都满足了,程序进入主页。

1
2
3
4
5
6
7
8
9
10
11
 // 原跳转函数示例
    private void gotoNext() {
        User user = DBUserApi.getLoginUser(this);
        if (user == null) {
            startActivity(new Intent(this, LoginActivity.class));
        } else if (user.getIsEmpty().booleanValue()) {
            startActivity(new Intent(this, RegistInfoActivity.class));
        } else {
            startActivity(new Intent(this, MainActivity.class));
        }
    }

所以我们可以在 不存在本地用户时(user == null),写入本地用户信息,然后直接跳转到基本数据填写页面(RegistInfoActivity)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 修改后的代码 
   private void gotoNext() {
        User user = DBUserApi.getLoginUser(this);
        if (user == null) {
            // 写入本地用户信息
            LoginInfoServer(this).saveLoginInfo("13000000000","13000000000");
            UserInfoServer(this).saveUserInfo(this, "13000000000", Logindata());
            // 跳转到数据填写页面
            startActivity(new Intent(this, RegistInfoActivity.class));
        } else if (user.getIsEmpty().booleanValue()) {
            startActivity(new Intent(this, RegistInfoActivity.class));
        } else {
            startActivity(new Intent(this, MainActivity.class));
        }
    }

其中添加数据的这两行代码如下。在程序调用 DBUserApi.getLoginUser(this) 获取登录的用户时,会先从SharePreferences中获取用户id,根据此id再在数据库中查询。所以我们要分别在SharePreferences中和数据库中写入用户信息。再次提示上述言论只针对魅族手环app,其他app的登录逻辑可不一定是这样,要大家根据代码认真分析。

1
2
3
4
// 向SharePreferences写入用户id和密码
LoginInfoServer(this).saveLoginInfo("13000000000","13000000000");
// 向sql数据库中写入用户id和空白的用户详细数据(Logindata:年龄、身高...)
UserInfoServer(this).saveUserInfo(this, "13000000000", Logindata());

这里saveLoginInfo传入了两个参数:用户id和密码,都是我随机设定的,原本在登录时,用户密码参数是传入了一个空字符串的,因为在app开发中,我们是不应该明文储存用户密码的。但是这里我并没有传入空值。主要是考虑到smali代码的缘故。

下面就是前面两行java代码对应的smali代码:

    const-string v2, "13000000000"
    new-instance v1, Lcom/meizu/smart/wristband/models/database/servers/LoginInfoServer;
    invoke-direct {v1, p0}, Lcom/meizu/smart/wristband/models/database/servers/LoginInfoServer;-><init>(Landroid/content/Context;)V
    invoke-virtual {v1, v2, v2}, Lcom/meizu/smart/wristband/models/database/servers/LoginInfoServer;->saveLoginInfo(Ljava/lang/String;Ljava/lang/String;)V

    new-instance v1, Lcom/meizu/smart/wristband/models/database/servers/UserInfoServer;
    invoke-direct {v1, p0}, Lcom/meizu/smart/wristband/models/database/servers/UserInfoServer;-><init>(Landroid/content/Context;)V
    new-instance v0, Lcom/meizu/smart/wristband/models/newwork/response/Logindata;
    invoke-direct {v0}, Lcom/meizu/smart/wristband/models/newwork/response/Logindata;-><init>()V
    invoke-virtual {v1, p0, v2, v0}, Lcom/meizu/smart/wristband/models/database/servers/UserInfoServer;->saveUserInfo(Landroid/content/Context;Ljava/lang/String;Lcom/meizu/smart/wristband/models/newwork/response/Logindata;)Z

这里你可能需要稍微了解一下smali才行,稍等你一段时间去百度看看……

分析上面的代码,都是三段式的,从左到右依次是(我自己的话总结出来的,不是专业术语):指令的类型、涉及到的寄存器或参数、类或方法

最左侧的指令的类型,有:

类型 含义
const-string 指定一个字符串
new-instance 指定一个类的实例对象
invoke-* 调用方法

中间表示涉及到的变量,v* 表示寄存器(这里简单理解成变量),p*表示这条语句所处方法的参数,这里p0表示this。

于是:

smali代码 含义
const-string v2, “13000000000” v2储存着一个字符串“13000000000”
new-instance v1, Lcom/……/LoginInfoServer; v1储存着LoginInfoServer类的实例
invoke-direct {v1, p0}, Lcom/…/LoginInfoServer;->(Landroid/content/Context;)V 对v1初始化,传入类型为Context的参数(这里因为是在activity中调用的,所以activity本身的this就是Context,于是传入p0)
invoke-virtual {v1, v2, v2}, Lcom/…/LoginInfoServer;->saveLoginInfo(Ljava/lang/String;Ljava/lang/String;)V v1调用saveLoginInfo,并传入两个字符串作为参数(这里解释了为啥id密码都写成同样的内容,为了偷懒少写一行代码,当然也可以说是合理复用寄存器优化程序)

第二句java代码以此类推,我就不展开描述了。后面修改跳转的activity路径就更简单了,相信你可以自己完成~

0x02. 修改代码2.0——假装网络访问成功

到这里我们已经可以进入应用并成功同步时间

img

要是手环固件也可以自己写的话就更好了

但是有意思的事情出现了,针对应用内的功能修改不生效,比如说修改目标步数,在设置中改好了,到主界面一看还是原先的值。

又是一顿代码检索,发现这个应用的逻辑是所有操作都要先发送到服务器,如果发送失败就不会进行后面的步骤。

于是找到代码中访问网络的部分(models/network/NetWorkApi1:executeCommon),在访问网络之前,直接返回了一个字符串。这里我还担心返回值会不会同样影响到程序的运行,比如后面有对字符串的json的转换之类的,所以传了一个“{}”避免出错。不过好在这个程序并没有对返回值做检查,只要有返回就不影响后面代码的运行。

魅族手环app大量的运用了rxjava的链式调用,所以我们需要返回的其实是 Observable 类型的内容。

1
2
// 直接返回一个字符串
return Observable.just("{}")

对应的samli代码

    const-string v0, "{}"
    invoke-static {v0}, Lrx/Observable;->just(Ljava/lang/Object;)Lrx/Observable;
    move-result-object v0
    return-object v0

但如果他对返回的内容也做了检查的话,估计就要换个思路改或者做更巧妙的修改了。

0x03. 解决发热

然后又回到了老问题,只要运行这个程序手机就发热,看cpu占用达到了18%-20%,这种情况可以推测出是某个线程死循环导致了的单核满载进而发热。

使用Android Studio自带的Android Profiler可以用来监测手机上某个被debug的应用的状态数据,用它来监测CPU并在手机开始发热时录制几秒。在录制的信息中看到了”com.mob.*“字样的内容,Mob是安卓中一个应用广泛的第三方库,经常被用来增加社交应用分享功能。

推测可能是由于应用太久未更新,老版本的Mob库不适合新的安卓版本,或老版本的网络接口被取消导致的。考虑到分享功能没啥必要,所以直接去Application文件中找到初始化的代码删除就解决了。

感慨一下,如果当初知道改这东西这么容易,没准就不会轻易投向小米了。

0x04. 收尾

最后就是修改修改layout文件,把用不到的功能直接从前端隐藏,注意这里不能删除,因为代码中往往会有前端组件的ID引用,如果直接删除掉会导致程序报错,所以我们隐藏即可(或者设置长或宽为0dp)。

0x05. 重新编译

在编译前,我们要准备好签名文件,大家在android studio中直接生成一个新的即可,准备好签名文件路径、别名和密码。

1
2
3
4
# 环境变量
keystroe=/Users/balabala/meizuband.jks
alias=band
password=nicaicaiwodemimashishenmo

然后再次使用apktool进行编译,使用jarsigner来给应用签名,使用adb直接安装到手机上

1
2
3
4
# 编译、签名与安装
apktool b $output -o $output/dist/unsigned.apk
echo $password | jarsigner -verbose -keystore $keystroe -signedjar $output/dist/signed.apk $output/dist/unsigned.apk $alias
adb install $output/dist/signed.apk

img

写这种程序竖着用屏幕提升效率真是大大滴