吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 4178|回复: 53
收起左侧

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

  [复制链接]
正己 发表于 2024-2-26 19:19
本帖最后由 正己 于 2024-2-28 15:05 编辑


|500

一、课程目标

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 < length; max++) {
            // 定位文件指针到max位置
            randomAccessFile.seek(max);
            // 定义一个长度为30的字节数组用于读取数据
            byte[] bArr = new byte[30];
            // 从当前文件指针位置读取数据到bArr中
            randomAccessFile.read(bArr);
            // 将读取的字节数组转换成字符串,并检查是否包含"flag{"字符串
            if (new String(bArr, StandardCharsets.UTF_8).indexOf("flag{") != -1) {
                // 如果找到"flag{",则从这个位置开始,截取到"}"为止的字符串,并拼接上"}"
                String str3 = str2.substring(indexOf).split("\\}")[0] + "}";
                // 关闭文件并返回找到的字符串
                randomAccessFile.close();
                return str3;
            }
        }
        // 如果整个文件中都没有找到符合条件的字符串,则关闭文件并返回null
        randomAccessFile.close();
        return null;
    } catch (Exception e) {
        // 处理异常,打印异常堆栈信息
        e.printStackTrace();
        return null;
    }
}
方法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 < bArr2.length; i++) {
            allocate.put((byte) (bArr2[i] ^ byteArray[i % byteArray.length]));
        }

        // 将处理后的数据赋值给bArr。
        bArr = allocate.array();

    } else {
        // 如果签名数组为空或不存在,将bArr初始化为空字节数组。
        bArr = new byte[0];
    }

    // 构建一个包含flag信息的字符串
    StringBuilder d2 = a.d("for honest players only: \n");
    d2.append(new String(bArr)); // 将bArr转换为字符串并附加到提示信息后。

    // 将构建好的提示信息设置给TextView显示。
    ((TextView) findViewById(R.id.tvFlagHint)).setText(d2.toString());
}
方法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<String, int[][]> 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() < 9 && i < 40) {
        int i2 = i + 1;
        String substring = "0485312670fb07047ebd2f19b91e1c5f".substring(i, i2);
        if (!stringBuffer.toString().contains(substring)) {
            stringBuffer.append(substring);
        }
        i = i2;
    }

    return !str.equals(stringBuffer.toString().toUpperCase()) ? "" : "唉!哪有什么亿载沉睡的玄天帝,不过是一位被诅咒束缚的旧日之尊,在灯枯之际挣扎的南柯一梦罢了。有缘人,这份机缘就赠予你了。坐标在B.d";
}
public static String d(String str) {
    return "机缘是{" + Utils.md5(Utils.getSha1("password+你的uid".getBytes())) + "}";
}
方法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文件的数据结构,解析后的几个结构体如下:

部分名称 描述
dex_header dex文件头,指定了dex文件的一些数据,记录了其他数据结构在dex文件中的物理偏移
string_ids 字符串列表,全局共享使用的常量池
type_ids 类型签名列表,组成的常量池
proto_ids 方法声明列表,组成的常量池
field_ids 字段列表,组成的常量池
method_ids 方法列表,组成的常量池
class_defs 类型结构体列表,组成的常量池
map_list 记录了前面7个部分的偏移和大小

DexHeader

字段名 描述 备注
magic[8] 表示是一个有效的dex文件 值一般固定为dex.035
checksum dex文件的校验和,用来判断文件是否已经损坏或者篡改 使用adler32算法
signature[kSHA1DigestLen] SHA-1哈希值,用来识别未经dexopt优化的dex文件 kSHA1DigestLen为SHA-1哈希长度
fileSize 整个文件的长度,包括dexHeader在内
headerSize dexHeader占用的字节数 一般都是0x70
endianTag 指定dex运行环境的CPU字节序 默认小端字节序0x12345678
linkSize 链接段的大小
linkOff 链接段的偏移
mapOff DexMapList结构的文件偏移
stringIdsSize 字符串列表的大小
stringIdsOff 字符串列表的文件偏移
typeIdsSize 类型签名列表的大小
typeIdsOff 类型签名列表的文件偏移
protoIdsSize 方法声明列表的大小
protoIdsOff 方法声明列表的文件偏移
fieldIdsSize 字段列表的大小
fieldIdsOff 字段列表的文件偏移
methodIdsSize 方法列表的大小
methodIdsOff 方法列表的文件偏移
classDefsSize 类型结构体列表的大小
classDefsOff 类型结构体列表的文件偏移
dataSize 数据段的大小
dataOff 数据段的文件偏移

DexClassDataHeader

字段名 描述
staticFieldsSize 静态字段的个数
instanceFieldsSize 实例字段的个数
directMethodsSize 直接方法的个数
virtualMethodsSize 虚方法的个数

DexField

字段名 描述
fieldIdx 指向DexFieldId的索引
accessFlags 访问标志

DexMethod

字段名 描述
methodIdx 指向DexMethodId的索引
accessFlags 访问标志
codeOff 指向DexCode结构的偏移

