最近在找记录待办任务的安卓app,找到一个还挺不错的,既能为单日设置任务,也能设置周期性的任务,例如每日的阅读、练琴、学日语。

这个app有需要订阅购买的高级版功能,不过对我而言免费版的功能就足够了,我的需求并不高。

唯一不足的就是,周期性任务只能创建3个。

原神的每日任务都有四个呢,三个怎么够用。

只是为了这一个功能而买所有高级版功能,怎么想都不太划算。于是便想着用这个app来练练手,学习学习安卓反编译,把重复任务的数量限制给解除掉。

上面一节是我去年6~7月的需求,我在破解完这个任务待办app的数量限制后,没有及时完成这篇文章。而现在我再来完成时,发现已不记得当时的思路了,遂重新开写。

新的需求是破解一个公司提供的手机令牌app,跟别的2FA应用类似,利用TOTP算法,基于秘钥,每分钟生成不同的验证码。但是不同于Google Authenticator、 Microsoft Authenticator、FreeOTP,这个app导入秘钥时需要密码,而且还有时间限制,超过了某个时间后,便无法导入。例如秘钥导入的有效期是2026年3月29日,那我在30日便无法导入,断网、改系统时间也不行。

这对于经常换手机的用户(爷)来说,相当不便。

因为不清楚它背后的算法实现,所以我没法去掉它导入秘钥时要输入密码这一步,但能把它的时间限制功能给去掉。只要我有秘钥和导入密码,无论什么时候,都应该能导入和生成验证码。

本文即记录反编译修改功能和重新打包的思路与过程,参考了XDA上的教程[GUIDE][INDEX\]How to modify an apk,在此致谢。

反编译apk

将apk以普通压缩包解压出来后,得到的一众文件中,dex文件则是从java代码编译出的文件,主要的逻辑也在其中。

第一步反编译,其实有三个方式可以使用

  • apktool 具备解压apk,反编译smali,重新打包的功能。

  • jadx-gui 直接读apk文件并反编译成java代码,类似jd-gui

  • dex-tools + jd-gui 前者将apk中的dex转化为常规的jar,后者将jar反编译成java代码,提高可读性

在我的实践中,阅读代码主要是通过jadx-gui,其具备方法、类名跳转的功能,方便查看引用;修改和重新打包则是apktool反编译出的smali中进行。

分析app

先对要破解的app进行分析,要破解它导入秘钥时的时间限制,就应先找到导入秘钥界面的Java类名。

手机连接电脑,启用adb。在手机打开目标界面(即Acticity)的时候,在adb中执行

dumpsys activity top | grep ACTIVITY

获取当前正在运行的活动。排除一番,就能得到目标app的活动名称。之后去看这个类里面的实现方法就行。

另外,超过时间限制后导入秘钥时,会提示"种子文件已过期",而修改系统时间,关闭网络后导入秘钥时,会提示"获取不到网络时间,请检查网络连接状态"。也可以全局搜索这两个字符串,去寻找对应的实现代码。

破解思路

首先解压和反编译这个apk,利用apktool。

java -jar apktool_2.11.1.jar d 手机令牌.apk -o 目标文件夹/

同时用jadx-gui直接打开apk,定位到目标Activity类

也可以用jadx-gui将apk反编译后的源代码,以gradle项目的形式导出出来,方便用VS Code查看

jadx导出为gradle.webp

找到导入秘钥的类以及方法后,发现没有判断时间的逻辑。点开其中调用的几个方法,也没有。

没有时间判断逻辑.webp

这下只能用字符串搜索了。在VS Code中全局搜索"种子文件已过期"这样的字符串后,终于在一个Verify类中找到了时间的判断逻辑。

找到时间判断逻辑.webp

大致的验证秘钥合法性思路为,SystemUtils.getInternetTimeStamp() 联网获取当前时间。

  • 如果网络没开,获取不到时间,使得currentTimeStamp==0,则直接验证失败。

  • 如果秘钥中缺有效期属性,验证失败。

  • 如果开了网络,获取到了真正时间,但是当前时间超过了秘钥的有效期,则验证失败。

都验证成功的话,才去校验其他信息,例如导入密码对不对等。

至此,破解思路也呼之欲出了。

给currentTimeStamp随便赋一个值,绕过第一个currentTimeStamp==0的判断。或者把向网络获取时间的调用,改为从系统获取时间。这样即使app不联网也一样能导入

第二个判断不需要绕过,因为算法的具体实现我们不知道,无法去判断秘钥中也没有这个属性。

第三个判断中直接删去return false,或者直接跳过判断。不管有效期认证过不过,直接去校验秘钥其他信息。

修改smali

apktool反编译apk后,得到的并不是java代码,而是smali代码,这是一种安卓虚拟机的反汇编语言。在解包后的文件夹中,smali文件夹里。文件夹的结构按照类名的结构组织。

找到Verify类的smali代码,根据方法名找到对应的代码段

找到smali代码中方法位置.webp

再根据jadx-gui中看到的字符串,进一步找到代码段

首先把向网络请求时间的调用,改为从系统获取时间,即System.currentTimeMillis()方法,这样就绕过了第一个判断。

替换为系统调用.webp

随后用goto指令,直接跳过第三个判断。

直接跳过校验.webp

打包、签名

在改完后,重新打包之前,建议先检查apk原先的签名。当然这一步可做可不做,只是保险起见。

apksigner在安卓SDK中有,在build-tools目录下

apksigner verify -v 手机令牌.apk

得到如下输出,哪里true了,就说明用了v几的签名。由此可知该apk用了v2签名

Verifies
Verified using v1 scheme (JAR signing): true
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): false
Verified using v3.1 scheme (APK Signature Scheme v3.1): false
Verified using v4 scheme (APK Signature Scheme v4): false

修改完smali后,照样用apktool对其打包

java -jar apktool_2.11.1.jar b 反编译代码目录/ -o 手机令牌_修改.apk

如果修改的过程中,出现了什么语法错误,在打包的过程中会提示出来的。

打包完后的apk,因为未签名,所以还无法安装到手机上。

创建个签名秘钥。之后会要求输入个验证密码,以及其他信息。随意即可。

keytool -genkey -alias {随便设置} -keyalg RSA -keystore {秘钥文件名}.jks

利用apksigner,对apk进行签名。

apksigner sign --ks 秘钥文件.jks -out 手机令牌_修改_signed.apk 手机令牌_修改.apk

最后安装到手机上

adb install 手机令牌_修改_signed.apk

经验证,可以正常使用,并且不受网络限制,不受有效期限制,只要有秘钥本身,有导入密码,随时都可以导入。

总结

爽,跟开发者斗智斗勇。

去年六七月那一次,逆向一个待办app,以及这一次手机令牌app,都算是比较顺利的,没花太多天的时间。得亏是java代码,好逆,如果开发者把具体实现逻辑封装在C库中,就很难办了。

逆向需要经验,开发者可能会有包括代码混淆在内的各种手段,来给破解者增加阻力。这并非是一朝一夕的学习能看穿的。

如果开发者再加一个签名校验,这个阻力又会加大一番。