一、课程目标
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个部分的偏移和大小 |
字段名 |
描述 |
备注 |
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 |
数据段的文件偏移 |
|
字段名 |
描述 |
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起步探索