Android-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.javapackage android.app;ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
函数原型为
//?ContextImpl.javapackage?android.app;
@UnsupportedAppUsagestatic?ContextImpl?createAppContext(ActivityThread?mainThread,?LoadedApk?packageInfo)?{return?createAppContext(mainThread,?packageInfo,?null);}
第一个参数好说,因为这是个单例类,调用 currentActivityThread 即可获取 ActivityThread 对象
//?ActivityThread.javapackage?android.app;
@UnsupportedAppUsageprivate?static?volatile?ActivityThread?sCurrentActivityThread;
@UnsupportedAppUsagepublic?static?ActivityThread?currentActivityThread()?{return?sCurrentActivityThread;}
但是需要注意的是有 「@UnsupportedAppUsage」修饰,需要反射调用。在学习 Application 的创建流程的时候可知(其实是我不会上网找的流程),另一个 LoadedApk 对象是通过 getPackageInfoNoCheck 方法创建的。
//?ActivityThread.javapackage?android.app;
@Override@UnsupportedAppUsagepublic?final?LoadedApk?getPackageInfoNoCheck(ApplicationInfo?ai,CompatibilityInfo?compatInfo)?{return?getPackageInfo(ai,?compatInfo,?null,?false,?true,?false);}
这个值保存在 ActivityThread 实例的 mBoundApplication.info 变量里。
//?ActivityThread.javapackage?android.app;
@UnsupportedAppUsageAppBindData?mBoundApplication;
@UnsupportedAppUsageprivate?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,NullPointerException{
//?反射获取?ActivityThread?的?currentActivityThread?获取?mainThreadClass?activityThreadClass?=?Class.forName("android.app.ActivityThread");Method?currentActivityThreadMethod?=activityThreadClass.getDeclaredMethod("currentActivityThread");currentActivityThreadMet
hod.setAccessible(true);Object?mainThreadObj?=?currentActivityThreadMethod.invoke(null);
//?反射获取?mainThread?实例中的?mBoundApplication?字段 Field?mBoundApplicationField?=?activityThreadClass.getDeclaredField("mBoundApplication");mBoundApplicationField.setAccessible(true);Object?mBoundApplicationObj?=?mBoundApplicationField.get(mainThreadObj);
//?获取?mBoundApplication?的?packageInfo?变量 if?(mBoundApplicationObj?==?null)?throw?new?NullPointerException("mBoundApplicationObj 反射值空");Class?mBoundApplicationClass?=?mBoundApplicationObj.getClass();Field?infoField?=?mBoundApplicationClass.getDeclaredField("info");infoField.setAccessible(true);Object?packageInfoObj?=?infoField.get(mBoundApplicationObj);
//?反射调用?ContextImpl.createAppContext(ActivityThread?mainThread,?LoadedApk?packageInfo)if?(mainThreadObj?==?null)?throw?new?NullPointerException("mainThreadObj 反射值空");if?(packageInfoObj?==?null)?throw?new?NullPointerException("packageInfoObj 反射值空");Method?createAppContextMethod?=?Class.forName("android.app.ContextImpl").getDeclaredMethod("createAppContext",?mainThreadObj.getClass(),?packageInfoObj.getClass());createAppContextMethod.setAccessible(true);return?(Context)?createAppContextMethod.invoke(null,?mainThreadObj,?packageInfoObj);
}}
后面的事就好办多了,就是在 Application 的构造函数里用我们手动构造的 context 去获取签名(这个时候还没有 context)
public?class?MyApp?extends?Application?{
private?static?boolean?sEarlyCheckSignResult?=?false;public?static?boolean?getEarlyCheckSignResult(){?return?sEarlyCheckSignResult;}
public?MyApp()?{//?在构造函数里提早检测 sEarlyCheckSignResult?=?earlyCheckSign();}
boolean?earlyCheckSign(){
评论