一、系统环境
OS: Windows_NT x64 10.0.19045
python:3.8.10
Node.js: 18.17.1
frida :14.2.14
objection:1.11.0
vscode: 1.87.2
device:nexus 5x-7.1.2
二、详细分析
前言:
从界面看这个apk用的是手势解锁,随意尝试划动几次发现没有任何文字方面的提示,接下来就用JADX和GDA静态分析看看代码逻辑
正文:
apk未加壳,只有一个MainActivity,查看OnCreate函数可以看到首先是对界面控件初始化操作,接着是对手势密码控件绑定监听函数
1.静态分析
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(C0794R.layout.activity_main);
TextView textView = (TextView) findViewById(C0794R.id.tv_text);
this.tvText = textView;
textView.setText("\u3000\u3000吾名玄天帝,昔为诸界之尊,因古诅咒,沉睡亿载。今幸苏醒,欲召百万神兵仙将,复掌万界,重铸天序。此举,需汝解封印,贡力之源。若助吾破诅归位,赐汝万界神尊,封一神域为土,永居众神之巅。");
GestureUnlock gestureUnlock = (GestureUnlock) findViewById(C0794R.id.myunlock);
this.myunlock = gestureUnlock;
gestureUnlock.setIGestureListener(new IGestureListener() { // from class: com.zj.wuaipojie2024_2.MainActivity.1
@Override // com.example.gesturelock.IGestureListener
public void isSetUp(String str) {
}
@Override // com.example.gesturelock.IGestureListener
public void isSuccessful(String str) {
Log.e("zj595", str);
}
@Override // com.example.gesturelock.IGestureListener
public void isError(String str) {
Log.e("zj595", str);
MainActivity.this.checkPassword(str);
}
});
}
查看手势密码控件类代码没有发现对手势密码进行判断的地方
public GestureUnlock(Context context, AttributeSet attributeSet, int i) {
super(context, attributeSet, i);
this.cicleRadius = 10;
this.firstInit = false;
this.points = new ArrayList();
this.selectP = new ArrayList();
this.alreadyTouch = false;
this.isUp = false;
this.lockTouch = false;
this.returnFun = 0;
this.defaultKey = "01234";
this.setUpKey = BuildConfig.FLAVOR;
this.errorKey = BuildConfig.FLAVOR;
this.handler = new Handler(Looper.myLooper(), new Handler.Callback() { // from class: com.example.gesturelock.GestureUnlock.1
@Override // android.os.Handler.Callback
public boolean handleMessage(Message message) {
try {
int i2 = message.arg1;
if (i2 != 1) {
if (i2 != 2) {
if (i2 != 3) {
if (i2 == 4) {
Toast.makeText(GestureUnlock.this.context, "请连接至少" + GestureUnlock.this.minSelect + "个点", 0).show();
}
} else if (GestureUnlock.this.gestureListener != null) {
GestureUnlock.this.gestureListener.isSetUp(GestureUnlock.this.setUpKey);
}
} else if (GestureUnlock.this.gestureListener != null) {
GestureUnlock.this.gestureListener.isError(GestureUnlock.this.errorKey);
}
} else if (GestureUnlock.this.gestureListener != null) {
GestureUnlock.this.gestureListener.isSuccessful(GestureUnlock.this.defaultKey);
}
Thread.sleep(GestureUnlock.this.determineTime * 1000.0f);
GestureUnlock.this.selectP.clear();
Iterator it = GestureUnlock.this.points.iterator();
while (it.hasNext()) {
Iterator it2 = ((List) it.next()).iterator();
while (it2.hasNext()) {
((GesturePoint) it2.next()).setCode(1);
}
}
GestureUnlock.this.invalidate();
} catch (InterruptedException e) {
e.printStackTrace();
}
return GestureUnlock.DEFAULT_LOOK;
}
});
this.context = context;
initAttrs(context, attributeSet);
}
从字面意思看 isSuccessful 这个函数是成功解锁后调用,在界面上手动绘制 defaultKey 对应的图形后没有任何提示
GestureUnlock.this.gestureListener.isSuccessful(GestureUnlock.this.defaultKey);
public void isSuccessful(String str) {
Log.e("zj595", str);
}
再看手势密码控件中其他的监听函数,发现只有 isError 中包含其他函数
public void isError(String str) {
Log.e("zj595", str);
MainActivity.this.checkPassword(str);
}
原来 checkPassword 这个函数才是重点,也就是手势密码输错的情况下才会触发,这个函数还不能直接看到内部逻辑,属于套娃系列(加载了其他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(C0794R.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;
}
}
分析具体逻辑发现下面这段是重点,通过加载其他dex并调用里面的函数,返回的值可能就是 flag
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(C0794R.array.A_offset));
if (str2 == null || !str2.startsWith("唉!")) {
return false;
}
this.tvText.setText(str2);
this.myunlock.setVisibility(8);
return true;
将参数带入到DexClassLoader中以更直观
#DexClassLoader (String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)
#dexPath 包含dex文件的jar包或apk文件路径
#optimizedDirectory 释放目录,可以理解为缓存目录,必须为应用私有目录,不能为空
#librarySearchPath native库的路径(so文件),可为空
#parent 父类加载器
DexClassLoader dexClassLoader = DexClassLoader("classes.dex", "/", null, this.getClassLoader())
#loadClass(String name)
#String name 要记载的类名
Class<?> clazz = dexClassLoader.loadClass("com.zj.wuaipojie2024_2.C");
#getDeclaredMethod(String name, Class<?>... parameterTypes)
#name 要获取的方法的名称
#parameterTypes 参数是 Class<?> 类型的可变参数,表示要获取的方法的参数类型
Method method = clazz.getDeclaredMethod("isValidate", Context.class, String.class, int[].class)
#invoke(Object obj, Object[] args)
#obj 所在方法的对象
#args 调用方法的参数
String str2 = method.invoke(null, this, BuildConfig.FLAVOR, 0x7F030000)
#BuildConfig.FLAVOR的值从文件中来看目前是空值,但不确定运行时是否会改变,目前就用变量名代替
#public static final String FLAVOR = "";
isValidate函数的内容在classes.dex中,接下来进入到 classes.dex分析
#isValidate(this, BuildConfig.FLAVOR, 0x7F030000)
public static String isValidate(Context p0,String p1,int[] p2){
try{
Class[] uClassArray = new Class[]{Context.class,String.class};
Object[] objArray = new Object[]{p0,p1};
return C.getStaticMethod(p0, p2, "com.zj.wuaipojie2024_2.A", "d", uClassArray).invoke(null, objArray);
}catch(java.lang.Exception e7){
Log.e("ZJ595", "咦,似乎是坏掉的dex呢!");
e7.printStackTrace();
return "";
}
}
isValidate函数内部调用了自定义的函数getStaticMethod
#getStaticMethod(this,0x7F030000,"com.zj.wuaipojie2024_2.A","d",{Context.class,String.class})
private static Method getStaticMethod(Context p0,int[] p1,String p2,String p3,Class[] p4){
String str = null;
try{
File uFile = C.fix(C.read(p0), p1[0], p1[1], p1[2], p0);
File dir = p0.getDir("fixed", 0);
uFile.delete();
new File(dir, uFile.getName()).delete();
return new DexClassLoader(uFile.getAbsolutePath(), dir.getAbsolutePath(), str, p0.getClass().getClassLoader()).loadClass(p2).getDeclaredMethod(p3, p4);
}catch(java.lang.Exception e6){
e6.printStackTrace();
return str;
}
}
getStaticMethod函数内部首先调用 read函数读取了 decode.dex的内容
private static ByteBuffer read(Context p0){
ByteBuffer uByteBuffer = null;
try{
File uFile = new File(p0.getDir("data", 0), "decode.dex");
if (!uFile.exists()) {
return uByteBuffer;
}
FileInputStream uFileInputSt = new FileInputStream(uFile);
byte[] uobyteArray = new byte[uFileInputSt.available()];
uFileInputSt.read(uobyteArray);
uFileInputSt.close();
return ByteBuffer.wrap(uobyteArray);
}catch(java.lang.Exception e0){
return e0;
}
}
再将读取的内容传入到 fix 函数进行操作并将操作后的内容保存到 2.dex 中
private static File fix(ByteBuffer p0,int p1,int p2,int p3,Context p4){
try{
p1 = D.getClassDefData(p0, p1).get("class_data_off").intValue();
HashMap classData = D.getClassData(p0, p1);
p2[2] = p3;
p0.position(p1);
p0.put(D.encodeClassData(classData));
p0.position(32);
byte[] uobyteArray = new byte[(p0.capacity() - 32)];
p0.get(uobyteArray);
p0.position(12);
p0.put(Utils.getSha1(uobyteArray));
p0.position(8);
p0.putInt(Integer.reverseBytes(Utils.checksum(p0)));
File uFile = new File(p4.getDir("data", 0), "2.dex");
FileOutputStream uFileOutputS = new FileOutputStream(uFile);
uFileOutputS.write(p0.array());
uFileOutputS.close();
return uFile;
}catch(java.lang.Exception e2){
e2.printStackTrace();
return null;
}
}
从fix函数中对函数getClassDefData、getClassData、encodeClassData的调用来看,这个操作可能是在对 classes.dex里面的内容进行修复
public static HashMap getClassDefData(ByteBuffer p0,int p1){
if (p0 == null) {
throw new IllegalArgumentException("Buffer cannot be null");
}
p0.position(100);
p0.position(((p1 * 32) + Integer.reverseBytes(p0.getInt())));
HashMap hashMap = new HashMap();
hashMap.put("class_idx", Integer.valueOf(Integer.reverseBytes(p0.getInt())));
hashMap.put("access_flag", Integer.valueOf(Integer.reverseBytes(p0.getInt())));
hashMap.put("superclass_idx", Integer.valueOf(Integer.reverseBytes(p0.getInt())));
hashMap.put("interfaces_off", Integer.valueOf(Integer.reverseBytes(p0.getInt())));
hashMap.put("source_file_idx", Integer.valueOf(Integer.reverseBytes(p0.getInt())));
hashMap.put("annotation_off", Integer.valueOf(Integer.reverseBytes(p0.getInt())));
hashMap.put("class_data_off", Integer.valueOf(Integer.reverseBytes(p0.getInt())));
hashMap.put("static_values_off", Integer.valueOf(Integer.reverseBytes(p0.getInt())));
return hashMap;
}
最后再加载修复完成后的 2.dex 并利用反射获取方法返回给 isValidate 函数调用
至此静态分析结束,目前可以看到的是这个应用采用的是对dex函数进行动态修改并调用的处理方式,接下来需要使用动态分析
2.动态分析
2.1 Objection工具
#使用Objection来hook看看 classes.dex 中的函数被调用时传入的参数和返回值
$ objection -g com.zj.wuaipojie2024_2 explore
#先查看类在内存中是否存在
$ android hooking search classes com.zj.wuaipojie2024_2.C
#输出
Note that Java classes are only loaded when they are used, so if the expected class has not been found, it might not have been loaded yet.Found 0 classes
内存中居然没有这个类,那就表示classes.dex没有被正常加载,有点奇怪,将classes.dex拖到010 editor看看文件有没有问题
果然有问题,checksum和signature都是错误的
logcat的输出也证实了dex文件是无法加载的
Failure to verify dex file '/data/user/0/com.zj.wuaipojie2024_2/app_data/1.dex': Bad checksum (c607ea12, expected 22dcea4c)
Unable to load dex file: /data/user/0/com.zj.wuaipojie2024_2/app_data/1.dex
使用 DexRepair 对dex头部进行修复
java -jar DexRepair.jar classes.dex
把修复完成后的文件重命名那个为 classes.dex,覆盖掉apk中 【assets】目录下原来的classes.dex,然后重新打包签名安装
#再次使用objection发现可以找到com.zj.wuaipojie2024_2.C这个类了
$ android hooking search classes com.zj.wuaipojie2024_2.C
com.zj.wuaipojie2024_2.CFound 1 classes
#准备用objection看看com.zj.wuaipojie2024_2.C这个类下面有哪些方法出了BUG,只能换JEB来进行动态调试了
$ android hooking list class_methods com.zj.wuaipojie2024_2.C
An unexpected internal exception has occurred. If this looks like a code related error, please file a bug report!
script is destroyed
2.2 JEB工具
1.首先让apk可以被调试,用 NP 工具打开apk中AndroidManifest.xml加入调试参数,新增下图中红圈中的内容然后保存签名
2.运行app,将添加了调试参数后的 apk 包拖入到 JEB中(JEB解析 apk 包的过程中弹出的确认窗口都选是)
解析apk包完成后按照下面的3个步骤操作就可以了
一、选择 Assets 目录下的 classes.dex,再选择类 C
二、在函数 isValidate开头位置下断点(快捷键 Ctrl+B)
三、点击最上面的 debug图标(小虫子)
回到应用界面,手势密码随便划拉几下就会触发断点停在 isValidate 函数
调试的过程中注意观察右边 局部变量 窗口里面的参数变化
为了先看看 isValidate 函数最终返回上面,这里就用 跳过(快捷键F6)的方式执行,执行到最后发现程序进入到了异常处理,说明调用的某个函数出了问题
重新再调试,这次进入到函数内部调试,进入到 read 函数发现 "decode.dex" 未找到所以就不会有数据
fix 函数需要使用 read 函数返回的数据来进行修复,没有数据就返回了异常,这就是 isValidate 函数返回异常的原因
知道了原因后就好办了,将之前修复头文件后的 classes.dex改名为 decode.dex后放到应用的app_data目录下
$ adb push decode.dex /data/user/0/com.zj.wuaipojie2024_2/app_data
再次调试后没有再出现异常,调试 isValidate 函数最终返回的结果为 null,再去看 isValidate 函数内部调用的函数,结合之前的静态分析发现在 fix 函数中将修复完成后的内容保存到了 2.dex中
File uFile = new File(p4.getDir("data", 0), "2.dex");
FileOutputStream uFileOutputS = new FileOutputStream(uFile);
uFileOutputS.write(p0.array());
uFileOutputS.close();
后面代码又将 2.dex 给删除掉了,所以在删除代码之前下断点获取到 2.dex
private static Method getStaticMethod(Context p0,int[] p1,String p2,String p3,Class[] p4){
String str = null;
try{
File uFile = C.fix(C.read(p0), p1[0], p1[1], p1[2], p0);
File dir = p0.getDir("fixed", 0);
uFile.delete();
new File(dir, uFile.getName()).delete();
return new DexClassLoader(uFile.getAbsolutePath(), dir.getAbsolutePath(), str, p0.getClass().getClassLoader()).loadClass(p2).getDeclaredMethod(p3, p4);
}catch(java.lang.Exception e6){
e6.printStackTrace();
return str;
}
用 GDA 查看 2.dex发现 com.zj.wuaipojie2024_2.A下面的 d 函数已经被修复
修复前
修复后
# 分析修复后的函数内容发现最后一句话藏有玄机,原来com.zj.wuaipojie2024_2.B.d这个函数也需要修复
return "唉!哪有什么亿载沉睡的玄天帝,不过是一位被诅咒束缚的旧日之尊,在灯枯之际挣扎的南柯一梦罢了。有缘人,这份机缘就赠予你了。坐标在B.d";
再次分析 fix 函数注意到参数 p1
private static Method getStaticMethod(Context p0,int[] p1,String p2,String p3,Class[] p4){
String str = null;
try{
File uFile = C.fix(C.read(p0), p1[0], p1[1], p1[2], p0);
}
p1是个数组,往上层调用追踪来源
public static String isValidate(Context p0,String p1,int[] p2){
Class[] uClassArray = new Class[]{Context.class,String.class};
Object[] objArray = new Object[]{p0,p1};
return C.getStaticMethod(p0, p2, "com.zj.wuaipojie2024_2.A", "d", uClassArray).invoke(null, objArray);
}
回到主 MainActivity发现来源 C0794R.array.A_offset
public boolean checkPassword(String str) {
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(C0794R.array.A_offset));
}
C0794R.array.A_offset位置在 resources.arsc/res/values/arrays.xml
这个数组应该就是要修复函数的地址,D_offset里面数组内容可能就是com.zj.wuaipojie2024_2.B.d的地址,将A_offset的内容改成 D_offset的内容后再按照之前获取到 2.dex的方法重新操作一遍
修复完成后的内容,可以看到 flag 基本上已经出来了,password就是 com.zj.wuaipojie2024_2.A.d 函数中的运算结果, uid 是吾爱破解论坛自己账号uid,两个字符串连接后使用 Utils中的getSha1和md5进行加密后的结果就是最终 flag
flag最终还原过程
# 获取password
import java.io.*;
public class GetPassword {
public static void main(String[] args) {
StringBuffer signInfo = new StringBuffer();;
int i = 0;
while (signInfo.length() < 9 && i < 40) {
int j = i + 1;
String str = "0485312670fb07047ebd2f19b91e1c5f".substring(i, j);
if (!signInfo.toString().contains(str)) {
signInfo = signInfo.append(str);
}
i = j;
}
System.out.println("password:"+signInfo.toString().toUpperCase());
}
}
password:048531267
# 获取flag
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Flag {
public static byte[] getSha1(byte[] bArr) {
try {
return MessageDigest.getInstance("SHA").digest(bArr);
} catch (Exception unused) {
return null;
}
}
public static String md5(byte[] bArr) {
try {
String bigInteger = new BigInteger(1, MessageDigest.getInstance("md5").digest(bArr)).toString(16);
for (int i = 0; i < 32 - bigInteger.length(); i++) {
bigInteger = "0" + bigInteger;
}
return bigInteger;
} catch (NoSuchAlgorithmException unused) {
throw new RuntimeException("ops!!");
}
}
public static void main(String args[]) {
String password = "048531267";
String uid = "2241403";
String str = password + uid;
System.out.println("Flag:" + "{" + Flag.md5(Flag.getSha1(str.getBytes())) + "}" );
}
}
Flag:{575b6099594fb7873ef1f4ecbb66ac0b}