目录

破解一个极难的软件

芜湖

喜大普奔,终于是成功破解这个软件。说是破解但是实际上这是个免费软件,但是软件不提供下载功能。以前的老版本软件是有下载功能的,但是没有收藏功能,很不好用,但是现在新版本有了收藏功能,但是没有了下载功能。。。并不知道是怎么想的,而且很多图片都没有了,好在我都给爬下来了~~

这次逆向过程非常不顺利,不过好在经过两个星期的空余时间,总算是解决掉了。现在记录一下整个过程。

有问题下方留言哈~

初次尝试

最开始的时候大概流程是先改安装包的后缀名,改成.zip然后解压出来,得到.dex文件,然后通过dex2jar工具转换成为jar包,接着通过jd-gui工具看到代码。

不过此时发现软件是加固的,想来也是,现在很多软件都会加固,所以我也了解一下常见的去壳方式。对于安卓来说,最稳妥的办法就是dump内存。以前脱壳的时候使用的是一个xposed模块,在虚拟机上面脱壳,我记得好像过程很顺利,但是最后得到的代码并不是很好看。看上去像是被混淆了一样。

考虑到加固技术发展的很快,我没有使用过去的老工具,直接去找了新的dump工具,drizzleDumper。通过adb传输到模拟器上面,运行软件,开启drizzleDumper,结果它突然发疯了,一直输出信息。首次使用这个工具,我也不知道是不是正常情况,所以我等了一会儿。觉得差不多了,就通过Ctrl+C终止了程序运行。此时dump了很多.dex文件,但是似乎只有两个大小。一个是198kb另一个是2028kb

看上去可能是遇到反dump技术。但是不重要,既然已经得到了dex文件,就来看看里面都是什么吧。

https://qiniusave.xint.top/img/image-20210621215940455.png

小的dex里面是这个,明显是360的加固,老朋友了,第一次破的壳就是360的,但是失败了。那是在两年前。现在就再次挑战它。

重要的代码显然不在这里,所以看另一个dex

https://qiniusave.xint.top/img/image-20210621220138398.png

好家伙,看上去好像不太对。首先,并没有看到类似入口的代码,其次代码文件的命名全都变成字母了。另外有些代码并没有反编译出来。这个看上去就很棘手了。对于命名全都变成字母的问题,我一开始认为这个应该是代码经过了混淆。所以,也就继续去分析它的代码。

通过分析,发现软件用了chromium,可能是一个内建的浏览器?但是说实话,使用软件的过程并没有觉得它像是一个浏览器外壳。所以并不知道这个项目在这里的用处是什么。然后就是android.support这个包。此时看到这个包,我以为是安卓开发的公共代码。因为我在别的apk中也见过它,所以直接忽略了它。接下来,就是下面经过"混淆"的代码了。

好在虽然符号名被混淆了,但是内容基本上都反编译出来了。首先进行第一轮筛查,找出了代码文件比较大的。其次通过代码文件import的包,推测他是做什么的。最后筛查出来几个比较有价值的类(诸如Js.class)。

接着通过frida下钩子。结果frida一直提示找不到类,但是通过jd-gui看到类确实在那里。

接着翻阅frida的官方文档,找到了枚举所有类的方法,重定向到文件里面。确实发现了很多字母命名的类,并且也找到了对应的类。

https://qiniusave.xint.top/img/image-20210622222510434.png

但是通过frida确确实实找不到类,此时就觉得很奇怪。后来留意了一下字母类的数量发现,frida只导出了500+个类,但是jd-gui却有足足2500+个类。此时觉得不对劲了。所以考虑是不是dex出了问题,于是考虑使用别的方法dump内存。

后来发现了objection工具,其中有个插件叫做FRIDA-DEXDump

objection - 基于frida的命令行hook工具食用手册 | Mario (strivemario.work)

使用这个工具得到了完全不同的结果

https://qiniusave.xint.top/img/image-20210622223049545.png

看起来很靠谱的样子,使用新的反编译工具jadx工具,打开了dex文件,突然发现之前的操作都是白给。。。

https://qiniusave.xint.top/img/image-20210622223222755.png

得到的代码非常完整,并且原本看到的字母命名的类已经全都没有了,此时突然醒悟过来。那个字母命名的类应该是反编译器确实找不到类的名字,然后自己给它命名了。通过dex2jar转成jar之后再用jd-gui打开,也是完整的代码。由此可见,这个锅必须由drizzleDumper来背。西内!

