问题来源
在使用Xpatch(https://github.com/WindySha/Xpatch
)对App进行二次打包时,经常会出现重打包后的App启动卡死或者无法获取到网络数据。这是因为这些App在启动时做了签名校验,检验App的签名是不是App自己原本的签名,不是,则阻止程序继续执行。
因此,破解App签名显得非常有必要。
本文主要就介绍使用Xpatch破解App签名校验的两种方法。
破解步骤
在破解某个App的签名之前,我们先需要获取这个App的原签名,然后Hook获取App签名的方法,使其返回的结果跟原App里返回结果一致,这样就能够破解App的签名校验。
因此,步骤可以总结为两步:
- 获取App原签名信息;
- 替换App获取签名方法的返回结果,使其跟原签名结果一致。
获取原签名
获取原签名其实非常简单,Android系统已经提供公开的API。
签名文件信息是在App安装的时候通过PMS解析出来,保存到应用的PackageInfo
中,获取指定包名的PackageInfo
信息,只需调用的PackageManager
的getPackageInfo
方法即可,代码如下:
public static String getPackageSignature(Context context, String packageName) {
String sigature = null;
try {
PackageInfo info = context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
if (info.signatures != null && info.signatures.length > 0) {
sigature = info.signatures[0].toCharsString();
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return sigature;
}
获取指定一个包名的签名,我们只需写一个demo App,在demo的Application
onCreate
方法里(或者其他任意方法)调用上面方法即可,调用此方法时,context
传demo的context,packageName
传需要获取的App包名。返回结果后,将返回的字符串打印日志(或其他方式)记录下来,便于下面的Hook方法中使用。
使用Xposed Hook获取签名的方法
既然经过Xpatch重打包后的App可以加载任意Xposed插件(https://github.com/WindySha/Xpatch),那我们何不写一个Xposed插件Hook住上面获取App的签名的方法呢。
App获取签名的方法是PackageManager
的getPackageInfo
方法,而PackageManager
是一个抽象类,其具体的实现类是android.app.ApplicationPackageManager.java
。其实只用Hook这个类的getPackageInfo
方法即可。
由于getPackageInfo
方法返回的是一个packageInfo
对象,签名字符只是这个对象一个成员变量,因此我们不需要完全替换此方法的返回结果,只需要替换返回结果里的签名字符信息即可。
故,选择在afterHookedMethod
方法里执行替换签名字符的操作,具体实现代码如下:
// App的原签名
private final static String originalSignature = "3082023b308201a4a00302010202044be8c388300d06092a86....";
public static void hookSignature(ClassLoader classLoader, final String targetPackageName) {
XposedHelpers.findAndHookMethod("android.app.ApplicationPackageManager", classLoader,
"getPackageInfo", String.class, int.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
try {
if (param.args[0] != null && param.args[0] instanceof String) {
String packageName = (String) param.args[0];
if (!packageName.equals(targetPackageName)) {
return;
}
}
int flag = (int) param.args[1];
PackageInfo packageInfo = (PackageInfo) param.getResult();
if (PackageManager.GET_SIGNATURES == flag) {
if (packageInfo.signatures != null && packageInfo.signatures.length > 0) {
// 替换结果里的签名信息
packageInfo.signatures[0] = new Signature(originalSignature);
}
} else if (Build.VERSION.SDK_INT >= 28 && PackageManager.GET_SIGNING_CERTIFICATES == flag) {
if (packageInfo.signingInfo != null) {
Signature[] signaturesArray = packageInfo.signingInfo.getApkContentsSigners();
if (signaturesArray != null && signaturesArray.length > 0) {
signaturesArray[0] = new Signature(originalSignature);
}
}
}
// 更改最终的返回结果
param.setResult(packageInfo);
} catch (Throwable e) {
}
}
})
}
将上面代码整理成一个Xposed插件,即可实现破解指定App的签名校验。
使用动态代理Hook获取签名的接口
其实使用Xposed Hook的方法已经非常简单了,使用动态代码的方案一般是在没有Xposed框架支持的情况下使用,其通用性更强一些,但实现起来也更加复杂。这里介绍动态代码的方法主要用于拓展思路。
Java的动态代理技术在这里就不详细介绍,不了解原理的可以查阅其他相关资料学习,这里主要介绍运用动态代码实现PMS在本地的代理方法的拦截,从而实现签名的破解。
因为ApplicationPackageManager
的getPackageInfo
的最终实现是IPackageManager
接口通过Binder跨进程调用PMS实现的,其实现逻辑如下:
@Override
public PackageInfo getPackageInfo(String packageName, int flags)
throws NameNotFoundException {
return getPackageInfoAsUser(packageName, flags, mContext.getUserId());
}
private final IPackageManager mPM;
@Override
public PackageInfo getPackageInfoAsUser(String packageName, int flags, int userId)
throws NameNotFoundException {
try {
PackageInfo pi = mPM.getPackageInfo(packageName, flags, userId);
if (pi != null) {
return pi;
}
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
throw new NameNotFoundException(packageName);
}
在动态代理之前,除了需要知道是代理哪个接口,还需要知道实现这个接口的对象。
在android.app.ActivityThread.java
中可以找到其实现对象:
static volatile IPackageManager sPackageManager;
public static IPackageManager getPackageManager() {
if (sPackageManager != null) {
return sPackageManager;
}
IBinder b = ServiceManager.getService("package");
sPackageManager = IPackageManager.Stub.asInterface(b);
return sPackageManager;
}
通过反射调用ActivityThread
的getPackageManager
方法即可获取到接口的实例。
动态代理HookgetPackageInfo
实现代码为:
public static void hookPMS(Context context) {
try {
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
// 获取全局的ActivityThread对象的实现
Object activityThreadObj = currentActivityThreadMethod.invoke(null);
// 获取ActivityThread里的getPackageManager方法获取原始的sPackageManager对象
Method getPackageManagerMethod = activityThreadClass.getDeclaredMethod("getPackageManager");
getPackageManagerMethod.setAccessible(true);
Object packageManagerObj = getPackageManagerMethod.invoke(activityThreadObj);
// 准备好代理对象, 用来替换原始的对象
Class<?> iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager");
Object proxy = Proxy.newProxyInstance(
iPackageManagerInterface.getClassLoader(),
new Class<?>[]{iPackageManagerInterface},
new MyInvocationHandler(packageManagerObj));
// 1. 替换掉ActivityThread里面的 sPackageManager 字段
Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager");
sPackageManagerField.setAccessible(true);
sPackageManagerField.set(activityThreadObj, proxy);
// 2. 替换 ApplicationPackageManager里面的 mPM对象
PackageManager pm = context.getPackageManager();
Field mPmField = pm.getClass().getDeclaredField("mPM");
mPmField.setAccessible(true);
mPmField.set(pm, proxy);
} catch (Exception e) {
}
}
private final static String originalSignature = "3082023b308201a4a00302010202044be8c388300d06092a86....";
static class MyInvocationHandler implements InvocationHandler {
private Object pmBase;
public MyInvocationHandler(Object base) {
pmBase = base;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("getPackageInfo".equals(method.getName())) {
if (args[0] != null && args[0] instanceof String) {
String packageName = (String) args[0];
if (!packageName.equals(currentPackageName)) {
return method.invoke(pmBase, args);
}
}
Integer flag = (Integer) args[1];
if (PackageManager.GET_SIGNATURES == flag) {
PackageInfo packageInfo = (PackageInfo) method.invoke(pmBase, args);
if (packageInfo.signatures != null && packageInfo.signatures.length > 0) {
// 替换结果里的签名信息
packageInfo.signatures[0] = new Signature(originalSignature);
}
return packageInfo;
} else if (Build.VERSION.SDK_INT >= 28 && PackageManager.GET_SIGNING_CERTIFICATES == flag) {
PackageInfo packageInfo = (PackageInfo) method.invoke(pmBase, args);
if (packageInfo.signingInfo != null) {
Signature[] signaturesArray = packageInfo.signingInfo.getApkContentsSigners();
if (signaturesArray != null && signaturesArray.length > 0) {
signaturesArray[0] = new Signature(originalSignature);
}
}
return packageInfo;
}
}
return method.invoke(pmBase, args);
}
}
通过动态代理不仅可以HookPackageManger
的方法,也可以Hook住所有system_server进程的服务在本地的代理,比如AMS,PMS,IMMS等等。而且,这种技术在插件化开发中运用非常广泛。著名的双开框架VirtualApp就大量运用到动态代理Hook系统服务的技术。如果想对此深入了解,建议阅读此文:http://weishu.me/2016/03/07/understand-plugin-framework-ams-pms-hook/,这篇文档讲解十分详细。
验证效果
为了验证上面的方案可行,我们需要找一个做了签名校验的App进行验证。假如没有现成的App用来验证,也可以自己写一个Demo,并将Demo App签名,然后破解验证。不过,这样做还是比较繁琐。
庆幸的是,笔者找到了一个做了签名校验的App,趣头条App。
通过Xpatch重打包后的趣头条App,安装后,新闻列表界面无法显示数据。猜测可能是做了签名校验导致的问题。因此,我们使用上面的方法一(Xposed Hook方法)来破解签名,最终结果令人惊喜,趣头条新闻列表数据成功刷新出来!!这样,既验证了我们破解签名成功,也说明趣头条确实是做了签名校验。
下面贴上趣头条的原Apk签名信息字符,感兴趣的可以自己写个Xposed插件破解试试看。
private static final String QU_TOU_TIAO_PACKAGENAME = "com.jifen.qukan";
private static final String QU_TOU_TIAO_SIGNATURE = "3082021b30820184a0030201020204574beab6300d06092a864886f70d01010505003052310c300a06035504061303303231310b3009060355040813025348310b3009060355040713025348310b3009060355040a13025a48310b3009060355040b1302434e310e300c0603550403130551754b616e301e170d3136303533303037323433385a170d3431303532343037323433385a3052310c300a06035504061303303231310b3009060355040813025348310b3009060355040713025348310b3009060355040a13025a48310b3009060355040b1302434e310e300c0603550403130551754b616e30819f300d06092a864886f70d010101050003818d0030818902818100aa5bae49b771380e692444437b82b375cabdefb3f23307c29510653776b8e4115f776bea5eb6690285f97d4e6e8d0469e49f79ecba31e4b7fb85dd612ee6b27ef38502aa38d055ddad2aa7b52d19fb8d2aeeb59a830b91c341f1b467655e7313e9ff65feb6539bf1655f35a37e17faa94e506a08219df196730f45d9c1cd94d30203010001300d06092a864886f70d0101050500038181000e6cc9fb74aef11dd33d6603869a9db61b8dcedae77bc815433026693fe59fd4b75a3284170f8872737e55595c1fd40da3dfbe5ad8a4e96802f53637977f0eb6e9b0dc35161cbaed398b41ecd73c4009a1dae7bcb00b75c3f8d5792405bcc5e4602d9dff6a0dc4739240a3b42626f5efce4d7baea0fced2b13361cb4ded8ed0b"
欢迎关注个人技术公众号:Android葵花宝典
或微信扫一扫关注
![](https://img.haomeiwen.com/i1639238/cb01855ba0ca2745.jpg)
网友评论