正己 发表于 2024-2-26 19:19

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

本帖最后由 正己 于 2024-2-28 15:05 编辑

https://www.bilibili.com/video/BV1NW421P7CF/
![|500](https://pic.rmb.bdstatic.com/bjh/240226/c66e1b5d8a9d09e037108b9234450a8b6913.png)
# 一、课程目标

frida实战安卓初级题与中级题

# 二、工具

1.安卓初级题与中级题
2.jadx-gui
3.VS Code
4.jeb

# 三、课程内容
## 1.初级题1
`关键函数解析`
```java
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;
            // 从当前文件指针位置读取数据到bArr中
            randomAccessFile.read(bArr);
            // 将读取的字节数组转换成字符串,并检查是否包含"flag{"字符串
            if (new String(bArr, StandardCharsets.UTF_8).indexOf("flag{") != -1) {
                // 如果找到"flag{",则从这个位置开始,截取到"}"为止的字符串,并拼接上"}"
                String str3 = str2.substring(indexOf).split("\\}") + "}";
                // 关闭文件并返回找到的字符串
                randomAccessFile.close();
                return str3;
            }
      }
      // 如果整个文件中都没有找到符合条件的字符串,则关闭文件并返回null
      randomAccessFile.close();
      return null;
    } catch (Exception e) {
      // 处理异常,打印异常堆栈信息
      e.printStackTrace();
      return null;
    }
}

```
```js
方法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

`关键函数解析`
```java
// 定义一个静态字节数组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;
    }

    // 检查signatureArr是否非空且至少包含一个元素。
    if (signatureArr != null && signatureArr.length >= 1) {
      // 将第一个签名信息转换为字节数组。
      byte[] byteArray = signatureArr.toByteArray();

      // 分配一个ByteBuffer,大小与bArr2相同,用于存储异或操作的结果。
      ByteBuffer allocate = ByteBuffer.allocate(bArr2.length);

      // 对bArr2和byteArray进行异或操作,并将结果存储在allocate中。
      for (int i = 0; i < bArr2.length; i++) {
            allocate.put((byte) (bArr2 ^ byteArray));
      }

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

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

    // 构建一个包含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());
}


```

```js
方法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', );
      },
      onComplete: function(){

      }
    });
}
```
## 3.中级题
放一张流程图帮助理解
![](https://pic.rmb.bdstatic.com/bjh/240226/7a758528714c8a4aac92e9c4e2f075316866.png)
根据logcat发现是一个错误的dex,checksum验证失败,利用(https://github.com/luoyesiqiu/DexRepair)修复头文件
```
java -jar DexRepair.jar /path/to/dex
```
塞回安装包,发现还是错误,仔细检查代码,发现读不到dex数据,对C类里read方法的decode.dex修改成1.dex

`关键函数解析`
```java
public boolean checkPassword(String str) {
    try {
      // 打开assets目录下的"classes.dex"文件作为InputStream
      InputStream open = getAssets().open("classes.dex");
      // 创建一个字节数组,大小为可读取的字节数,即整个文件的大小
      byte[] bArr = new byte;
      // 从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, iArr, iArr, 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") = 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.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())) + "}";
}

```


```js
方法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', );   
            // 打印新的返回值
            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            | 表示是一个有效的dex文件                         | 值一般固定为`dex.035`      |
| checksum            | dex文件的校验和,用来判断文件是否已经损坏或者篡改 | 使用adler32算法            |
| signature | 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   | 虚方法               |

# 四、答疑

待更新

# 五、视频及课件地址


[百度云](https://pan.baidu.com/s/1cFWTLn14jeWfpXxlx3syYw?pwd=nqu9)
[阿里云](https://www.aliyundrive.com/s/TJoKMK6du6x)
[哔哩哔哩](https://www.bilibili.com/video/BV1wT411N7sV/?spm_id_from=333.788&vd_source=6dde16dc6479f00694baaf73a2225452)
[教程开源地址](https://github.com/ZJ595/AndroidReverse)
PS:解压密码都是52pj,阿里云由于不能分享压缩包,所以下载exe文件,双击自解压

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

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


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

正己 发表于 2024-5-25 09:54

xuqi 发表于 2024-5-24 22:46
看您的视频里显示状态为"未签名",谢谢啦

在mt签个名就行了

正己 发表于 2024-2-26 19:20

视频剪辑中,晚点上传

ojp2008520 发表于 2024-2-26 21:18

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

如果我是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:29

干就完了

正己 发表于 2024-2-26 23:34

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

那就需要去理解脱壳的原理,从源码中去解读脱壳点,其实脱壳工具就是一个很好的学习样本

dhsfb 发表于 2024-2-27 08:49

讲解详尽,感谢大佬无私的分享!!
页: [1] 2 3 4 5 6
查看完整版本: 《安卓逆向这档事》番外实战篇2-【2024春节】解题领红包活动,启动!