说起来,之前drizzleDumper也是个好伙计,只不过技术发展太快了,有名气的技术很快就会被针对。

思路

既然已经拿到了真正的代码,就可以开始正常的步骤了,接下来就是一通操作分析了。

基本思路就是先搭建好frida环境,为了方便,这次使用了objection工具,这样就能通过命令行快速验证目标函数是不是需要hook的了。如此一来肯定是很省时间了。通过adb连接夜神安卓模拟器调试软件,找到需要hook的关键函数,并且验证正确与否。

接着开发出相应的xposed模块,就可以开始修改软件了。

不过这地方有坑,两点

  • 夜神模拟器有两个版本,一个是Android5一个是Android7,其中前者自带xposed框架,但是后者没有。只有root权限,尝试给后者安装xposed结果失败了,索性不折腾了。决定最终模块运行在我手机的vmos虚拟机中。不过frida调试则还是在夜神模拟器中。
  • 连接adb的时候,建议找一个低版本的adb,否则会因为版本不匹配的关系不让使用adb,高版本可以连接Android7但是尝试连接Android5会失败。我使用的是r24.0.4版本。

配置基本环境

配置frida

这部分请看之前写的文章使用Frida完成android的hook - 星途 · 小镇 (lonelysinging.github.io)或者看别的教程,此处不再赘述了

使用adb连接虚拟机,并且设置端口转发

adb connect 127.0.0.1:62001
adb forward tcp:27042 tcp:27042
adb forward tcp:27043 tcp:27043

夜神模拟器adb端口默认是62001,如果是Android5的那个,好像是52001这个不确定,需要自行查询一下。

执行frida-ps -U命令能够正确获得进程列表表示frida已经配置完成了。

安装objection

这步可以参考这个博客objection - 基于frida的命令行hook工具食用手册 | Mario (strivemario.work)写的比较好。如果英文水平足够,当然是直接建议去官网比较稳妥。

  • 安装

    pip install objection

    安装时间非常长,不清楚是不是我网络的问题。另外,如果电脑同时安装了python2python3的话,需要注意区分pip的版本,我用的是python3并且只安装了这一个。所以直接执行pip是没有问题的。

  • 指定被操作的进程

    objection -g com.mmzztt.app explore

  • 列出所有activity

    android hooking list activities

    com.apicloud.tencentads.SplashActivity
    com.qq.e.ads.ADActivity
    com.qq.e.ads.LandscapeADActivity
    com.qq.e.ads.PortraitADActivity
    com.qq.e.ads.RewardvideoLandscapeADActivity
    com.qq.e.ads.RewardvideoPortraitADActivity
    // 上面的似乎都是和广告相关的东西
    com.uzmap.pkg.EntranceActivity	// 看起来是什么入口?
    com.uzmap.pkg.LauncherUI	// 似乎是入口相关的
    

    这里看com.uzmap前缀可能有什么戏

见到上面的结果,就证明fridaobjection都已经安装完成了

dump出dex

虽然前面说了怎么dump出dex,但是还是补充上如何操作。

  • 安装

    pip install frida-dexdump

  • 在虚拟机里面运行目标程序

  • 命令行直接运行frida-dexdump等待结束

    https://qiniusave.xint.top/img/image-20210614210601035.png

    好像不是很成功的样子,出现了一些错误。但是好像不是很重要。

  • 此时根据上图结果就能看到他把dex文件放到了用户目录下,使用包名命名的文件夹中了。

    https://qiniusave.xint.top/img/image-20210622223049545.png

    看起来比较顺利

jadx

这是个新鲜的工具,在这里下载skylot/jadx: Dex to Java decompiler (github.com)

这个通过这个软件可以直接打开dex文件,不需要通过dex2jar工具转换了。真的是非常省事儿~

分析代码

分析代码之前,需要先想清楚需求是什么。此次想要修改的软件是一个看图的软件,但是我不准备直接给出软件名称,如果想知道是什么软件,请聪明的读者思考一下安卓包名的命名规则是什么~

这个软件就是一个看写真图片的软件,现在我想修改软件,使之可以实现双击下载功能。并且能够关闭掉讨厌的更新提示。