DexClassData

字段名 描述
header 指定字段和方法的个数
staticFields 静态字段
instanceFields 实例字段
directMethods 直接方法
virtualMethods 虚方法

四、答疑

待更新

五、视频及课件地址

百度云
阿里云
哔哩哔哩
教程开源地址
PS:解压密码都是52pj,阿里云由于不能分享压缩包,所以下载exe文件,双击自解压

六、其他章节

《安卓逆向这档事》一、模拟器环境搭建
《安卓逆向这档事》二、初识APK文件结构、双开、汉化、基础修改
《安卓逆向这档事》三、初识smail,vip终结者
《安卓逆向这档事》四、恭喜你获得广告&弹窗静默卡
《安卓逆向这档事》五、1000-7=?&动态调试&Log插桩
《安卓逆向这档事》六、校验的N次方-签名校验对抗、PM代{过}{滤}理、IO重定向
《安卓逆向这档事》七、Sorry,会Hook真的可以为所欲为-Xposed快速上手(上)模块编写,常用Api
《安卓逆向这档事》八、Sorry,会Hook真的可以为所欲为-xposed快速上手(下)快速hook
《安卓逆向这档事》九、密码学基础、算法自吐、非标准加密对抗
《安卓逆向这档事》十、不是我说,有了IDA还要什么女朋友?
《安卓逆向这档事》十二、大佬帮我分析一下
《安卓逆向这档事》番外实战篇1-某电影视全家桶
《安卓逆向这档事》十三、是时候学习一下Frida一把梭了(上)
《安卓逆向这档事》十四、是时候学习一下Frida一把梭了(中)
《安卓逆向这档事》十五、是时候学习一下Frida一把梭了(下)
《安卓逆向这档事》十六、是时候学习一下Frida一把梭了(终)
《安卓逆向这档事》十七、你的RPCvs佬的RPC

七、参考文档

dex起步探索

免费评分

参与人数 21威望 +1 吾爱币 +45 热心值 +19 收起 理由
Guanzhong + 1 + 1 用心讨论,共获提升!
twodoors + 1 谢谢@Thanks!
zhangxu888 + 1 + 1 用心讨论,共获提升!
RickSanchez + 1 + 1 我很赞同!
wangjinhao123 + 1 + 1 我很赞同!
Courser + 1 + 1 谢谢@Thanks!
Chenda1 + 1 + 1 热心回复!
dongye + 1 + 1 谢谢@Thanks!
gunxsword + 1 + 1 热心回复!
Larry404 + 1 谢谢大佬分享技术@Thanks!
Doubts + 1 + 1 谢谢@Thanks!
lvbuqing + 2 + 1 用心讨论,共获提升!
jackyyue_cn + 1 + 1 谢谢@Thanks!
allspark + 1 + 1 用心讨论,共获提升!
pp67868450 + 1 + 1 做了好久都没做出来,答案也看不懂
solly + 2 + 1 用心讨论,共获提升!
debug_cat + 2 + 1 谢谢@Thanks!
gamingnow + 1 + 1 用心讨论,共获提升!
Hmily + 1 + 20 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
爱飞的猫 + 3 + 1 为正己老师打 CALL
zlnxlzht + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

soughing 发表于 2024-2-27 07:36

大佬,谢谢您的解答。
但是现在我对于这个课程有个疑问,基本现在所有的APP都加了壳的
现在就是卡住这里,不知如果弄这个 虽然有其他人写的脱壳程序,但是根本不知道怎么弄的,类似于脚本小子的感觉。
 楼主| 正己 发表于 2024-5-25 09:54
 楼主| 正己 发表于 2024-2-26 19:20
视频剪辑中,晚点上传

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
BonnieRan + 1 + 1 谢谢@Thanks!

查看全部评分

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

点评

后面会讲脱壳的,放心  详情 回复 发表于 2024-2-26 22:38
如果我是DJ? 发表于 2024-2-26 21:35
大佬666啊
 楼主| 正己 发表于 2024-2-26 22:38
ojp2008520 发表于 2024-2-26 21:18
大佬,谢谢您的解答。
但是现在我对于这个课程有个疑问,基本现在所有的APP都加了壳的
现在就是卡住这里 ...

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

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

点评

那就需要去理解脱壳的原理,从源码中去解读脱壳点,其实脱壳工具就是一个很好的学习样本  详情 回复 发表于 2024-2-26 23:34
西枫游戏 发表于 2024-2-26 23:29
干就完了
 楼主| 正己 发表于 2024-2-26 23:34
ojp2008520 发表于 2024-2-26 23:20
现在脱壳修复查到资料 学会了,但是用的都是别人的脱壳工具 具体里面的流程是怎样的 请问有不有资料呢, ...

那就需要去理解脱壳的原理,从源码中去解读脱壳点,其实脱壳工具就是一个很好的学习样本
dhsfb 发表于 2024-2-27 08:49
讲解详尽,感谢大佬无私的分享!!
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-12-12 14:31

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表