《安卓逆向这档事》番外实战篇2-【2024春节】解题领红包活动,启动!

查看 136|回复 10
作者:soughing   


一、课程目标
frida实战安卓初级题与中级题
二、工具
1.安卓初级题与中级题
2.jadx-gui
3.VS Code
4.jeb
三、课程内容
1.初级题1
关键函数解析
public static String extractDataFromFile(String str) {
    // 定义局部变量str2
    String str2;
    // 定义局部变量indexOf
    int indexOf;
    try {
        // 创建一个用于读取文件的RandomAccessFile实例,以只读模式打开
        RandomAccessFile randomAccessFile = new RandomAccessFile(str, "r");
        // 获取文件的长度
        long length = randomAccessFile.length();
        // 从文件末尾开始向前搜索,搜索范围限制为文件末尾的30个字节内
        for (long max = Math.max(length - 30, 0L); max
方法1:
var ClassName=Java.use("com.zj.wuaipojie2024_1.YSQDActivity");
console.log(ClassName.extractDataFromFile("/data/user/0/com.zj.wuaipojie2024_1/files/ys.mp4"));
方法2:
android intent launch_activity com.zj.wuaipojie2024_1.YSQDActivity
2.初级题2
关键函数解析
// 定义一个静态字节数组o,用于与签名校验数据异或
public static byte[] o = {86, -18, 98, 103, 75, -73, 51, -104, 104, 94, 73, 81, 125, 118, 112, 100, -29, 63, -33, -110, 108, 115, 51, 59, 55, 52, 77};
@Override
public void onCreate(Bundle bundle) {
    byte[] bArr; // 定义一个字节数组变量bArr,用于存储处理结果。
    Signature[] signatureArr; // 定义一个Signature数组变量signatureArr,用于存储应用签名信息。
    super.onCreate(bundle);
    setContentView(R.layout.activity_flag); // 设置当前Activity使用的布局。
    byte[] bArr2 = o; // 将静态字节数组o的引用赋给bArr2。
    try {
        // 尝试获取当前应用的签名信息。
        signatureArr = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES).signatures;
    } catch (PackageManager.NameNotFoundException unused) {
        // 如果找不到包名,将bArr初始化为一个空的字节数组。
        bArr = new byte[0];
    }
    // 检查signatureArr是否非空且至少包含一个元素。
    if (signatureArr != null && signatureArr.length >= 1) {
        // 将第一个签名信息转换为字节数组。
        byte[] byteArray = signatureArr[0].toByteArray();
        // 分配一个ByteBuffer,大小与bArr2相同,用于存储异或操作的结果。
        ByteBuffer allocate = ByteBuffer.allocate(bArr2.length);
        // 对bArr2和byteArray进行异或操作,并将结果存储在allocate中。
        for (int i = 0; i
方法1:
android intent launch_activity com.kbtx.redpack_simple.FlagActivity
方法2:
function hookTest1(){
    var Arrays = Java.use("java.util.Arrays");
    Java.choose("com.kbtx.redpack_simple.WishActivity", {
        onMatch: function(obj){
                console.log("obj的值: " + obj);
            var oAsString = Arrays.toString(obj.o.value);
            console.log("o字段的值: " + oAsString);
            obj.o.value = Java.array('I', [90, 90, 122]);
        },
        onComplete: function(){
        }
    });
}
3.中级题
放一张流程图帮助理解

根据logcat发现是一个错误的dex,checksum验证失败,利用DexRepair修复头文件
java -jar DexRepair.jar /path/to/dex
塞回安装包,发现还是错误,仔细检查代码,发现读不到dex数据,对C类里read方法的decode.dex修改成1.dex
关键函数解析
public boolean checkPassword(String str) {
    try {
        // 打开assets目录下的"classes.dex"文件作为InputStream
        InputStream open = getAssets().open("classes.dex");
        // 创建一个字节数组,大小为可读取的字节数,即整个文件的大小
        byte[] bArr = new byte[open.available()];
        // 从InputStream中读取数据到字节数组中
        open.read(bArr);
        // 创建一个指向应用的内部目录("data"目录)中的"1.dex"文件的File对象
        File file = new File(getDir("data", 0), "1.dex");
        // 创建一个向该文件写入数据的FileOutputStream
        FileOutputStream fileOutputStream = new FileOutputStream(file);
        // 将字节数组bArr的内容写入到"1.dex"文件中
        fileOutputStream.write(bArr);
        // 关闭文件输出流
        fileOutputStream.close();
        // 关闭文件输入流
        open.close();
        // 使用DexClassLoader加载"1.dex"文件,并调用其中一个类的静态方法
        // "com.zj.wuaipojie2024_2.C"是类的全路径名
        // "isValidate"是方法名,它接收一个Context对象,一个String对象和一个int数组作为参数
        // 调用方法并传入当前Context(this),密码字符串str,以及一个从资源数组R.array.A_offset中获取的int数组
        String str2 = (String) new DexClassLoader(file.getAbsolutePath(),
                                                  getDir("dex", 0).getAbsolutePath(),
                                                  null,
                                                  getClass().getClassLoader())
                                    .loadClass("com.zj.wuaipojie2024_2.C")
                                    .getDeclaredMethod("isValidate", Context.class, String.class, int[].class)
                                    .invoke(null, this, str, getResources().getIntArray(R.array.A_offset));
        // 检查返回的字符串是否为null或者不以"唉!"开头
        if (str2 == null || !str2.startsWith("唉!")) {
            // 如果是,则认为密码检查失败
            return false;
        }
        // 如果密码检查成功,则更新UI组件tvText的文本为返回的字符串,并将myunlock组件设为不可见
        this.tvText.setText(str2);
        this.myunlock.setVisibility(8);
        // 返回true表示密码检查成功
        return true;
    } catch (Exception e) {
        // 捕获到异常,打印异常堆栈信息,并返回false表示密码检查失败
        e.printStackTrace();
        return false;
    }
}
public static String isValidate(Context context, String str, int[] iArr) throws Exception {
    try {
        // 尝试从动态加载的DEX中获取并调用静态方法
        // getStaticMethod是一个自定义方法,用于根据给定参数动态获取特定的静态方法
        // 参数包括上下文(context),一个整型数组(iArr),类的全名("com.zj.wuaipojie2024_2.A"),方法名("d"),以及该方法的参数类型(Context.class, String.class)
        // 该方法预期返回一个Method对象,该对象代表了一个静态方法,可以被调用
        // invoke方法用于执行这个静态方法,传入的参数为null(因为是静态方法,所以不需要实例),上下文(context)和字符串(str)
        // 方法执行的结果被强制转换为String类型,并作为isValidate方法的返回值
        return (String) getStaticMethod(context, iArr, "com.zj.wuaipojie2024_2.A", "d", Context.class, String.class).invoke(null, context, str);
    } catch (Exception e) {
        // 如果在尝试获取或调用方法时发生异常,记录错误信息到日志,并打印堆栈跟踪
        Log.e(TAG, "咦,似乎是坏掉的dex呢!");
        e.printStackTrace();
        // 出现异常时,方法返回一个空字符串
        return "";
    }
}
private static Method getStaticMethod(Context context, int[] iArr, String str, String str2, Class... clsArr) throws Exception {
    try {
        // read方法用于读取原始DEX文件,然后fix方法根据提供的iArr参数和上下文来处理数据。
        File fix = fix(read(context), iArr[0], iArr[1], iArr[2], context);
        // 获取应用的当前类加载器
        ClassLoader classLoader = context.getClass().getClassLoader();
        // 获取或创建一个名为"fixed"的目录,用于存放处理过的DEX文件
        File dir = context.getDir("fixed", 0);
        // 使用DexClassLoader动态加载修复后的DEX文件。
        // fix.getAbsolutePath()是DEX文件的路径,dir.getAbsolutePath()是优化后的DEX文件存放路径。
        // null是父类加载器,classLoader是应用的当前类加载器,作为新的类加载器的父加载器。
        Method declaredMethod = new DexClassLoader(fix.getAbsolutePath(), dir.getAbsolutePath(), null, classLoader)
                                .loadClass(str) // 加载指定的类
                                .getDeclaredMethod(str2, clsArr); // 获取指定的方法
        // 删除处理过的DEX文件和其在"fixed"目录下的优化版本,以清理临时文件
        fix.delete();
        new File(dir, fix.getName()).delete();
        // 返回找到的Method对象
        return declaredMethod;
    } catch (Exception e) {
        // 如果过程中发生任何异常,打印堆栈跟踪并返回null
        e.printStackTrace();
        return null;
    }
}
private static File fix(ByteBuffer byteBuffer, int i, int i2, int i3, Context context) throws Exception {
    try {
        // 获取或创建应用内"data"目录
        File dir = context.getDir("data", 0);
        // 使用自定义的D.getClassDefData方法获取类定义数据,然后从返回的HashMap中获取"class_data_off"的值
        int intValue = D.getClassDefData(byteBuffer, i).get("class_data_off").intValue();
        // 获取类数据,并根据给定的索引修改指定的直接方法的访问标志
        //已知i2是3,也就意味着访问的是直接方法列表中的第四个方法(因为数组索引是从0开始的)
        //i3则是方法的偏移,注意要转换成ULEB128格式
        HashMap classData = D.getClassData(byteBuffer, intValue);
        classData.get("direct_methods")[i2][2] = i3;
        // 使用自定义的D.encodeClassData方法将修改后的类数据编码回字节数组
        byte[] encodeClassData = D.encodeClassData(classData);
        // 将ByteBuffer的位置设置到类数据偏移处,并将修改后的类数据写回ByteBuffer
        byteBuffer.position(intValue);
        byteBuffer.put(encodeClassData);
        // 设置ByteBuffer的位置到32,从这个位置开始读取数据,用于SHA-1哈希计算
        byteBuffer.position(32);
        byte[] bArr = new byte[byteBuffer.capacity() - 32];
        byteBuffer.get(bArr);
        // 使用自定义的Utils.getSha1方法计算数据的SHA-1哈希
        byte[] sha1 = Utils.getSha1(bArr);
        // 将ByteBuffer的位置设置到12,并将计算出的SHA-1哈希写入ByteBuffer
        byteBuffer.position(12);
        byteBuffer.put(sha1);
        // 使用自定义的Utils.checksum方法计算校验和
        int checksum = Utils.checksum(byteBuffer);
        // 将ByteBuffer的位置设置到8,并将校验和写入ByteBuffer(注意校验和的字节顺序被反转以符合DEX文件格式)
        byteBuffer.position(8);
        byteBuffer.putInt(Integer.reverseBytes(checksum));
        // 获取ByteBuffer中的字节数组
        byte[] array = byteBuffer.array();
        // 创建一个新的DEX文件,并将修改后的数据写入该文件
        File file = new File(dir, "2.dex");
        FileOutputStream fileOutputStream = new FileOutputStream(file);
        fileOutputStream.write(array);
        fileOutputStream.close();
        // 返回新创建的DEX文件
        return file;
    } catch (Exception e) {
        // 在发生异常时打印堆栈跟踪并返回null
        e.printStackTrace();
        return null;
    }
}
修复后的代码:
public static String d(Context context, String str) {
    MainActivity.sSS(str);//frida检测
    String signInfo = Utils.getSignInfo(context);//签名校验
    if (signInfo == null || !signInfo.equals("fe4f4cec5de8e8cf2fca60a4e61f67bcd3036117")) {
        return "";
    }
        //输入的字符串与运算后的048531267进行对比
    StringBuffer stringBuffer = new StringBuffer();
    int i = 0;
    while (stringBuffer.length()
方法1:
function hook_delete() {
    Java.perform(function () {
        // 获取java.io.File类的引用
        var File = Java.use("java.io.File");
        // 挂钩delete方法
        File.delete.implementation = function () {
            // 打印尝试删除的文件路径
            console.log("Deleting file: " + this.getPath());
            return true;
        };
    });
}
function hook_resources() {
    Java.perform(function () {
        // 获取android.content.res.Resources类的引用
        var Resources = Java.use("android.content.res.Resources");   
        // 挂钩getIntArray方法
        Resources.getIntArray.overload('int').implementation = function (id) {
            // 换成b方法的偏移
            var replacementArray = Java.array('int', [0, 3, 8108]);   
            // 打印新的返回值
            console.log("Replacing getIntArray result with: " + JSON.stringify(replacementArray));   
            // 返回新的数组替代原始的返回值
            return replacementArray;
        };
    });
}
方法2:
利用python脚本算出ULEB128对应的地址,利用010editor手动修改偏移,然后在加密网站上直接跑就行,因为是标准加密
https://gchq.github.io/CyberChef/#recipe=SHA1(80)MD5()&input=cGFzc3dvcmQr5L2g55qEdWlk
ULEB128(Unsigned Little-Endian Base 128)是一种用于编码32位或64位无符号整数的可变长度编码方案。它主要用在编译器和二进制格式中,如DWARF调试信息和Android的DEX文件格式。ULEB128的目的是以尽可能少的字节表示一个数值,特别是对于小的数值非常有效。
dex结构体
安卓源码中的dalvik/libdex/DexFile.h这里可以找到dex文件的数据结构,解析后的几个结构体如下:
[table]
[tr]
[td]部分名称[/td]
[td]描述[/td]
[/tr]
[tr]
[td]dex_header

方法, 数组

笨笨家的唯一   

大佬,谢谢您的解答。
但是现在我对于这个课程有个疑问,基本现在所有的APP都加了壳的
现在就是卡住这里,不知如果弄这个 虽然有其他人写的脱壳程序,但是根本不知道怎么弄的,类似于脚本小子的感觉。
ojp2008520   

如果出现下图的错误,并且错误堆栈也能对应的话



error01.png (136.12 KB, 下载次数: 0)
下载附件
2024-4-16 14:14 上传


依据堆栈提示,找到关键函数【getStaticMethod】,跳转至【read】


error03.png (25.8 KB, 下载次数: 0)
下载附件
2024-4-16 14:17 上传



error02.png (23.96 KB, 下载次数: 0)
下载附件
2024-4-16 14:17 上传

发现【read】函数需要读取【/data/user/0/com.zj.wuaipojie2024_2/app_data/】目录下的【decode.dex】文件(也就是源文件,没解密的那个),而这里面没有,所以会报错。


error04.png (27.61 KB, 下载次数: 0)
下载附件
2024-4-16 14:20 上传

解决办法就是把上面的【1.dex】复制一个,然后改名改成【decode.dex】,这样就可以继续了



image.png (54.19 KB, 下载次数: 0)
下载附件
2024-4-16 14:30 上传

后面视频里面讲了,在44分钟30秒的时候讲的(才看到)
如果我是DJ?   

视频剪辑中,晚点上传
ojp2008520   

大佬,谢谢您的解答。
但是现在我对于这个课程有个疑问,基本现在所有的APP都加了壳的
现在就是卡住这里,不知如果弄这个 虽然有其他人写的脱壳程序,但是根本不知道怎么弄的,类似于脚本小子的感觉。
西枫游戏   

大佬666啊
dhsfb   


ojp2008520 发表于 2024-2-26 21:18
大佬,谢谢您的解答。
但是现在我对于这个课程有个疑问,基本现在所有的APP都加了壳的
现在就是卡住这里 ...

后面会讲脱壳的,放心
笨笨家的唯一   


正己 发表于 2024-2-26 22:38
后面会讲脱壳的,放心

现在脱壳修复查到资料 学会了,但是用的都是别人的脱壳工具 具体里面的流程是怎样的 请问有不有资料呢,应该不违规把?
ojp2008520   

干就完了
西枫游戏   


ojp2008520 发表于 2024-2-26 23:20
现在脱壳修复查到资料 学会了,但是用的都是别人的脱壳工具 具体里面的流程是怎样的 请问有不有资料呢, ...

那就需要去理解脱壳的原理,从源码中去解读脱壳点,其实脱壳工具就是一个很好的学习样本
您需要登录后才可以回帖 登录 | 立即注册

返回顶部