
一、课程目标
frida实战安卓初级题与中级题
二、工具
1.安卓初级题与中级题
2.jadx-gui
3.VS Code
4.jeb
三、课程内容
1.初级题1
关键函数解析
public static String extractDataFromFile(String str) {
String str2;
int indexOf;
try {
RandomAccessFile randomAccessFile = new RandomAccessFile(str, "r");
long length = randomAccessFile.length();
for (long max = Math.max(length - 30, 0L); max < length; max++) {
randomAccessFile.seek(max);
byte[] bArr = new byte[30];
randomAccessFile.read(bArr);
if (new String(bArr, StandardCharsets.UTF_8).indexOf("flag{") != -1) {
String str3 = str2.substring(indexOf).split("\\}")[0] + "}";
randomAccessFile.close();
return str3;
}
}
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
关键函数解析
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;
Signature[] signatureArr;
super.onCreate(bundle);
setContentView(R.layout.activity_flag);
byte[] bArr2 = o;
try {
signatureArr = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES).signatures;
} catch (PackageManager.NameNotFoundException unused) {
bArr = new byte[0];
}
if (signatureArr != null && signatureArr.length >= 1) {
byte[] byteArray = signatureArr[0].toByteArray();
ByteBuffer allocate = ByteBuffer.allocate(bArr2.length);
for (int i = 0; i < bArr2.length; i++) {
allocate.put((byte) (bArr2[i] ^ byteArray[i % byteArray.length]));
}
bArr = allocate.array();
} else {
bArr = new byte[0];
}
StringBuilder d2 = a.d("for honest players only: \n");
d2.append(new String(bArr));
((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 {
InputStream open = getAssets().open("classes.dex");
byte[] bArr = new byte[open.available()];
open.read(bArr);
File file = new File(getDir("data", 0), "1.dex");
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(bArr);
fileOutputStream.close();
open.close();
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));
if (str2 == null || !str2.startsWith("唉!")) {
return false;
}
this.tvText.setText(str2);
this.myunlock.setVisibility(8);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public static String isValidate(Context context, String str, int[] iArr) throws Exception {
try {
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 {
File fix = fix(read(context), iArr[0], iArr[1], iArr[2], context);
ClassLoader classLoader = context.getClass().getClassLoader();
File dir = context.getDir("fixed", 0);
Method declaredMethod = new DexClassLoader(fix.getAbsolutePath(), dir.getAbsolutePath(), null, classLoader)
.loadClass(str)
.getDeclaredMethod(str2, clsArr);
fix.delete();
new File(dir, fix.getName()).delete();
return declaredMethod;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private static File fix(ByteBuffer byteBuffer, int i, int i2, int i3, Context context) throws Exception {
try {
File dir = context.getDir("data", 0);
int intValue = D.getClassDefData(byteBuffer, i).get("class_data_off").intValue();
HashMap<String, int[][]> classData = D.getClassData(byteBuffer, intValue);
classData.get("direct_methods")[i2][2] = i3;
byte[] encodeClassData = D.encodeClassData(classData);
byteBuffer.position(intValue);
byteBuffer.put(encodeClassData);
byteBuffer.position(32);
byte[] bArr = new byte[byteBuffer.capacity() - 32];
byteBuffer.get(bArr);
byte[] sha1 = Utils.getSha1(bArr);
byteBuffer.position(12);
byteBuffer.put(sha1);
int checksum = Utils.checksum(byteBuffer);
byteBuffer.position(8);
byteBuffer.putInt(Integer.reverseBytes(checksum));
byte[] array = byteBuffer.array();
File file = new File(dir, "2.dex");
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(array);
fileOutputStream.close();
return file;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
修复后的代码:
public static String d(Context context, String str) {
MainActivity.sSS(str);
String signInfo = Utils.getSignInfo(context);
if (signInfo == null || !signInfo.equals("fe4f4cec5de8e8cf2fca60a4e61f67bcd3036117")) {
return "";
}
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 () {
var File = Java.use("java.io.File");
File.delete.implementation = function () {
console.log("Deleting file: " + this.getPath());
return true;
};
});
}
function hook_resources() {
Java.perform(function () {
var Resources = Java.use("android.content.res.Resources");
Resources.getIntArray.overload('int').implementation = function (id) {
var replacementArray = Java.array('int', [0, 3, 8108]);
console.log("Replacing getIntArray result with: " + JSON.stringify(replacementArray));
return replacementArray;
};
});
}
方法2:
利用python脚本算出ULEB128对应的地址,利用010editor手动修改偏移,然后在加密网站上直接跑就行,因为是标准加密
https:
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起步探索