Locus Map Pro 3.4.0 Patch笔记分享
Locus Map Pro APK介绍
“Locus Map Pro - Outdoor GPS”是一款户外徒步软件。具有记录徒步轨迹,显示加载离线地图等功能。
因为软件是外国公司开发的,没有支持中文。国内用户因为语言的问题使用极不方便。该软件在德国Android
App市场,Google Play,
三星市场上架。为了减小开发包的大小,从3.10开始,ApK用到的部分so库需要从服务器上下载。下载过程会检测用户使用的APK市场,并需要登录用户帐号。因为众多周知的原因,我们大陆形成了独特的APK市场。手机厂家的自定义系统中根本没有内置这三家市场软件,导致用户直接下载失败。而APK开发者显然没考虑到中国市场的问题,所以仅仅提示“网络故障”,搞得用户莫名其妙。很多付费购买了APK的用户也无法正常使用。
基于这两点原因,特别是第二条,在开发者没有支持中国市场之前,我们只能自力更生的解决这些问题。
研究目的
- 汉化软件,方便国人使用
- 能顺利安装APK
需要解决的问题和困难
- 因为需要重新打包,所以需要绕过APK的签名检测。
- 为了方便安装,需要把要下载的so内置到apk中。
- 因为不需要再下载so文件,所以需要绕过下载检测流程。
- 因为没有产生真正的购买过程,所以增强特性会被禁用,变成免费版本。所以需要解除限制。
现在我们以3.40版本为例,探讨如何解决上面的问题。
解除签名限制
首先用apktool对apk解包
1 | java -jar apktool2.0.jar d Locus3.40.apk -f -o outdir |
输出结果如下:
I: Using Apktool 2.0.0-Beta7 on Locus3.40.apk
I: Loading resource table…
I: Decoding AndroidManifest.xml with resources…
I: Loading resource table from file: C:\Users\monkey\apktool\framework\1.apk
I: Regular manifest package…
I: Decoding file-resources…
W: Could not decode attr value, using undecoded value instead: ns=, name=style,
value=0x7f0d0066
W: Could not decode attr value, using undecoded value instead: ns=, name=style,
value=0x7f0d0047
W: Could not decode attr value, using undecoded value instead: ns=, name=style,
value=0x7f0d007f
W: Could not decode attr value, using undecoded value instead: ns=, name=style,
value=0x7f0d0066
W: Could not decode attr value, using undecoded value instead: ns=, name=style,
value=0x7f0d006e
I: Decoding values / XMLs…
Can’t find framework resources for package of id: -1. You must install proper framework files, see project website for more info.
很不幸,解析资源时遇到不能识别的资源id(原因见黑体部分),解包过程退出。
我们先不处理资源,增加-r参数后,重新解包。
1 | java -jar apktool2.0.jar d Locus3.40.apk -f -o outdir -r |
这次输出如下
I: Using Apktool 2.0.0-Beta7 on Locus3.40.apk
I: Loading resource table…
I: Copying raw resources…
I: Loading resource table…
I: Baksmaling…
I: Copying assets and libs…
I: Copying unknown files/dir…
I: Copying original files…
解包过程顺利完成。
现在我们在smali文件中查找字符串“Landroid/content/pm/Signature;”, 在Mn.2.smali中找到下面的函数
1 | .method private static ・([Landroid/content/pm/Signature;)Z |
这段代码是计算签名的hashcode,如何值为-0x53ad97d7或0x1a222754就认为签名是合法的。估计签名分别为免费版和pro版的签名证书是不同的。
这里有两种办法来绕过,一种是修改跳转指令:if-ne改为if-eq;另一种是比较前给v0重新赋值为和v1相同的值:const v0, -0x53ad97d7.
任选一种,修改后重新打包。
1 | java -jar apktool2.0.jar b outdir -f -o Locus3.40_modi.apk |
I: Using Apktool 2.0.0-Beta7 on out
I: Smaling…
Exception in
thread “main”
org.jf.dexlib2.writer.util.TryListBuilder$InvalidTryException: Multile
overlapping catches for Ljava/lang/Exception; with differenthandlers
at org.jf.dexlib2.writer.util.TryListBuilder$MutableTryBlock.addHandler(TryListBuilder.java:180)
at org.jf.dexlib2.writer.util.TryListBuilder.addHandler(TryListBuilder.java:311)
at org.jf.dexlib2.writer.util.TryListBuilder.massageTryBlocks(TryListBuilder.java:69)
at org.jf.dexlib2.writer.DexWriter.writeCodeItem(DexWriter.java:881)
at org.jf.dexlib2.writer.DexWriter.writeDebugAndCodeItems(DexWriter.java:759)
at org.jf.dexlib2.writer.DexWriter.writeTo(DexWriter.java:214)
at org.jf.dexlib2.writer.DexWriter.writeTo(DexWriter.java:192)
at brut.androlib.src.SmaliBuilder.build(SmaliBuilder.java:58)
at brut.androlib.src.SmaliBuilder.build(SmaliBuilder.java:41)
at brut.androlib.Androlib.buildSourcesSmali(Androlib.java:337)
at brut.androlib.Androlib.buildSources(Androlib.java:298)
at brut.androlib.Androlib.build(Androlib.java:284)
at brut.androlib.Androlib.build(Androlib.java:258)
at brut.apktool.Main.cmdBuild(Main.java:233)
at brut.apktool.Main.main(Main.java:88)
很不幸,打包失败了。
使我们修改的原因导致的麽。实践证明,无论我们是否做修改,都无法打包成功。那我们该怎么解决这个问题呢。下面我们就来揭晓答案。
重新打包
前面我们发现用apktool把APK解包后,无法重新打包。这是什么原因呢?分析发现这是因为开发者在生成APK包的时候对代码进行了混淆。很多Java类的类名、方法名、字段名都被替换为非ascii字符。导致重新打包失败。例如上一篇我们修改的签名验证的函数就是如此。
1 | .method private static ・([Landroid/content/pm/Signature;)Z |
我们可以看到函数名不是我们常见的ascii字符。
如果我们想重新打包,就要首先解决这个问题。
把字符串重新映射为ascii码
利用混淆类似的原理。混淆是把ascii字符串映射为非ascii字符,我们需要一个类似的逆向过程。
这里我们借助apktool来帮我们完成这个工作。
apktool的代码是开源的。我们需要对代码做一些修改,让生成类名、字段名、方法名、代码内字符串的部分做一些修改,从而生成ascii字符的名称。
修改apktool源代码的过程在此略过,感兴趣的可以参考《战胜混淆后的非ASCII字符 – Android 逆向系列三》
我们把修改代码后编译出来的apk命名为apktool_modi.jar
重新操作,我们发现可以打包成功了。
让我们暂时停下来休息一下,去解决接下来的挑战。
重新打包后,安装,程序直接退出。因为有些需要的类被加密了,解密后反射方法的方法名已经被映射为ascii,所以无法反射出相关方法,导致空指针异常。所以我们需要把类解密出来,并进行处理。
解密被加密的类
重新打包后,安装,程序直接退出,查看系统日志,提示如下异常信息。
程序出现空指针异常。
12-15 09:09:29.264: E/AndroidRuntime(857): FATAL EXCEPTION: main
12-15
09:09:29.264: E/AndroidRuntime(857): java.lang.RuntimeException: Unable
to get provider locus.api.core.LocusDataProvider:
java.lang.NullPointerException
12-15 09:09:29.264: E/AndroidRuntime(857): at android.app.ActivityThread.installProvider(ActivityThread.java:4822)
12-15 09:09:29.264: E/AndroidRuntime(857): at android.app.ActivityThread.installContentProviders(ActivityThread.java:4432)
12-15 09:09:29.264: E/AndroidRuntime(857): at android.app.ActivityThread.handleBindApplication(ActivityThread.java:4372)
12-15 09:09:29.264: E/AndroidRuntime(857): at android.app.ActivityThread.access$1300(ActivityThread.java:141)
12-15 09:09:29.264: E/AndroidRuntime(857): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1294)
12-15 09:09:29.264: E/AndroidRuntime(857): at android.os.Handler.dispatchMessage(Handler.java:99)
12-15 09:09:29.264: E/AndroidRuntime(857): at android.os.Looper.loop(Looper.java:137)
12-15 09:09:29.264: E/AndroidRuntime(857): at android.app.ActivityThread.main(ActivityThread.java:5041)
12-15 09:09:29.264: E/AndroidRuntime(857): at java.lang.reflect.Method.invokeNative(Native Method)
12-15 09:09:29.264: E/AndroidRuntime(857): at java.lang.reflect.Method.invoke(Method.java:511)
12-15
09:09:29.264: E/AndroidRuntime(857): at
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793)
12-15 09:09:29.264: E/AndroidRuntime(857): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:560)
12-15 09:09:29.264: E/AndroidRuntime(857): at dalvik.system.NativeStart.main(Native Method)
12-15 09:09:29.264: E/AndroidRuntime(857): Caused by: java.lang.NullPointerException
12-15 09:09:29.264: E/AndroidRuntime(857): at com.asamm.locus.core.MainApplication._cb8b(:103)
12-15 09:09:29.264: E/AndroidRuntime(857): at o._c5a7.onCreate(:43)
12-15 09:09:29.264: E/AndroidRuntime(857): at android.content.ContentProvider.attachInfo(ContentProvider.java:1058)
12-15 09:09:29.264: E/AndroidRuntime(857): at android.app.ActivityThread.installProvider(ActivityThread.java:4819)
12-15 09:09:29.264: E/AndroidRuntime(857): … 12 more
分析异常发现,最后抛出异常的应用层函数为
12-15 09:09:29.264: E/AndroidRuntime(857): at com.asamm.locus.core.MainApplication._cb8b(:103)
异常中显示了代码行数103(Locus编译时没有删除调试信息,所以可以显示行号。若删除调试信息,这里显示的是指令位移)。
直接在MainApplication.smali文件中搜索”.line 103”(不包含引号).
1 | …… |
代码中通过调用o/LY$_e383bb的_cb8f方法加载一个类定义对象。奇怪的是我们翻查了代码,没有发现o.LY类的存在。一度我怀疑是APKTool工具存在问题,部分类在反编译时丢失了。查看了apktool代码,发现dex文件中显示的类的个数和解析出的类的个数相同。分析o/LY$_e383bb的_cb8f的方法,发现是这里面动态的实现了加载o.LY类。
LY$_e383bb.smali文件又9338行,这里就简单介绍下里面的实现原理。
有一个类加载器和o.LY类的代码被压缩加密后存储为字节数组,存储为类的静态字段。类先解密类加载器代码,然后用类加载器代码加载o.LY类。
现在解释下找到解密后的类的方法。
因为类字节的压缩、加密,解密最终都是通过字节数据来传递的。所以我们先以[B来确认代码的相关位置。
解密代码的最后一步,从一个数组中读取一个整数值,来作为最终字节码的长度,然后生成该长度的数组,把字节码最终解密到该数组中。
下面就是找到的符合特征的地方。
1 | :try_start_15 |
现在我们借助DebugTool工具,把数组信息输出到硬盘上。
DebugTool是我写的一组smali代码输出工具。可以在下面地址找到它。
DebugTool
现在我们在代码中增加输出语句。
修改上面的代码如下。
1 | invoke-virtual {v2, v5, v6}, Ljava/lang/Class;->getMethod(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method; |
这段代码里v1的值没有被修改过,所以可以直接输出,否则提前保存v1的原始值。
这里类加载器的代码和o.LY的代码都会被输出,o.LY的代码保存在类加载器的后面。所以需要对输出文件处理,提取出o.LY的代码。o.LY的字节码存储为一个dex文件格式。所以我们把它存储为o.LY.dex,让后把它压缩为o.LY.zip,然后重命名为o.LY.apk.
最后是用我们自己编译的apktool工具解出o.LY的smali代码.
因为原来的目录已经存在了ly.smali,所以我们把这个LY.smali重新命名为LY.4.smali,把它拷贝到我们的locus的smali的o目录下。
现在因为我们已经把类代码直接解密出来了,所以不需要在对o.LY的代码解密了,所以替换o/LY$_e383bb的内如下:
1 | .class public Lo/LY$_e383bb; |
至此o.LY的代码解密完毕.
重新打包,运行,还是报错。因为还有一个加密的类o.aA.
用同样的方法处理,再次打包运行。
现在发现,apk终于可以运行了, 很是兴奋。但是提示需要下载数据,数据还是下载不了。
不要急,接下来我们就解决运行所需要的数据的问题。
应用程序可以运行了,但是提示运行需要下载大约5M的数据,点击下载,总是提示网络错误。怎么办呢?
准备运行需要的数据
分析代码,并综合热心网友提供的信息。发现所谓的需要的数据就是两个.so文件:libproj.so和libjsqlite.so。
下载数据需要验证用户的购买信息。支持google play、三星应用市场和德国应用市场的账户信息。
要攻克账号这块,困难重重。
只好从别的地方下功夫了。
热心网友提供了apk的钛备份数据。里面有下载好的.so文件。当然了只有热心网友的手机芯片的版本armv7.这对国内用户差不多就够了。
我们就制作armv7特别版吧。
把两个so文件拷贝到lib\armeabi-v7a目录。
现在看看哪里加载了这两个so文件。
搜索”proj”或”jsql”(包括引号部分),找到了文件My.2.smali.
1 | .class public Lo/My; |
把两个so加载函数分别修改为
1 | #加载libproj.so |
现在修改.method public static _cb8a()Z函数内容为 :
1 | .method public static _cb8a()Z |
重新编译打包运行,还是提示需要下载。
搜索调用My._cb8a方法的地方。
找到下面几个文件
./com/asamm/locus/core/StartScreen$iF.3.smali: invoke-static {}, Lo/My;->_cb8a()Z
./com/asamm/locus/core/StartScreen$iF.3.smali: invoke-static {}, Lo/My;->_cb8a()Z
./o/_c4bd.smali: invoke-static {}, Lo/My;->_cb8a()Z
./o/_efbe83.smali: invoke-static {}, Lo/My;->_cb8a()Z
./o/My.2.smali: sput-object v0, Lo/My;->_cb8a:[Ljava/lang/String;
StartScreen$iF.3.smali像是和启动界面相关的。
我们来看一下:
找到下面这个方法。
1 | .method public _efbda5(ZZ)Z |
我们把它修改会始终返回false,
1 | .method public _efbda5(ZZ)Z |
再次打包,启动APK,发现不再提示需要下载数据了。
但是我们仍然没有成功,我们又遇到了别的问题。
下载数据终于想办法绕过去了,但是启动应用后,应用还是退出了,究竟是什么原因呢?
处理资源中的非ascii字符
现在启动应用,应用异常退出,抛出异常信息如下:
12-15 11:51:59.396: E/AndroidRuntime(32656): FATAL EXCEPTION: main
12-15
11:51:59.396: E/AndroidRuntime(32656): java.lang.RuntimeException:
Unable to start activity
ComponentInfo{menion.android.locus.pro/com.asamm.locus.basic.MainActivityBasic}:
android.view.InflateException: Binary XML file line #162: Error
inflating class o.ヌ
12-15 11:51:59.396: E/AndroidRuntime(32656): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2180)
12-15 11:51:59.396: E/AndroidRuntime(32656): at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2230)
12-15 11:51:59.396: E/AndroidRuntime(32656): at android.app.ActivityThread.access$600(ActivityThread.java:141)
12-15 11:51:59.396: E/AndroidRuntime(32656): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1234)
12-15 11:51:59.396: E/AndroidRuntime(32656): at android.os.Handler.dispatchMessage(Handler.java:99)
12-15 11:51:59.396: E/AndroidRuntime(32656): at android.os.Looper.loop(Looper.java:137)
12-15 11:51:59.396: E/AndroidRuntime(32656): at android.app.ActivityThread.main(ActivityThread.java:5041)
12-15 11:51:59.396: E/AndroidRuntime(32656): at java.lang.reflect.Method.invokeNative(Native Method)
12-15 11:51:59.396: E/AndroidRuntime(32656): at java.lang.reflect.Method.invoke(Method.java:511)
12-15
11:51:59.396: E/AndroidRuntime(32656): at
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793)
12-15 11:51:59.396: E/AndroidRuntime(32656): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:560)
12-15 11:51:59.396: E/AndroidRuntime(32656): at dalvik.system.NativeStart.main(Native Method)
12-15
11:51:59.396: E/AndroidRuntime(32656): Caused by:
android.view.InflateException: Binary XML file line #162: Error
inflating class o.ヌ
12-15 11:51:59.396: E/AndroidRuntime(32656): at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:698)
12-15 11:51:59.396: E/AndroidRuntime(32656): at android.view.LayoutInflater.rInflate(LayoutInflater.java:746)
12-15 11:51:59.396: E/AndroidRuntime(32656): at android.view.LayoutInflater.rInflate(LayoutInflater.java:749)
12-15 11:51:59.396: E/AndroidRuntime(32656): at android.view.LayoutInflater.rInflate(LayoutInflater.java:749)
12-15 11:51:59.396: E/AndroidRuntime(32656): at android.view.LayoutInflater.rInflate(LayoutInflater.java:749)
12-15 11:51:59.396: E/AndroidRuntime(32656): at android.view.LayoutInflater.inflate(LayoutInflater.java:489)
12-15 11:51:59.396: E/AndroidRuntime(32656): at android.view.LayoutInflater.inflate(LayoutInflater.java:396)
12-15 11:51:59.396: E/AndroidRuntime(32656): at android.view.LayoutInflater.inflate(LayoutInflater.java:352)
12-15 11:51:59.396: E/AndroidRuntime(32656): at android.view.View.inflate(View.java:16465)
12-15 11:51:59.396: E/AndroidRuntime(32656): at o._efbe83.onCreate(:284)
12-15 11:51:59.396: E/AndroidRuntime(32656): at android.app.Activity.performCreate(Activity.java:5104)
12-15 11:51:59.396: E/AndroidRuntime(32656): at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1080)
12-15 11:51:59.396: E/AndroidRuntime(32656): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2144)
12-15 11:51:59.396: E/AndroidRuntime(32656): … 11 more
12-15
11:51:59.396: E/AndroidRuntime(32656): Caused by:
java.lang.ClassNotFoundException: Didn’t find class “o.ヌ” on path:
/data/app/menion.android.locus.pro-1.apk
12-15 11:51:59.396: E/AndroidRuntime(32656): at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:65)
12-15 11:51:59.396: E/AndroidRuntime(32656): at java.lang.ClassLoader.loadClass(ClassLoader.java:501)
12-15 11:51:59.396: E/AndroidRuntime(32656): at java.lang.ClassLoader.loadClass(ClassLoader.java:461)
12-15 11:51:59.396: E/AndroidRuntime(32656): at android.view.LayoutInflater.createView(LayoutInflater.java:552)
12-15 11:51:59.396: E/AndroidRuntime(32656): at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:687)
12-15 11:51:59.396: E/AndroidRuntime(32656): … 23 more
提示很明显找不到一个类:“o.ヌ”。
这是因为资源中引用到了类名,而这个类名现在已经不存在了。
我们需要把资源中的类名替换为新的类名。
需要替换的文件有Androidmanifeste.xml和layout文件夹下的xml文件。
如何替换资源中的特殊字符,请参考我的博客:
处理资源文件中引用到了非ASCII字符的类名字符串–Android 逆向系列四
我已经提供了代码的参考实现:
AXMLStringTransformer.java
我们把资源文件处理后,重新打包,运行。
应用终于启动了,但是应用变成了免费版本。
现在应用变成了免费版本,不但有广告,还有很多有用的功能用不了,真着急。
解除功能限制
首先找到com/asamm/locus/utils/Native/Native.smali文件
替换isFullFeatured native方法为下面内容
1 | .method public static isFullFeatured(Landroid/app/Application;)Z |
重新打包,启动,发现广告消失了,APK变为Pro版了。
但体验后发现有些功能,如天气仍然不能是用。
说明还有些功能限制没有解除.
Native.smali文件里有一个函数:
1 | .method public static native performAction(Landroid/app/Application;Ljava/lang/Runnable;)V |
这个native方法是执行一个线程方法。这个方法在执行操作前,会检测是不是全功能版,我们前面在smali(或者Java层面)把代码修改为全功能版。但是performAction调用的是native版本的isFullFeatured函数,native版本的函数是没有被修改的。
现在我们修改so库中的isFullFeatured函数。
查看Native.smali的static块函数,发现没有直接load so库。
1 | # direct methods |
该函数解密加密的so文件,,解密后释放到APK的数据目录。动态加载后,删除解密后的so文件。
在方法的最后几行,找到调用delete函数的地方,注释掉delete函数。
修改后的内容为:
1 | # direct methods |
打包后重新运行程序,退出应用。
如果是用的是模拟器,可以用adb shell或dbms工具,把/data/data/menion.android.locus.pro目录下寻找一个隐藏文件,重命名为libmacore.so,然后拷贝出来,如果是设备,就需要root后才能读出来。
用支持arm指令的反编译程序(推荐IDA)找到isFullFeatured函数.
这是我用的工具的指令示意图
我们首先找到返回true的指令代码,查找出对应的指令代码。
我们选择直接短路函数。
arm函数的特征是,寄存器入栈,执行函数指令,寄存器出栈。返回值保存在r0寄存器。
入栈指令
我们修改后的函数指令如下:
1 | push.w {r0, r1, r4, r5, r6, r7, r8, lr} |
我们查出来movs r0, #0x1的指令字节码为:01 20 .见上图。
pop.w {r2, r3, r4, r5, r6, r7, r8, pc}的指令字节码为:BD E8 FC 81
要修改的代码位置为:0x00001cb4
我们用二进制编辑文件打开libmacore.so文件,找到位置0x00001cb4,把当前位置的内容替换为:
01 20 BD E8 FC 81
修改完毕后保存文件,然后替换lib/armeabi-v7a/libmacore.so文件。
现在我们只支持armv7a,可以把lib目录下的其他三个芯片色目录删除,减小最后输出包的大小。
重新打包,安装测试,现在那些原来不能用的功能,如天气,现在可以用了。
至此:Locus Pro Patch系列完成。
###写在最后
- 该系列文章是对自己所做工作的整理,为了方便其他爱好者DIY,不是原始的分析过程。所以步骤只是解释了怎么做,而没有提及步骤的发现过程。
- apk 压缩文件效率比较低。修改过程中可以把解包目录下的unknown目录中的文件拷贝到别的地方,然后用压缩工具添加。我在这样做后,打包时间从3分多钟降为不到2分钟。对频繁打包的同学,可考虑此方法。
- Patch因为修改短路了部分代码,可能会引起最后做成的apk部分功能不稳定,这可能在所难免。本人能力、财力都有限,不能为Patch给您带来的损失负责,测试练习前,请确认您为完全民事行为能力人。
- 应用中要下载的so文件是关于数据库操作,和数学投影变换的,所以在一定的版本更迭期间,功能应该会保持不变,所以可能可以重复利用。
- 如果有条件,请支持正版。向开发者反馈,请求提供中文化版本。
- 行文仓促,难免有错漏和词不达意的地方,阅读时,请包容。