现在明确了目标,所以接下来需要关注的部分就是软件下载图片的部分,以及双击会执行的代码。至于自动更新,一开始是没啥头绪的,软件的入口并不是很明确,所以我不准备按照执行流程分析了。

相关目录分析

我一般在具体分析软件之前,会先去看看它的缓存目录,或者配置文件什么的地方,有时候能够发现非常有用的线索。如何发现相关目录呢?一般来说,重要的目录就是Android/data/包名/这样的目录,如果目标软件在/sdcard/下面也建立的文件夹的话,也可以看看。如果有root权限的话,可以去软件的/data/data/包名底下看看,有时候可以看到配置文件什么的。能够提供相关线索。

而通过objection很方便就能得到相关目录。啥都有了,此时通过ES文件浏览器什么的去转一圈,总能发现好玩的。

https://qiniusave.xint.top/img/image-20210624222217118.png

尤其是这次,直接找到了软件缓存文件的地方,就是Android/data/包名/

https://qiniusave.xint.top/img/image-20210624221641554.png

看到这个文件名和大小,就已经瞬间明白这个是什么了。通过图片的方式打开,果不其然就是缓存的图片。而其文件的命名,不用看也能知道是32位的md5。对这个东西已经太熟悉了。

所以,接下来,就不需要考虑自己下载了,只需要通过代码找到操作缓存文件的地方即可,然后直接复制缓存文件,重命名即可得到"下载"的文件。

找有价值的函数

https://qiniusave.xint.top/img/image-20210624220518576.png

没什么好说的,基本上反编译工具已经把类名和函数名反编译过来了,所以基本上把大概代码浏览一遍,发现这样明显的类了,接下来主要是验证类和函数了。基本思路就是通过objection对整个类的所有函数都下hook,然后再操作软件,看看会调用到哪个函数。确定函数价值。

例如通过以下命令操作

# 列出类的所有函数
android hooking list class_methods com.uzmap.pkg.uzmodules.photoBrowserSu.PhotoBrowser

# hook方法,列出其参数和返回值,还有堆栈
android hooking watch class_method com.uzmap.pkg.uzmodules.photoBrowserSu.ImageBrowserAdapter.$init --dump-args --dump-backtrace --dump-return

# hook整个类的所有方法
android hooking watch class android.support.v4.view.PagerAdapter

列出类的所有函数,对比和jadx的结果,确保找到的类和方法是正确的,别像我一样傻乎乎的对着一堆不存在的方法下hook

hook方法的话,则能看到方法的返回值、参数、以及调用堆栈。前面两者肯定不用考虑非常重要,而对于堆栈,如果是想要分析软件类和方法的调用关系,则是非常好用的信息。能够帮你理清谁调用谁,通过这些信息,也能进一步推测反编译不出类名的类是干什么的。如果想要实现复杂的功能,可能确实需要明白目标软件是怎么工作的。

hook整个类的话,看不到方法的参数和返回值。但是能够帮忙确定类中哪个方法被调用了,也是非常重要的。能够一次性筛查出重要的类。

获取url列表

经过一番查找,发现软件工作原理是有一个缓存管理类。需要这张图片的话,就会问这个缓存管理类要,如果已经有缓存的图的话,直接返回路径,然后载入。否则的话,才会下载。但是经过简单看代码之后,并没有发现一个保存取md5文件名的数组,但是发现了这个

https://qiniusave.xint.top/img/image-20210624230628010.png

这个构造函数有一个参数是一个ArrayList通过hook这个函数,能够发现里面存的是原始的url。比较麻烦,但是咱们上面已经发现了文件名就是什么东西取了md5,是不是就是urlmd5之后直接做文件名呢?

hook函数之后,可以直接看到参数,对于java自带的类型,objection可以直接看到其内容。拿到第一个元素,取md5之后,对比其缓存的文件,确实发现了同名文件。所以,只需要对这个数组中的元素取md5就能在缓存目录找到对应的文件,之后复制走,加个后缀就能作为正常图片打开了。

确定当前图片索引

接下来就是要知道现在看的是哪张图片,首先引入眼帘的就是浏览界面的进度。但是该怎么找到它呢?

https://qiniusave.xint.top/img/image-20210624231324273.png

最快速的办法,就是通过布局文件找。

通过apktool解包apk文件,之后得到了布局文件

https://qiniusave.xint.top/img/image-20210624231515528.png

