《安卓逆向这档事》番外实战篇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)
大佬,谢谢您的解答。
但是现在我对于这个课程有个疑问,基本现在所有的APP都加了壳的
现在就是卡住这里,不知如果弄这个 虽然有其他人写的脱壳程序,但是根本不知道怎么弄的,类似于脚本小子的感觉。
xuqi 发表于 2024-5-24 22:46
看您的视频里显示状态为"未签名",谢谢啦
在mt签个名就行了 视频剪辑中,晚点上传 大佬,谢谢您的解答。
但是现在我对于这个课程有个疑问,基本现在所有的APP都加了壳的
现在就是卡住这里,不知如果弄这个 虽然有其他人写的脱壳程序,但是根本不知道怎么弄的,类似于脚本小子的感觉。 大佬666啊 ojp2008520 发表于 2024-2-26 21:18
大佬,谢谢您的解答。
但是现在我对于这个课程有个疑问,基本现在所有的APP都加了壳的
现在就是卡住这里 ...
后面会讲脱壳的,放心 正己 发表于 2024-2-26 22:38
后面会讲脱壳的,放心
现在脱壳修复查到资料 学会了,但是用的都是别人的脱壳工具 具体里面的流程是怎样的 请问有不有资料呢,应该不违规把? 干就完了 ojp2008520 发表于 2024-2-26 23:20
现在脱壳修复查到资料 学会了,但是用的都是别人的脱壳工具 具体里面的流程是怎样的 请问有不有资料呢, ...
那就需要去理解脱壳的原理,从源码中去解读脱壳点,其实脱壳工具就是一个很好的学习样本 讲解详尽,感谢大佬无私的分享!!