吾爱破解《安卓逆向这档事》另类破解

正己大佬课程中用到的APK,看了几集他的解法,很快很迅速,有些小工具挺好用的。他的解法看视频就好了,我就在这分享一下我的解法。

1. 修改APK资源数据

载入GDA,程序无壳,入口类为com.zj.wuaipojie.ui.MainActivity

既然它是让我们修改某个Activity中的文字和图片信息,那就直接用Android Killer来反编译APK并修改。发现在DEX文件转换为JAR文件时出错了,提示我们不支持该版本的DEX文件。

这是因为AK中的dex2jar工具版本太旧了,在网上下载最新版本的dex2jar将它替换即可。

此时再次反编译APK就没问题了。

搜索关键字符串,找到资源定位,并进行汉化。

查看其它字符串或图片,都是建了一个索引,我们找到这些索引并修改其中的字符串或图片即可。

比如替换图片,在res目录下找到对应位置,或直接搜索“first_img.jpg”,替换为其它图片。

比如汉化文字,同样操作。

但我们发现在activity_challenge_first.xml中没有找到英语的索引。搜索该字符串发现在代码中定义了。

在代码中不能直接输入中文,需要转成Unicode编码再写入到代码中。

重新编译打包,发现编译时又出现了一个错误:

这是因为aapt2才支持navigation,而AndroidKiller默认采用aapt。所以我们只能手动打包一下,加上--use-aapt2选项。

1
apktool b Project -o wuaipojie.apk --use-aapt2

又出错了,提示在AndroidManifest.xml的第4行的android:dataExtractionRules属性没有找到,有点奇怪为什么没有找到,不是在项目里面吗?不过它是个云备份功能,没有也行。

直接在AndroidManifest.xml中删掉这个属性,重新编译就成功了。然后继续用Android Killer帮APK签名。

卸载手机上的源APK,安装修改好后的APK,噔噔~成功啦!

这节课的内容还有一个应用双开的知识。其实就是修改包名,因为Android系统是根据包名来判断是否是同一个应用程序。

如果是相同包名,在adb install时就失败了,因为相同包名而不同签名的应用是不能被安装的。这也是为什么我们安装修改后的APK前要先卸载源APK。查看用户安装的所有应用的包名:

1
adb shell pm list packages -3

2. 快速定位资源ID与字符串