结果如您所见,似乎并没有相关的布局数据。所以,考虑这部分可能是动态产生的布局。所以,没办法,还是得继续找代码。其实如果顺利的话,通过布局文件里面的id属性,能够直接找到对应的类,这样对这个类中的方法下hook就能直接获取到当前图片的索引了。

依旧是上面的老办法,继续对可疑的类下钩子。最后发现了一个有用的方法

https://qiniusave.xint.top/img/image-20210624231855129.png

从名字上看,就能知道这个可能是用来设定当前看的这张图的索引的。第二个参数position,应该就是当前图片索引。对这个函数下断点

https://qiniusave.xint.top/img/image-20210624232142901.png

能够看到这个参数就是当前看到的图片的索引减一。那么它的值应该对应的就是数组的下标了。

找到双击操作

既然收集到了需要的数据,接下来只需要找到一个触发函数,把相关的复制文件的逻辑放进去,就完成了这个目标。找双击函数的思路,就是看photobrower这个类,因为软件可以双击放大。

https://qiniusave.xint.top/img/image-20210624232855877.png

找到了这个函数onDoubleTap(),但是,这个函数是一个匿名对象里面方法。并且我没有找到对应的hook方法,所以只能走偏方了。我注意到它几乎一定会执行smoothScale()函数,我只需要hook它就行了。

至此,就通过objection找到了所有需要的函数,于是就可以开始写xposed模块了。

升级功能的屏蔽

对于这个,通过字符串搜索,确实找到了对应的配置类。但是下的钩子并没有被触发,因为软件是平台生成的,所以代码不被使用也很正常。后面分析发现,更新相关的方法名反编译失败了。也就是说,即使找到了对应的逻辑也不能正确的hook,这样的话就没有意义了。

https://qiniusave.xint.top/img/image-20210624233906344.png

但是也并不是没有办法了,观察这个升级框,点击返回什么的都不能关闭它,只能点击立即更新,之后就能继续使用软件了。但是会后台下载安装包,之后更新。不过在下载的时候也可以正常使用软件。所以,另一个思路就是让这个弹窗弹不出来。

https://qiniusave.xint.top/img/image-20210624233935129.png

通过分析布局文件,依旧是没找到对应的布局,也就找不到对应的类了。这样的话,只能通过别的方法了。考虑到动态的布局,最终也会是一个布局文件。所以,是否有办法把运行的界面布局dump出来呢?其实这个是最近使用Autojs软件的时候知道的功能。去百度了如何dump布局,果然通过adb是可以dump布局的。(实际上,Android Studio是有这样的功能的,但是一直失败,不清楚是为什么)

下面两个命令需要进去adb shell

uiautomator dump /sdcard/ui.xml
# 获取顶层的布局

dumpsys window | grep  mCurrentFocus
# 获取顶层的Activity

后者只有一开始发现的com.uzmap.pkg.LauncherUI 显然也是没什么用的。但是前者,其中发现了一些id

https://qiniusave.xint.top/img/image-20210624234943313.png

众所周知,这个id是注册在public.xml这样的文件中的,找到了如下图这样的东西。后面的id是十六进制,转换成十进制,然后搜索R.java文件,就能找到具体是那里使用了这个布局。

https://qiniusave.xint.top/img/image-20210624235036862.png

最终发现,直接搜索id就能直接找到代码。。。转换进制什么的完全没有必要。。。

https://qiniusave.xint.top/img/image-20210624235433486.png

然后最终在附近的类中看到了下面这个方法

https://qiniusave.xint.top/img/image-20210620113953432.png

hook了之后,确定了这个就是弹出更新弹窗的方法。接下来,就可以开始正式开发xposed模块了

xposed模块

直接贴出代码,若是对java没有一些了解,大概完全不用考虑学习编写模块了。但是如果会java的话,下面的代码应该能够直接看懂。

对于xposed框架代码的编写,主要就是用到了java的反射。我理解的反射就是把类的信息给映射成一个对象,通过这个对象可以得到对应的类信息。由此,可以得到类的结构。

另外,在比较重要的一点就是,如果代码是加固的,那就稍微麻烦一点。需要拿到加固的类加载器,具体看这里使用xposed来hook使用360加固的应用 - 简书 (jianshu.com)

还有,就是在手机上的vmos虚拟机,xposed的日志不能正常工作,所以我写了一个writefile()方法,用来做日志输出。

