一、课程目标
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