/**
- 做普通的签名校验
*/ private boolean doNormalSignCheck() { String trueSignMD5 = “d0add9987c7c84aeb7198c3ff26ca152”; String nowSignMD5 = “”; try { // 得到签名的MD5 PackageInfo packageInfo = getPackageManager().getPackageInfo( getPackageName(), PackageManager.GET_SIGNATURES); Signature[] signs = packageInfo.signatures; String signBase64 = Base64Util.encodeToString(signs[0].toByteArray()); nowSignMD5 = MD5Utils.MD5(signBase64); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } return trueSignMD5.equals(nowSignMD5); }
系统将应用的签名信息封装在 PackageInfo 中,调用 PackageManager 的 getPackageInfo(String packageName, int flags) 即可获取指定包名的签名信息。
这个并不用我多说了,如果没听过的话,用搜索引擎找一下「Android 签名校验」,花上几分钟就明白了,很容易的。
编译出 release 包并安装,可以看见运行效果很满意。但是事实真的如此么?下面我们让他作为受害者,被一键破解。
使用工具去除先前的校验
很多人可能不知道,去除简单的签名校验连小朋友都能做到!
请看具有「安全性测试」功能的「M* 管理器」上场,一键去除我们上文准备好的受害者的签名校验:
神奇的一幕发生了,居然还是通过,也就是我们刚才的操作形同虚设,我们把被破解后的安装包传回 PC,准备下一步分析
JADX 上场
为了知道他做了什么,我们需要逆向出目前受害者的代码。这里我们使用开源项目jadx来完成。
打开 jadx 之后会直接弹出「打开」对话框,选取被破解的 apk 即可:
简单对比下可以发现,多了一个「HookApplication」类
点击进去即可直接看见源代码:
public?class?HookApplication?extends?Application?implements?InvocationHandler?{ private?static?final?int?GET_SIGNATURES?=?64; private?String?appPkgName?=?BuildConfig.FLAVOR; private?Object?base; private?byte[][]?sign;
private?void?hook(Context?context)?{ try?{ DataInputStream?dataInputStream?=?new?DataInputStream(new?ByteArrayInputStream(Base64.decode(“省略很长的签名?base64”,?0))); byte[][]?bArr?=?new?byte[(dataInputStream.read()?&?255)][]; for?(int?i?=?0;?i?<?bArr.length;?i++)?{ bArr[i]?=?new?byte[dataInputStream.readInt()]; dataInputStream.readFully(bArr[i]); } Class?cls?=?Class.forName(“android.app.ActivityThread”); Object?invoke?=?cls.getDeclaredMethod(“currentActivityThread”,?new?Class[0]).invoke(null,?new?Object[0]); Field?declaredField?=?cls.getDeclaredField(“sPackageManager”); declaredField.setAccessible(true); Object?obj?=?declaredField.get(invoke); Class?cls2?=?Class.forName(“android.content.pm.IPackageManager”); this.base?=?obj; this.sign?=?bArr; this.appPkgName?=?context.getPackageName(); Object?newProxyInstance?=?Proxy.newProxyInstance(cls2.getClassLoader(),?new?Class[]{cls2},?this); declaredField.set(invoke,?newProxyInstance); PackageManager?packageManager?=?context.getPackageManager(); Field?declaredField2?=?packageManager.getClass().getDeclaredField(“mPM”); declaredField2.setAccessible(true); declaredField2.set(packageManager,?newProxyInstance); System.out.println(“PmsHook?success.”); }?catch?(Exception?e)?{ System.err.println(“PmsHook?failed.”); e.printStackTrace(); } }
/*?access?modifiers?changed?from:?protected?*/ public?void?attachBaseContext(Context?context)?{ hook(context); super.attachBaseContext(context); }
public?Object?invoke(Object?obj,?Method?method,?Object[]?objArr)?throws?Throwable?{ if?(“getPackageInfo”.equals(method.getName()))?{ String?str?=?objArr[0]; if?((objArr[1].intValue()?&?64)?!=?0?&&?this.appPkgName.equals(str))?{ PackageInfo?packageInfo?=?(PackageInfo)?method.invoke(this.base,?objArr); packageInfo.signatures?=?new?Signature[this.sign.length]; for?(int?i?=?0;?i?<?packageInfo.signatures.length;?i++)?{ packageInfo.signatures[i]?=?new?Signature(this.sign[i]); } return?packageInfo; } } return?method.invoke(this.base,?objArr); } }
有点长,但是也不是很费解。
他继承自 Application,重写了 attachBaseContext 来调用 hook(context) ,在里面做了 IPackageManager 的动态代理,实现在调用 getPackageInfo 方法的时候,修改 signatures[] 为在破解之前计算好的数值。这就是为什么我们的检测手段无效了。
所谓的知己知彼,百战不殆,我们先来分析下他做了什么:
- 替换掉原来的 Application
- 在 attachBaseContext 里初始化 hook
- 动态代理 IPackageManager
- hook 替换掉 signatures 的值
所以应对方案也就水到渠成:
- 检查 Application
- 在调用 attachBaseContext 之前检测签名
- 检查 IPackageManager 有没有被动态代理
- 使用别的 API 去获取
检查 Application
他替换掉了 Application 为他自己的,那么变化的太多了,Application 的类名 / 方法数 / 字段数 / AndroidManifast 中 Application 节点的 name,都会变。我们这里以检查 Application 的类名为例:
/**
-
校验 application */ private boolean checkApplication(){ Application nowApplication = getApplication(); String trueApplicationName = “MyApp”; String nowApplicationName = nowApplication.getClass().getSimpleName(); return trueApplicationName.equals(nowApplicationName); } -
先定义我们自己的 Application ——「MyApp」 -
然后通过 getApplication() 获取到 Application 实例 -
然后通过 getClass() 获取到类信息 -
然后通过 getSimpleName() 获取到类名 -
与正确的值比对然后返回
可以看到可以检测出被二次打包
在?attachBaseContext 之前检测
只要我们检测的够早,他就追不上我们。不,他会 hook 到我们的几率就越小
A: 要有多早? B: emm,就在 Application 的构造方法里检测吧 A: 那,,,没 context 呀 B: 那就自己造一个 context! A: 你放屁! B: 走你
通过学习 Application 的创建流程可知,Context 是通过 LoadedApk 调用 createAppContext 方法实现的
// LoadedApk.java package android.app; ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
函数原型为
//?ContextImpl.java package?android.app;
@UnsupportedAppUsage static?ContextImpl?createAppContext(ActivityThread?mainThread,?LoadedApk?packageInfo)?{ return?createAppContext(mainThread,?packageInfo,?null); }
第一个参数好说,因为这是个单例类,调用 currentActivityThread 即可获取 ActivityThread 对象
//?ActivityThread.java package?android.app;
@UnsupportedAppUsage private?static?volatile?ActivityThread?sCurrentActivityThread;
@UnsupportedAppUsage public?static?ActivityThread?currentActivityThread()?{ return?sCurrentActivityThread; }
但是需要注意的是有 「@UnsupportedAppUsage」修饰,需要反射调用。在学习 Application 的创建流程的时候可知(其实是我不会上网找的流程),另一个 LoadedApk 对象是通过 getPackageInfoNoCheck 方法创建的。
//?ActivityThread.java package?android.app;
@Override @UnsupportedAppUsage public?final?LoadedApk?getPackageInfoNoCheck(ApplicationInfo?ai, CompatibilityInfo?compatInfo)?{ return?getPackageInfo(ai,?compatInfo,?null,?false,?true,?false); }
这个值保存在 ActivityThread 实例的 mBoundApplication.info 变量里。
//?ActivityThread.java package?android.app;
@UnsupportedAppUsage AppBindData?mBoundApplication;
@UnsupportedAppUsage private?void?handleBindApplication(AppBindData?data)?{ //?省略无关代码 mBoundApplication?=?data; //?省略无关代码 data.info?=?getPackageInfoNoCheck(data.appInfo,?data.compatInfo); //?省略无关代码 }
mBoundApplication 虽然不是静态变量,但是因为我们之前已经获取到了 ActivityThread 实例,所以不耽误我们反射获取。现在我们调用 ContextImpl.createAppContext 的条件已经满足了,反射调用即可。
ContextUtils 最终实现代码如下:
public?class?ContextUtils?{
/** *?手动构建?Context */ @SuppressLint({“DiscouragedPrivateApi”,“PrivateApi”}) public?static?Context?getContext()?throws?ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException, ?手动构建?Context */ @SuppressLint({“DiscouragedPrivateApi”,“PrivateApi”}) public?static?Context?getContext()?throws?ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException,
|