除此之外,没有什么值得注意的了。

代码

package com.example.startu.myapplication233;

import android.app.Application;
import android.content.Context;
import android.os.Environment;
import android.view.View;
import android.widget.Toast;

import java.io.File;
import java.io.FileOutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XC_MethodReplacement;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;

public class HookTest implements IXposedHookLoadPackage {

    public void writefile(String str) throws Throwable{
        String sdCardDir =Environment.getExternalStorageDirectory().getAbsolutePath();
        File saveFile = new File(sdCardDir, "aaaa.txt");
        FileOutputStream outStream = new FileOutputStream(saveFile,true);
        outStream.write((str+"\n").getBytes());
        outStream.close();
    }
	// 这地方没有去找软件里面的方法,觉得太麻烦了。所以直接去网络上找到代码复制过来了
    private static String getMD5(String info) {
        try {
            //获取 MessageDigest 对象,参数为 MD5 字符串,表示这是一个 MD5 算法(其他还有 SHA1 算法等):
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            //update(byte[])方法,输入原数据
            //类似StringBuilder对象的append()方法,追加模式,属于一个累计更改的过程
            md5.update(info.getBytes("UTF-8"));
            //digest()被调用后,MessageDigest对象就被重置,即不能连续再次调用该方法计算原数据的MD5值。可以手动调用reset()方法重置输入源。
            //digest()返回值16位长度的哈希值,由byte[]承接
            byte[] md5Array = md5.digest();
            //byte[]通常我们会转化为十六进制的32位长度的字符串来使用,本文会介绍三种常用的转换方法
            return bytesToHex1(md5Array);
        } catch (NoSuchAlgorithmException e) {
            return "";
        } catch (UnsupportedEncodingException e) {
            return "";
        }
    }

    private static String bytesToHex1(byte[] md5Array) {
        StringBuilder strBuilder = new StringBuilder();
        for (int i = 0; i < md5Array.length; i++) {
            int temp = 0xff & md5Array[i];//TODO:此处为什么添加 0xff & ?
            String hexString = Integer.toHexString(temp);
            if (hexString.length() == 1) {//如果是十六进制的0f,默认只显示f,此时要补上0
                strBuilder.append("0").append(hexString);
            } else {
                strBuilder.append(hexString);
            }
        }
        return strBuilder.toString();
    }

   	// 依旧不想使用软件里面的复制方法,直接复制网上的代码了。但是可能在Android11上面出现问题,具体看下面说明
    public void copyfile(File fromFile, File toFile,Boolean rewrite ) throws Throwable
    {
        if (!fromFile.exists()) {
            return;
        }
        if (!fromFile.isFile()) {
            return ;
        }
        if (!fromFile.canRead()) {
            return ;
        }
        if (!toFile.getParentFile().exists()) {
            toFile.getParentFile().mkdirs();
        }
        if (toFile.exists() && rewrite) {
            toFile.delete();
        }

        try {
            java.io.FileInputStream fosfrom = new java.io.FileInputStream(fromFile);
            java.io.FileOutputStream fosto = new FileOutputStream(toFile);
            byte bt[] = new byte[1024];
            int c;
            while ((c = fosfrom.read(bt)) > 0) {
                fosto.write(bt, 0, c); //将内容写到新文件当中
            }
            fosfrom.close();
            fosto.close();

        } catch (Exception ex) {
            writefile(ex.toString());
        }
    }

