Android逆向吾爱春节中级题目
背景
记录Android找flag的一个案例,2024吾爱春节题目3。计算最终flag的时候需要把password+uid
处理。比如找到了password是23456,自己的uid是20220000,组合起来就是2345620220000。
体验样本
把样本安装到手机上面。
一个绘图解锁,尝试一下,当然是没那么容易就解锁了。
开始反编译分析
环境:macOS,root的真机pixel3,MT管理,jadx。
通过dump堆栈收集到包名和activity信息。
adb shell dumpsys activity | grep -i run
# 高版本用这个
adb shell dumpsys activity top | grep ACTIVITY
ACTIVITY bin.mt.plus/l.۟ۚۡ۫ 28b8e30 pid=4807
ACTIVITY com.android.launcher3/.uioverrides.QuickstepLauncher f053047 pid=2666
ACTIVITY com.zj.wuaipojie2024_2/.MainActivity 7c20286 pid=8038
包名:com.zj.wuaipojie2024_2
当前页面:MainActivity
根据上面信息在jadx中找到页面的代码:
开始分析代码。
1、初始化绘图。
2、错误的回调中打印了绘图密码,然后checkPassword
。
3、复制classes.dex
到私有目录中名字1.dex
。
4、动态加载dex,应该是调用里面的函数做事情。
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));
这一行代码太长了,我复制出来了。
loadClass("com.zj.wuaipojie2024_2.C")
加载了1.dex
中的一个类C
,调用了isValidate
函数,传递的参数是绘图的密码和R.array.A_offset
对应的数组。绘图密码是log打印的,可以通过logcat查看。
E/zj595: 012543
E/zj595: 78
E/zj595: 15
E/zj595: 34
尝试几次解锁,可以通过过滤tag得到日志。
查看上面代码复制出来的dex:
File file = new File(getDir("data", 0), "1.dex");
进入app的内部目录:
adb shell
dipper:/ $ su
dipper:/ # cd data/data/com.zj.wuaipojie2024_2
dipper:/data/data/com.zj.wuaipojie2024_2 # ls -l
total 12
drwxrwx--x 2 u0_a160 u0_a160 3488 2024-02-22 14:05 app_data
drwxrwx--x 2 u0_a160 u0_a160 3488 2024-02-22 14:05 app_dex
drwxrws--x 2 u0_a160 u0_a160_cache 3488 2024-02-22 14:00 cache
drwxrws--x 2 u0_a160 u0_a160_cache 3488 2024-02-22 14:00 code_cache
dipper:/data/data/com.zj.wuaipojie2024_2 # cd app_data/
dipper:/data/data/com.zj.wuaipojie2024_2/app_data # ls -l
total 16
-rw------- 1 u0_a160 u0_a160 12384 2024-02-22 14:05 1.dex
dipper:/data/data/com.zj.wuaipojie2024_2/app_data #
这个dex中有重要逻辑:loadClass("com.zj.wuaipojie2024_2.C").getDeclaredMethod("isValidate")
动态加载这个dex,执行isValidate
。经过多次绘制密码后我发现logcat日志有点异常:
2024-02-22 14:09:15.332 8038-8038/? E/zj595: 345
2024-02-22 14:09:15.334 8038-8038/? W/wuaipojie2024_: Failure to verify dex file '/data/user/0/com.zj.wuaipojie2024_2/app_data/1.dex': Bad checksum (c607ea12, expected 22dcea4c)
2024-02-22 14:09:15.335 8038-8038/? E/System: Unable to load dex file: /data/user/0/com.zj.wuaipojie2024_2/app_data/1.dex
2024-02-22 14:09:15.335 8038-8038/? E/System: java.io.IOException: Failed to open dex files from /data/user/0/com.zj.wuaipojie2024_2/app_data/1.dex because: Failure to verify dex file '/data/user/0/com.zj.wuaipojie2024_2/app_data/1.dex': Bad checksum (c607ea12, expected 22dcea4c)
at dalvik.system.DexFile.openDexFileNative(Native Method)
at dalvik.system.DexFile.openDexFile(DexFile.java:373)
at dalvik.system.DexFile.<init>(DexFile.java:115)
at dalvik.system.DexFile.<init>(DexFile.java:88)
at dalvik.system.DexPathList.loadDexFile(DexPathList.java:438)
at dalvik.system.DexPathList.makeDexElements(DexPathList.java:387)
at dalvik.system.DexPathList.<init>(DexPathList.java:166)
at dalvik.system.BaseDexClassLoader.<init>(BaseDexClassLoader.java:134)
at dalvik.system.BaseDexClassLoader.<init>(BaseDexClassLoader.java:92)
at dalvik.system.DexClassLoader.<init>(DexClassLoader.java:55)
at com.zj.wuaipojie2024_2.MainActivity.checkPassword(MainActivity.java:72)
at com.zj.wuaipojie2024_2.MainActivity$1.isError(MainActivity.java:47)
at com.example.gesturelock.GestureUnlock$1.handleMessage(GestureUnlock.java:111)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7876)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
动态加载dex失败了:Failure to verify dex file '/data/user/0/com.zj.wuaipojie2024_2/app_data/1.dex': Bad checksum (c607ea12, expected 22dcea4c)
,这是原因,这是一个坏
的dex。
这可能是不小心故意的?
把assets中dex丢进mt中:
打开代码中动态加载的类loadClass("com.zj.wuaipojie2024_2.C")
,转java,导出。
导出文件方便分析代码。
下面是导出来的代码:
package com.zj.wuaipojie2024_2;
import android.content.Context;
import android.util.Log;
import dalvik.system.DexClassLoader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.HashMap;
public class C {
public static final String SIGNATURE = "fe4f4cec5de8e8cf2fca60a4e61f67bcd3036117";
private static final String TAG = "ZJ595";
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 = ((Integer) D.getClassDefData(byteBuffer, i).get("class_data_off")).intValue();
HashMap classData = D.getClassData(byteBuffer, intValue);
((int[][]) 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;
}
}
private static ByteBuffer read(Context context) {
try {
File file = new File(context.getDir("data", 0), "decode.dex");
if (file.exists()) {
FileInputStream fileInputStream = new FileInputStream(file);
byte[] bArr = new byte[fileInputStream.available()];
fileInputStream.read(bArr);
ByteBuffer wrap = ByteBuffer.wrap(bArr);
fileInputStream.close();
return wrap;
}
return null;
} catch (Exception unused) {
return null;
}
}
}
这是默认的代码,下面是分析增加注释的代码:
package com.zj.wuaipojie2024_2;
import android.content.Context;
import android.util.Log;
import dalvik.system.DexClassLoader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.HashMap;
public class C {
public static final String SIGNATURE = "fe4f4cec5de8e8cf2fca60a4e61f67bcd3036117";
private static final String TAG = "ZJ595";
// checkPassword -> com.zj.wuaipojie2024_2.C.isValidate (密码, A_offset)
// com.zj.wuaipojie2024_2.A.d (密码, A_offset)
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 {
// read 读取app_data -> decode.dex"
// fix 就是修复dex, 用到传递进来的A_offset, 偏移
File fix = fix(read(context), iArr[0], iArr[1], iArr[2], context);
ClassLoader classLoader = context.getClass().getClassLoader();
// 把修复的dex保存到fixed目录
File dir = context.getDir("fixed", 0);
// 动态加载dex,加载函数后返回Method
Method declaredMethod = new DexClassLoader(fix.getAbsolutePath(), dir.getAbsolutePath(), null, classLoader).loadClass(str).getDeclaredMethod(str2, clsArr);
// 删除修复的dex
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 = ((Integer) D.getClassDefData(byteBuffer, i).get("class_data_off")).intValue();
HashMap classData = D.getClassData(byteBuffer, intValue);
((int[][]) 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;
}
}
private static ByteBuffer read(Context context) {
try {
File file = new File(context.getDir("data", 0), "decode.dex");
if (file.exists()) {
FileInputStream fileInputStream = new FileInputStream(file);
byte[] bArr = new byte[fileInputStream.available()];
fileInputStream.read(bArr);
ByteBuffer wrap = ByteBuffer.wrap(bArr);
fileInputStream.close();
return wrap;
}
return null;
} catch (Exception unused) {
return null;
}
}
}
再查看A.d
的函数:
把密码传递进来,前后加个?再返回,应该没那么简单,C里面的代码就是修复这个函数。
流程梳理:
1、绘图解锁checkPassword -> 动态加载com.zj.wuaipojie2024_2.C.isValidate (密码, A_offset)。
2、C中反射调用com.zj.wuaipojie2024_2.A.d (密码, A_offset)。
3、动态修复dex,read 读取app_data -> decode.dex"。
4、修复dex, 用到传递进来的A_offset, 偏移。
5、把修复的dex保存到fixed目录。
6、动态加载dex,加载函数后返回Method,删除dex。
现在加载1.dex是坏的,修复的逻辑需要1.dex中的陷入死循环。要是里面的修复的代码可以正常执行,利用它修复dex就好了。
修复dex
思路:创建一个新的app项目,把代码复制过来修复dex。
1、先创建assets
目录,把样本的dex复制过来。
app启动后把dex复制到data中,重命名为1.dex
,这一块代码直接拿过来用:
复制出来:
public void copyDex() {
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();
} catch (Exception e) {
e.printStackTrace();
}
}
跑一跑看结果:
dipper:/data/data/com.example.myapplication/app_data # ls
1.dex oat
dex复制到内部目录,接着下一步跑fix
的代码。在修复代码的时候他读取是decode.dex
,我们修改为复制出来的1.dex
:
fix函数中修复com.zj.wuaipojie2024_2.A
之后的dex命名为2.dex
。
跑一次修复试试:
private void test() {
Toast.makeText(this, "test", Toast.LENGTH_SHORT).show();
// 模拟绘图后输出密码
isValidate(this, "232", getResources().getIntArray(R.array.A_offset),
"com.zj.wuaipojie2024_2.A");
}
在这里会遇到问题,isValidate
第二个参数是密码,这个随意,R.array.A_offset
从哪里来?
资源array
在开发的时候定义在xml中,在jadx中需要把xml的数据找出来,复制到新项目中。
这样就补了需要的参数。跑一次修复后去查看是否有2.dex
?
dipper:/data/data/com.example.myapplication/app_data # ls
1.dex 2.dex oat
mt打开2.dex转java导出:
package com.zj.wuaipojie2024_2;
import android.content.Context;
public class A {
private static final String SUCCESS_TAG = "唉!";
public static boolean b() {
return false;
}
public static String c(String str) {
return "?" + str + "?";
}
// 处理解锁密码的函数
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";
}
}
导出的代码中d函数已经被修复了:
从修复代码中可以得到信息:
1、校验了app的签名,失败就不做处理。
2、经过一些列处理后,判断传递解锁密码是不是一致,如果不一致返回空,如果一致提示一句话:机缘在B.d
中。
这是checkPassword之后执行的代码块,假设我们现在已经知道了密码,直接去B.d
找机缘就行了。看看B.d
的代码:
但是B里面和之前A一样都是前后加了?,显然不对的,还记得上面的array
?
偏移数据中第一次使用了A_offset
,后面接着有一个B_offset
。
<resources>
<array name="A_offset">
<item>0</item>
<item>3</item>
<item>7908</item>
</array>
<array name="B_offset">
<item>1</item>
<item>1</item>
<item>8108</item>
</array>
</resources>
既然这样,用代码修复一下B
类,我们在上一次修复好的2.dex的基础上修复B,这样代码就完整了。
修改read函数把1.dex修改2.dex。
fix函数中保存的dex名字3.dex
,执行一次修复:
dipper:/data/data/com.example.myapplication/app_data # ls
1.dex 2.dex 3.dex oat
把3.dex拿出来,加入到jadx中
这里就是flag的计算代码了。到此dex修复完成。
获取flag
flag的计算代码是这样的:
public static String d(String str) {
return "机缘是{" + Utils.md5(Utils.getSha1("password+你的uid".getBytes())) + "}";
}
Utils这个工具代码在修复dex中,直接把代码复制到我们的新项目中,他依赖的代码D也复制过来。
现在代码准备好了,这个函数传递str在里面没用到,他提示
password+你的uid,+号不要。uid是自己的论坛id,这个已经拿到了。password
怎么来?
把检测密码是否正确代码拿出来,跑一跑。
1、把native函数注释,这个和计算密码逻辑无关。
2、签名也不用检测了。
3、打印stringBuffer。
得到了锁屏密码:048531267
"机缘是{" + Utils.md5(Utils.getSha1("048531267自己的uid".getBytes())) + "}";
把自己的uid放进去跑一次代码:
最后
第一次玩这个题目,解题流程有点啰嗦了,祝大家新年好,身体健康,万事如意。
文章中样本及单独创建的项目代码可在星球获取。
原文始发于微信公众号(安全后厨):Android逆向技术61——Android逆向吾爱春节中级题目
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论