上一章我们说了签名的一些基本信息,这一章说说它的用法。我们知道 android 存在二次打包的现象,这样可以篡改一些功能,所以部分app会在一些重要功能或者接口请求或者程序的入口的地方使用签名校验,校验下 SHA1 或 MD5 等信息,看看是否是自己原始的签名,如果不是,则强制程序退出。这个属于安全防御,自然有盾就有矛,现在就探讨一下怎么绕过签名校验,也就是常说的 hook 。
所谓 hook ,也就是通过反射和动态代理,做一些常规做不到的操作,比如替换对象的属性值,或者拦截对象的方法等等,现在先说个简单的,比如 View 的点击事件,通常是 setOnClickListener() 设置个点击回调,如果这个 view 是三方的,并且已经被设置了点击事件,此时如果我们想统计它的点击次数怎么办?这时可以考虑反射了。
我们知道 View 的 点击事件
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
static class ListenerInfo {
...
public OnClickListener mOnClickListener;
}
看看 getListenerInfo() 是什么,它是个 ListenerInfo 对象,并且还是 View 的成员变量。由上面的这些信息,我们通过反射可以获取到这个属性值,然后进一步通过反射后去 ListenerInfo 里面的属性 mOnClickListener 的值,这样我们就拿到了 View 的点击事件的回调,我们这时候把点击回调包裹起来,重新赋值给 mOnClickListener 即可。好,上代码
private void hookClick(View view) {
try {
// 得到 View 的 ListenerInfo 对象
Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo");
getListenerInfo.setAccessible(true);
Object listenerInfo = getListenerInfo.invoke(view);
// 得到 原始的 OnClickListener 对象
Class> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");
Field clickListener = listenerInfoClz.getDeclaredField("mOnClickListener");
clickListener.setAccessible(true);
View.OnClickListener originOnClickListener = (View.OnClickListener) clickListener.get(listenerInfo);
// 用自定义的 OnClickListener 替换原始的 OnClickListener
View.OnClickListener hookedOnClickListener;
if(originOnClickListener instanceof HookedOnClickListener){
HookedOnClickListener listener = (HookedOnClickListener) originOnClickListener;
hookedOnClickListener = listener.origin;
} else {
hookedOnClickListener = new HookedOnClickListener(originOnClickListener);
}
clickListener.set(listenerInfo, hookedOnClickListener);
} catch (Exception e) {
}
}
static class HookedOnClickListener implements View.OnClickListener {
public View.OnClickListener origin;
HookedOnClickListener(View.OnClickListener origin) {
this.origin = origin;
}
@Override
public void onClick(View v) {
// 在这里添加自己想做的逻辑
Toast.makeText(getApplicationContext(), "hook click", Toast.LENGTH_SHORT).show();
if (origin != null) {
origin.onClick(v);
}
}
}
以上,就是一个简单的hook样例,这个是用反射来完成的。
InvocationHandler 及 Proxy 是动态代理,它里面的实现原理就是反射,封装了反射的代码,InvocationHandler 使用必须是接口才能调用。饶了这么一圈,才到了怎么hook签名这一步。 我们一般获取签名 SHA1 和 MD5 的方法如下
public static String takeSHA1Finger(Context context) {
PackageManager pm = context.getPackageManager();
String packageName = context.getPackageName();
int flags = PackageManager.GET_SIGNATURES;
PackageInfo packageInfo = null;
try {
//获得包的所有内容信息类
packageInfo = pm.getPackageInfo(packageName, flags);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
//签名信息
Signature[] signatures = packageInfo.signatures;
byte[] cert = signatures[0].toByteArray();
//将签名转换为字节数组流
InputStream input = new ByteArrayInputStream(cert);
CertificateFactory cf = null;
try {
cf = CertificateFactory.getInstance("X509");
} catch (Exception e) {
e.printStackTrace();
}
X509Certificate c = null;
try {
c = (X509Certificate) cf.generateCertificate(input);
} catch (Exception e) {
e.printStackTrace();
}
String hexString = null;
try {
//加密算法的类
MessageDigest md = MessageDigest.getInstance("SHA1");
// MessageDigest md = MessageDigest.getInstance("SHA256");
// MessageDigest md = MessageDigest.getInstance("MD5");
//获得公钥
byte[] publicKey = md.digest(c.getEncoded());
hexString = byte2HexFormatted(publicKey);
} catch (Exception e) {
e.printStackTrace();
}
return hexString;
}
/**
* 编码进行16 进制转换
*/
public static String byte2HexFormatted(byte[] arr) {
StringBuilder str = new StringBuilder(arr.length * 2);
for (int i = 0; i 2)
h = h.substring(l - 2, l);
str.append(h.toUpperCase());
if (i < (arr.length - 1))
str.append(':');
}
return str.toString();
}
比较关键的是 Signature[] signatures = packageInfo.signatures; 这行代码,签名的信息都存在这个里面,为了获取 App 的这个信息,我们也可以写个方法
public static void getSignature(Context context) {
try {
PackageInfo packageInfo = context.getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES);
if (packageInfo.signatures != null) {
return packageInfo.signatures[0].toCharsString();
}
} catch (Exception e) {
}
return null;
}
打印后,会是一个长串的值
30801b751426aa6dd5589e853489d13fccd2034130820229a0030201020204272eafed300d06092a864886f70d01010b05003051310b300906035504061302434e310b3009060355040813027368310b3009060355040713027368310c300a060355040a1303647a70310c300a060355040b1303647a70310c300a06035504031303647a70301e170d3230303731353038323434375a170d3435303730393038323434375a3051310b300906035504061302434e310b3009060355040813027368310b3009060355040713027368310c300a060355040a1303647a70310c300a060355040b1303647a70310c300a06035504031303647a7030820122300d06092a864886f70d01010105000382010f003082010a0282010100b0cef1dd755268d6f339cd897d7748f2f6cb641d8d7c4f0f2ccb6de3ed2d5a4c9405d792debdaa672aa8f8c1aa45f8233ebcacfc3343dc5b0eefa5f7038e9d4808067d2075eacc1ba1c80bc19bcaaba87d50073856de1715f3c5c2af52dd32709da2f254b106db20c96054c4efb238463d7b8b6b840adb5ac10feaf7d557181a8f5f07c36c1b24a2ba240442b71ca88771c5e471e7f24ef386226ca073890c46866a2da9c6ae5a318ede037291f72f5cc8ee022b00da014b68954c5c137a7637f0380a6349b9397b4e6ccee70ce04ba47433c2f6e4c8c9f0702a6eab2db0992ac6ad7d29791b6491d9fb3e37d7f275badb902053bff6175508a605d8ab0f7d5d0203010001a321301f301d0603551d0e0416041471c21602ed4357112be3e354f82b2c3c27590bd8300d06092a864886f70d01010b0500038201010080360bcb21c74c448c91d59c57f3aae80e1b7de47ffc06e85842268051e8642e6922b44bcb9bf4803b1b22ba49379a58200b4589decbf74eb28b2c059e524316d6f75cab06b2e8fdb8b22c73e327ef4ba3ab788a5f8194daf46e6ece40760f6d92bade8928289b5ff3700b36f96d72d40c5b059b8725f6c7f564f48caa8b3140f1398a501381b8c560ccd28fd1365320cc998064cb67a433590919a24d01f3aa4664c29316a9327ccaeae68d5fe3183eb3a6805a200fdaa03ffa0d8aac44131004846db2730d786fd51abdf47bd8c2bff4f518006b62aecf22cc64c5b6716d1754dccf20b44d78e85f068445c018d505
到这,基本就明白了。上面这个值决定了签名的值,那么我们只要想办法替换这个签名值就行了。一般二次打包用的都是自己的key,那么在这个地方返回自己的签名值就能把签名判断的问题给解决了,怎么返回呢?动态代理出场了。
这个值是通过 getPackageManager() 获取的,那么我们只要想办法去改变 PackageManager 的方法返回值就行。了解 Activity 的启动跳转流程的朋友,应该都了解 ActivityThread 这个类,我们获取它的 sPackageManager 这个属性。为什么是它? Context 调用 getPackageManager() 方法,实际上调用了 ContextImpl 的 getPackageManager() 方法
@Override
public PackageManager getPackageManager() {
if (mPackageManager != null) {
return mPackageManager;
}
IPackageManager pm = ActivityThread.getPackageManager();
if (pm != null) {
// Doesn't matter if we make more than one instance.
return (mPackageManager = new ApplicationPackageManager(this, pm));
}
return null;
}
// ApplicationPackageManager 类
protected ApplicationPackageManager(ContextImpl context,
IPackageManager pm) {
mContext = context;
mPM = pm;
}
@Override
public PackageInfo getPackageInfo(String packageName, int flags)
throws NameNotFoundException {
return getPackageInfoAsUser(packageName, flags, mContext.getUserId());
}
@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);
}
从这一串代码中,可以看出最终调用的是 mPM.getPackageInfo(packageName, flags, userId) 这行代码,mPM 是 IPackageManager pm = ActivityThread.getPackageManager() 从这里来的,看看这个方法
public static IPackageManager getPackageManager() {
if (sPackageManager != null) {
//Slog.v("PackageManager", "returning cur default = " + sPackageManager);
return sPackageManager;
}
IBinder b = ServiceManager.getService("package");
//Slog.v("PackageManager", "default service binder = " + b);
sPackageManager = IPackageManager.Stub.asInterface(b);
//Slog.v("PackageManager", "default service = " + sPackageManager);
return sPackageManager;
}
这里返回的就是 ActivityThread 中的 sPackageManager 这个属性。前因清楚了,那么就看看怎么替换它吧,老规矩,使用反射;动态代理也登场了,
public static void hookPMS(Context context, String signed){
try{
Class> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod =
activityThreadClass.getDeclaredMethod("currentActivityThread");
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
// 获取 ActivityThread 里面原始的 sPackageManager
Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager");
sPackageManagerField.setAccessible(true);
Object sPackageManager = sPackageManagerField.get(currentActivityThread);
// 代理对象, 用来替换原始的对象
Class> iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager");
Object proxy = Proxy.newProxyInstance(iPackageManagerInterface.getClassLoader(),
new Class>[] { iPackageManagerInterface },
new PmsHookInvocationHandler(sPackageManager, signed));
// 1. 替换掉ActivityThread里面的 sPackageManager 字段
sPackageManagerField.set(currentActivityThread, proxy);
// 2. 替换 ApplicationPackageManager里面的 mPM对象
PackageManager pm = context.getPackageManager();
Field mPmField = pm.getClass().getDeclaredField("mPM");
mPmField.setAccessible(true);
mPmField.set(pm, proxy);
}catch (Exception e){
}
}
上面是替换了 ActivityThread 和 PackageManager 中的 sPackageManager ,我们看看代理类的代码
public class PmsHookInvocationHandler implements InvocationHandler {
private Object base;
private String SIGN;
public PmsHookInvocationHandler(Object base, String sign) {
this.base = base;
this.SIGN = sign;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if("getPackageInfo".equals(method.getName())){
String pkgName = (String)args[0];
Integer flag = (Integer)args[1];
if(flag == PackageManager.GET_SIGNATURES){
Signature sign = new Signature(SIGN);
PackageInfo info = (PackageInfo) method.invoke(base, args);
info.signatures[0] = sign;
return info;
}
}
return method.invoke(base, args);
}
}
hookPMS(Context context, String signed) 方法中的 signed 就是我们自己二次打包的签名值,可以先通过代码获取一下,这个值只与签名keystore有关。
今天涉及到的是逆向的基本知识点,我们使用逆向知识为了确保手机安全及技能提升,不是用于钻 App 的漏洞,不允许用于非法盈利及篡改别人的应用。