    public void handleLoadPackage(final LoadPackageParam loadPackageParam) throws Throwable {
        XposedBridge.log("Loaded app: " + loadPackageParam.packageName);
        // writefile("启动包: "+loadPackageParam.packageName);
        if (loadPackageParam.packageName.equals("com.mmzztt.app")) {
            writefile("发现目标: "+loadPackageParam.packageName);
            Class<?> clazz = XposedHelpers.findClass("com.stub.StubApp", loadPackageParam.classLoader);
            // Class clazz = loadPackageParam.classLoader.loadClass("ceui.lisa.download.IllustDownload");
            XposedHelpers.findAndHookMethod(clazz, "interface7",Application.class,Context.class, new XC_MethodHook() {
                protected void beforeHookedMethod(XC_MethodHook.MethodHookParam param) throws Throwable {
                    super.beforeHookedMethod(param);
                    writefile("进入函数: ");
                    Context context = (Context) param.args[1];
                    //获取classloader,之后hook加固后的就使用这个classloader
                    ClassLoader realClassLoader = context.getClassLoader();
                    hookCheckoutXposed(realClassLoader,loadPackageParam);
                    writefile("hook结束");
                }

                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    // param.setResult("你已被劫持");
                    // writefile("新的图片:");
                }
            });
        }
    }

    private ArrayList<String> mImagePaths;
    private int pos;
    private  String cache_files="/sdcard/Android/data/com.mmzztt.app/cache/UIPhotoViewer/";
    private String mmttzz_files = "/sdcard/mmzztt/";
    private boolean is_copy = false;
    Context context;

    public void hookCheckoutXposed(ClassLoader classLoader,final LoadPackageParam loadPackageParam) throws Throwable {
        writefile("进入hook函数");

        Class Context_c = loadPackageParam.classLoader.loadClass("android.content.Context");

        // 获取 Context
        XposedHelpers.findAndHookMethod("com.uzmap.pkg.uzcore.uzmodule.UZModule", classLoader, "initPlatform", Context_c, new XC_MethodHook() {
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                if(param.args[0] != null) {
                    context = (Context)param.args[0];
                    cache_files = context.getExternalCacheDir().getAbsolutePath() + "/UIPhotoViewer/";
                    writefile("cache_files: "+cache_files);
                }
            }
        });

        XposedHelpers.findAndHookMethod("android.support.v4.view.PagerAdapter", classLoader, "setPrimaryItem", View.class,int.class,Object.class, new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                if(param.args[1] == null){
                    pos = 0;
                }else{
                    pos = (int)param.args[1];
                }
                is_copy = false;

            }
        });

        XposedHelpers.findAndHookMethod("com.uzmap.pkg.uzmodules.photoBrowserSu.view.largeImage.LargeImageView", classLoader, "smoothScale", float.class,int.class,int.class, new XC_MethodHook() {
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                writefile("双击了");

                String url = mImagePaths.get(pos);
                if(url == null){
                    Toast.makeText(context,"获取图片url失败",Toast.LENGTH_SHORT).show();
                    return ;
                }
                String md5_string = getMD5(url);
                String md5_name = md5_string+".jpg";
                String file_name = md5_name;

                writefile("定位"+pos+", 内容: " + url + ", md5: "+md5_string);

                String[] l_list = url.split("/");
                if(l_list != null){
                    file_name = l_list[l_list.length-1];
                }
                writefile("file_name: "+file_name+", l_list.length: "+l_list.length);
                if(!is_copy) {
                    copyfile(new File(cache_files + md5_string), new File(mmttzz_files + file_name), true);
                    Toast.makeText(context,"保存图片"+file_name,Toast.LENGTH_SHORT).show();
                    is_copy = true;
                }
            }
        });

        Class UZModuleContext_c = loadPackageParam.classLoader.loadClass("com.uzmap.pkg.uzcore.uzmodule.UZModuleContext");
        XposedHelpers.findAndHookMethod("com.apicloud.dialogBox.DialogBox", classLoader, "jsmethod_alert",UZModuleContext_c, new XC_MethodReplacement() {
            @Override
            protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
                writefile("禁止一个 alert弹窗");
                Toast.makeText(context,"你是真的想更新嘛?",Toast.LENGTH_SHORT).show();
                return null;
            }
        });

        Class ImageLoader_c = loadPackageParam.classLoader.loadClass("com.uzmap.pkg.uzmodules.photoBrowserSu.ImageLoader");
        Class ImageBrowserAdapter_c = loadPackageParam.classLoader.loadClass("com.uzmap.pkg.uzmodules.photoBrowserSu.ImageBrowserAdapter");

        XposedHelpers.findAndHookConstructor(ImageBrowserAdapter_c, Context_c,UZModuleContext_c,ArrayList.class,ImageLoader_c,String.class,  new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                writefile("进入 ImageBrowserAdapter 的构造函数了");
                try {
                    mImagePaths = (ArrayList<String>) param.args[2];
//                    for(int i=0;i<mImagePaths.size();i++){
//                        writefile(mImagePaths.get(i));
//                    }
                    writefile("数组长度: " + mImagePaths.size());
                }catch (Exception e){
                    writefile(e.toString());
                }
            }
        });
    }
}

弹出toast

