Android逆向(五) 找flag实例

一、系统环境

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.C

Found 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}

  • 25
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值