一键三连还要充值大会员?我bp不香吗(bushi

使用jadx查看Java源码,成功一键三连有两个条件:

  1. 硬币数至少10个
  2. 是大会员

那我们只要将硬币数修改为10以上,将isvip()方法的返回值改为true就可以了。直接修改Smali代码。

在分析Smali代码的过程中,发现都是用参数寄存器来存储硬币值,不太好直接修改。那我们就修改硬币的递增值。正常来说,按一次“获取硬币”增加一个硬币,我们可以修改为按一次“获取硬币”增加N个硬币。值也不能取太大,最大为0x7f。

isvip()方法的返回值修改为0x1,表示true。

同样方法打包签名,实现了每次增加127枚硬币,点击一键三连成功变蓝。

还有一种方法就是绕过硬币数量判断,这样就不用点击“获取硬币”就可“一键三连”了。

3. 去广告

在开启第三关的前3s,有一个启动广告。由于查看ChanllengeThird.class并没有发现相关内容,所以需要找到该Activity。使用Android SDK中的uiautomatorviewer.bat工具查看该页面的相关信息,发现这个图片资源id为imageView2。

到AK中搜索一下果然有所发现,该页面其实是AdActivity.class

这个页面我们是不想要的,所以在jadx中查看哪里调用了它,再将它去掉即可。我们发现在第一第二关中,都去到了相对应的挑战页面,而到了第三关,就去到了广告页面。我们可以尝试修改为去到挑战三页面。

不错,现在第三关一打开就是挑战三的页面了。

这个更新弹窗完全点不掉,发现在ChallengeThird.classonCreate()方法调用到了它。对于弹窗,一个简单粗暴的方法就是直接在Smali代码中将相关语句删除即可。

还有一种方法,进入checkUpdate()函数分析如何绕过弹窗。那就是现在的APK版本要大于等于它的弹窗指定版本。怎么才能知道它弹窗指定版本是多少呢?它这里开启了一个Service,并有响应请求,可以通过抓包得到弹窗的versionCode。

而应用程序的versionCode在AndroidManifest.xml中,将它的值修改为2及以上即可。但我们发现AK中解析出来的XML文件找不到versionCode,这是因为AXMLPrinter2解析库问题导致,而在jadx或GDA中是可以看到AndroidManifest.xml中的versionCode的。apktool工具还将versionCode和versionName的信息放在了apktool.yml中。

(单纯解压APK文件,AndroidManifest.xml乱码,需要用apktool工具解析APK文件)

直接修改apktool.yml中的versionCode并不可行,因为apktool在重打包时,虽然会读取apktool.yml中的versionCode作为参数,但是后面会被AndroidManifest.xml中“隐藏”的versionCode属性覆盖,所以最好的办法是将AndroidManifest.xml中的versionCode和versionName“可视化”。

出现一号广告弹窗,要不就去论坛,要不就直接退出第三关。但此时按手机上的返回键还是可以关闭弹窗,返回到第三关界面的。但是二号广告弹窗就不能按返回键了。

做粗暴的做法就是把第28行到53行对应的Smali代码全删掉。

当然如果这个工作量比较大的话,可以将展示弹窗的函数注释掉。

这个横幅广告把页面显示的内容遮挡住了,同样用uiautomatorviewer.bat工具查看这条横幅信息。在布局文件中删掉第11行的语句即可,或者添加一个属性android:visibility="gone",再或者修改图片的宽高为0dp。

4. 获取Java代码中变量的值

4.1 还原代码逻辑

关键是看check()方法是如何实现的。

传入的str就是我们输入的密钥,密钥以“flag{”开头,“}”结束。substring就是“flag{xxx}”的xxx。而下面这些语句就是生成密钥的操作,最后与substring比较是否相等。

我们现在最需要知道的就是string存的是什么,它调用了SPUtils.INSTANCE.getString()方法。

1
2
3
4
private static final String PROJECTION = "data";
public final String getString(Context context, String str, String str2) {
return context.getSharedPreferences(PROJECTION, 0).getString(str, str2);
}

进而调用了context.getSharedPreferences()方法,从名字上看是得到本地的共享数据信息。应用存储共享数据到本地有固定的目录,为/data/data/[包名]/shared_prefs。在文件名为“data”取出name值为“id”的字符串,也就是“v5le0n9”。如果为空,则取str2的值。

详情参考这篇文章:安卓学习专栏——从SharedPreferences中读取数据、SharedPreferences的get方法(图文+代码)

所以string存的就是我们的id名。接下来的事就是照搬代码,加密的事情让程序来做就好了,我们只要最后算出来的密钥。

1
adb shell input text "flag{RABeUAJbCwwGAQ==}"

4.2 jeb动态调试

但这节课的主要内容是用jeb动态调试Smali,说起来很久没有对Smali进行动调了,现在可以复习一下。我在Android逆向入门教程中是用Android Studio来动调Smali,使用jeb应该也差不多吧,我们来试试吧。

动态调试第一步,确保该应用是可被调试的。这里有两种方法:

  1. 编辑AndroidManifest.xml中application标签中的android:debuggable=”true”,再重新打包签名;
  2. 修改Android系统全局可调试ro.debuggable=1。

第一种大家应该很熟悉了,实在不会我上面那篇文章也有提到。这次主要讲第二种方法,使用到的工具mprop

选择与手机匹配的架构,目前貌似只有32位的,但在64位的手机上也能用。在全局下开启调试模式意味着系统的所有应用都可被调试。

1
2
3
4
5
6
7
8
9
adb push mprop /data/local/tmp
adb shell
su
cd /data/local/tmp
chmod 755 mprop
./mprop ro.debuggable 1
getprop ro.debuggable
stop
start

jeb的动调有两种模式,一种是普通调试(attach模式),另一种是debug调试(spawn模式)。

4.2.1 jeb普通调试

手机打开需要动调的页面,将APK载入jeb去到Activity对应的Smali代码中。Debugger -> Start或点击小虫子,选择对应进程attach。

鼠标选中需要打断点的行,Ctrl+B下断。我们无非是想得到密钥,所以要进去check()里面,在它判断两个字符串是否相等时可以得到真正的密钥。

虽然它说已安装的程序未标记为“可调试”,但是Android系统开启了全局可调试,所以是可以进行调试的。

按照要求输入伪造密钥,点击“验证”按钮,程序停在断点处,单步执行将真正的密钥存到v0,查看v0的值即可。

结束调试,输入验证一下,密钥正确。

4.2.2 jeb debug调试

用AK得到该程序的包名和入口类:

使用以下命令启动应用进程:

1
adb shell am start -D -n com.zj.wuaipojie/.ui.MainActivity

后面就和普通模式一样操作了,不同的是debug模式可以断在MainActivity.onCreate()中,前提是已经在Debugger -> Start前下好断点。

4.3 Log打印出变量的值

通过在AK中使用Log插桩的方式,从logcat中得到密钥。如果应用没有进行签名校验,这个方法是最快的。

5. 签名校验

原程序所有签名都通过,并在Logcat打印相关签名信息。

当我们在SMALI学习中修改会员,vip_coin并未初始化,需要在<init>()函数中设置成员变量的值。

修改vipEndTime()时,发现时间戳末尾还多了513,我们改好后也增加513试试。

重新安装APK发现安装失败。如果出现以下错误:

1
Failure [INSTALL_FAILED_INVALID_APK: Failed to extract native libraries, res=-2]

AndroidManifest.xml中的application标签中android:extractNativeLibs="false"修改为true或去掉这一属性即可。

安装成功后重新打开第5关,发现页面空白。查看Logcat一直在打印sign,也就是一直在执行checkSign()函数。如果签名与Java代码中sign变量不一致,则执行System.exit(0);。(这个函数不是程序退出函数吗,为什么App没有退出而是不断执行该Actvity的onCreate()函数呢?)

5.1 普通签名校验

那我们先把这一个签名校验绕过。有很多种方法,在onCreate()中改变执行流程,或在checkSign()中改变执行流程都可以。由于后面还会验证普通签名,所以我们在checkSign()中改变if跳转逻辑一劳永逸。

此时再打开SMALI学习发现确实已经修改成功了。

新的API签名校验useNewAPICheck(),查看源码发现它与普通签名校验本质上是一样的,只不过使用了不同摘要算法。

5.2 CRC校验

再来看CRC校验,每次修改Smali代码生成classes.dex中的CRC值都不一样,当然可以在check_crc()中修改if语句逻辑绕过校验,但还有一种方法是I/O重定向,这里会使用到NDK开发的知识。

也就是本来读的是修改后的classes.dex文件中的CRC,但通过I/O重定向可以读到原classes.dex文件中的CRC。经过分析I/O重定向的代码基本上对如何在Android中进行APK的重定向有一定的了解,而这段代码恰好就在lib52pojie.soJava_com_zj_wuaipojie_util_SecurityUtil_hook()中,我们可以编写Smali代码使程序调用这个函数,执行I/O重定向。

在执行check_crc()前加上下面调用hook()函数的代码:

1
2
3
4
5
6
7
sget-object p10, Lcom/zj/wuaipojie/util/ContextUtils;->INSTANCE:Lcom/zj/wuaipojie/util/ContextUtils;  

invoke-virtual {p10}, Lcom/zj/wuaipojie/util/ContextUtils;->getContext()Landroid/content/Context;

move-result-object p10

invoke-static {p10}, Lcom/zj/wuaipojie/util/SecurityUtil;->hook(Landroid/content/Context;)V

接着顺着hook()逻辑将原包放到/data/user/0/{PackageName}/files/base.apk中。这里有一个bug就是我还没放base.apk,直接点验证,发现CRC校验也通过。查看Logcat发现,并没有打印出相关CRC的信息,有可能去到了异常处理函数中,而异常处理函数刚好返回true,所以导致我们看到“通过”二字,这其实也是一个误打误撞绕过CRC的思路,只对本程序有效。怀疑是getPackageCodePath()出错了,但我真不会,只能另寻他路,改变if语句逻辑。

5.3 hash校验

原APKhash值可以抓包获取。

修改Smali代码:

成功安装APK后,需要收到响应包,再进行验证才可以通过。

5.4 So签名校验

这个以我目前的水平不能修改So层代码,所以只能通过I/O重定向或hook方式绕过校验。I/O重定向确实不会,只好用hook方式来解决这个问题了。

5.5 root检测

5.5.1 读取手机编译版本、调试状态

例如读取/system/build.prop中的ro.build.tags是test-keys(测试版),还是release-keys(发布版);去获取ro.debuggable、ro.secure的值检测是否有调试状态。

这个返回结果“release-keys”,代表此系统是官方发布版,否则是非官方发布版。

5.5.2 检查是否存在Superuser.apk和su命令

Superuser.apk是一个被广泛使用的用来root安卓设备的软件,所以可以检查这个App是否存在。su是Linux下切换用户的命令,在使用时不带参数,就是切换到超级用户。通常我们获取root权限,就是使用su命令来实现的,所以可以检查这个命令是否存在。

which是Linux下的一个命令,可以在系统PATH变量指定的路径中搜索某个系统命令的位置并且返回第一个搜索结果。

绕过检测的方法就是将这些hook住,返回false即可。

Android root检测方法小结

5.6 课后作业(xposed检测)

使用jadx分析应用,m108()的代码含义,这个函数是检测手机中是否存在xposed应用。

这个应用的知识点应该就这样了,针对xposed做的一些检测。