这部分需要获取目标软件的Context,这个需要分析代码,一般来说,没有加固的代码,在入口类中就能找到。但是如果是加固之后的,可能需要找加固方案的代码了。具体看上面的代码,找到什么方法参数是Context的就能获取到,当然是不是任意一个Context都行我就不清楚了。

关于复制文件

手机更新Android 11之后,对应用就不能随意访问Adnroid/data/目录了,所以上面的代码可能会复制文件失败,因为模块并不是目标软件,所以可能会有权限问题,当然只是猜测。遇到这种情况,可以试试hook目标软件自己的复制文件方法。或者拿到url自己下载,或者直接拿到输出流写到自己的目录。

操作过程中可能需要的操作

启用ADB网络调试

TCP/IP方式:
setprop service.adb.tcp.port 5555
stop adbd
start adbd
 
usb方式:
setprop service.adb.tcp.port -1
stop adbd
start adbd

from:(2条消息) Android 网络调试 adb tcpip 开启方法_Shawn Kong的专栏-CSDN博客

禁用 SELinux

setenforce 0
  • 级别0:表示Permissive模式。
  • 级别1:表示Enforcing模式。
  • 至于disabled模式和其他模式的切换只能修改配置文件,命令不起作用。其次,修改完成之后,必须重启系统才能够生效。

from: Android查看SELinux状态及关闭SELinux - 简书 (jianshu.com)

apktool

解包

apktool.jar d Shaft_3.1.5.apk -o Sha

打包

apktool.bat b [资源文件夹] [打包生成的apk文件] 

from:(2条消息) 使用ApkTool_一起进步的博客-CSDN博客_apktool

BusyBox

meefik/busybox: BusyBox for Android (github.com)

添加库

repositories {
    jcenter()
}

compileOnly 'de.robv.android.xposed:api:82'
compileOnly 'de.robv.android.xposed:api:82:sources'

新项目需要添加的

        <meta-data
            android:name="xposedmodule"
            android:value="true" />
        <meta-data
            android:name="xposeddescription"
            android:value="模块描述" />
        <meta-data
            android:name="xposedminversion"
            android:value="54" />

旧版本下载地址

Android Studio 下载文件归档 | Android 开发者 | Android Developers (google.cn)

3.2.1版本

遇到的问题

使用旧版本 初始化项目的时候就会出错

Unable to resolve dependency for ‘:app@debug/compileClasspath‘: Could not resolve com.android.suppor_达帮主-CSDN博客

操作过程的一些记录

  • 安装

    pip install frida-dexdump

  • dump dex

    frida-dexdump

  • 通过jafx-gui打开dex

    https://qiniusave.xint.top/img/image-20210614212913273.png

    绝了,这个软件的反反编译能力我觉得挺强的

  • 终于找到了关键方法

    com.uzmap.pkg.uzmodules.photoBrowserSu.ImageDownLoader.saveFile(java.io.InputStream, java.lang.String)

  • 通过分析xml,找到了菜单

  • 找到了取消按钮的点击事件

    https://qiniusave.xint.top/img/image-20210614233042906.png

    但是通过这个方法并不能得到什么有用的信息,好像拿不到需要的图片。所以考虑别的方法。

    我有注意到似乎有双击操作。。。

  • 使用前面的xposed模板,写入类名和方法,一次成功

  • Xposed第一课(微信篇) hook含有多个参数的方法 - 简书 (jianshu.com)

  • 通过xposed成功拿到双击会触发的方法,感觉快看到胜利的曙光了

  • https://qiniusave.xint.top/img/image-20210615233012378.png

  • 上一步找到了双击,接下来就要获取到当前正在看的图片了

    https://qiniusave.xint.top/img/image-20210616230425945.png

    此处找到了在载入图片完成之后会调用的方法了,但是这个方法并不好用

    于是找到了更好用的办法

    android.support.v4.view.ViewPager;

    https://qiniusave.xint.top/img/image-20210616235043719.png

    这两个是加载和预加载,通过这两个可以得到加载的具体的具体图片

  • ImageBrowserAdapter 构造函数可以获得列表,但是其中都是url需要取md5

  • uiautomator dump /sdcard/ui.xml 命令可以得到当前最顶层的布局

  • dumpsys window | grep mCurrentFocus 命令可以得到最顶层的Activity

  • 禁止弹窗