From a8c51116b88fe71aa451380c8c3bd496a4bc0053 Mon Sep 17 00:00:00 2001 From: mahongyin <1976222027@qq.com> Date: Tue, 2 Mar 2021 16:36:38 +0800 Subject: [PATCH] =?UTF-8?q?=20App.hook(context);//hook=E7=AD=BE=E5=90=8D?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=20//=E5=9C=A8=E8=BF=99=E9=87=8C=20=E9=87=8D?= =?UTF-8?q?=E7=BD=AEPackageManager=20=E5=8F=AA=E8=A6=81=E5=9C=A8=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E5=89=8D=E9=87=8D=E7=BD=AE=E5=8D=B3=E5=8F=AF=20=20//?= =?UTF-8?q?=20AppSigning.resetPackageManager(getBaseContext());=E5=86=8Dho?= =?UTF-8?q?ok=E4=B9=8B=E5=90=8E=20=E9=AA=8C=E8=AF=81=E7=AD=BE=E5=90=8D?= =?UTF-8?q?=E4=B9=8B=E5=89=8D=E9=87=8D=E7=BD=AE=E5=8D=B3=E5=8F=AF=20?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E7=9C=9F=E5=AE=9E=E7=AD=BE=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit APISecurity.init(this)//验证三步走 1.验证签名是否符合自己预设 2.验证包名 3验证apk源文件签名信息 --- README.md | 8 + apisecurity/build.gradle | 4 +- .../java/cn/android/security/APISecurity.java | 213 +++++++---- .../java/cn/android/security/AppSigning.java | 304 ++++++++++++---- app/build.gradle | 14 +- app/src/main/AndroidManifest.xml | 1 + app/src/main/cpp/apisecurity-lib.cpp | 336 +++++++++++++----- .../cn/android/sample/HookApplication.java | 21 +- .../java/cn/android/sample/MainActivity.java | 11 +- app/src/main/res/layout/activity_main.xml | 6 +- build.gradle | 2 +- gradle.properties | 2 + gradle/wrapper/gradle-wrapper.properties | 2 +- 防止被一键去除签名校验.zip | Bin 0 -> 18871 bytes 14 files changed, 687 insertions(+), 237 deletions(-) create mode 100644 防止被一键去除签名校验.zip diff --git a/README.md b/README.md index 479b422..96e7b7f 100644 --- a/README.md +++ b/README.md @@ -55,3 +55,11 @@ Android API Security(.so),安卓APP/API安全加密so库,防二次打包, String val = "POST https://www.xxx.com/login?id=1&pwd=xxx......"; String sign = MGAPISecurity.sign(aptStr) ``` + App.hook(context);//hook签名验证 +//在这里 重置PackageManager 只要在验证前重置即可 + // AppSigning.resetPackageManager(getBaseContext());再hook之后 验证签名之前重置即可 获取真实签名 + + APISecurity.init(this)//验证三步走 + 1.验证签名是否符合自己预设 + 2.验证包名 + 3验证apk源文件签名信息 \ No newline at end of file diff --git a/apisecurity/build.gradle b/apisecurity/build.gradle index a089e8f..4063dd8 100644 --- a/apisecurity/build.gradle +++ b/apisecurity/build.gradle @@ -8,7 +8,7 @@ android { targetSdkVersion ANDROID_BUILD_TARGET_SDK_VERSION as int versionCode 1 versionName "1.0" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } @@ -21,5 +21,5 @@ android { } dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.annotation:annotation:1.0.0' } \ No newline at end of file diff --git a/apisecurity/src/main/java/cn/android/security/APISecurity.java b/apisecurity/src/main/java/cn/android/security/APISecurity.java index 67c6140..acda096 100644 --- a/apisecurity/src/main/java/cn/android/security/APISecurity.java +++ b/apisecurity/src/main/java/cn/android/security/APISecurity.java @@ -4,14 +4,30 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.content.pm.Signature; import android.os.Build; import android.os.Debug; +import android.text.TextUtils; +import android.util.DisplayMetrics; import android.util.Log; import java.io.BufferedReader; +import java.io.ByteArrayInputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; @@ -31,97 +47,108 @@ public class APISecurity { /** * 被Native调用的Java方法 - * - * @param msg */ public void javaMethod(String msg) { - Log.e("错误代码",msg); + Log.e("错误代码", msg); // System.exit(1); } - private void verify(Context context){ -// String ppp = runCommand().get(0); - - Log.e("包路径文件签名", getApkSignatures(context,"com.tencent.mm")); - - Log.e("已安装APP签名", AppSigning.getSingInfo(context, "com.tencent.mm", AppSigning.SHA1)); + public static void verify(Context context) { + Log.e("mhyLog", "hash"+AppSigning.getSignatureHash(context)); + runCommand(); + Log.e("包路径文件签名", getApkSignatures(context, context.getPackageName())); + Log.e("已安装APP签名", getInstalledAPKSignature(context, context.getPackageName())); //通过获取其他应用的签名 如果一样那么被hook了 } -//从安装文件获取签名 + /** 防破签名 1 + * 获取已安装的app签名 + * */ + public static String getInstalledAPKSignature(Context context, String packageName) { + PackageManager pm = context.getPackageManager(); + try { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { + PackageInfo appInfo = pm.getPackageInfo(packageName.trim(), PackageManager.GET_SIGNING_CERTIFICATES); + if (appInfo == null || appInfo.signingInfo == null) + return ""; + return AppSigning.getSignatureString(appInfo.signingInfo.getApkContentsSigners(), AppSigning.SHA1); + } else { + PackageInfo appInfo = pm.getPackageInfo(packageName.trim(), PackageManager.GET_SIGNATURES); + if (appInfo == null || appInfo.signatures == null) + return ""; + return AppSigning.getSignatureString(appInfo.signatures, AppSigning.SHA1); + } + } catch (Exception e) { + e.printStackTrace(); + } + return ""; + } + + /** 防破签名 2 + * C调用Java 从源安装文件获取签名信息 + * */ public static String getApkSignatures(Context context, String packname) { String sign = ""; String path = null; - try { - path = context.getPackageManager().getApplicationInfo(packname, 0).sourceDir; - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - } -// PackageInfo packageInfo = context.getPackageManager().getPackageInfo(packname, PackageManager.GET_META_DATA); -// ApplicationInfo applicationInfo = packageInfo.applicationInfo; -// String path =applicationInfo.publicSourceDir;//sourceDir; // 获取当前apk包的绝对路径 - File apkFile=new File(path); +// try {//获取此包安装路径 +// //第一种方法 +// ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(packname, 0); +// path = applicationInfo.sourceDir; +// //第二种方法 +//// ApplicationInfo applicationInfo = context.getPackageManager().getPackageInfo(packname, PackageManager.GET_META_DATA).applicationInfo; +//// path = applicationInfo.publicSourceDir;//sourceDir; // 获取当前apk包的绝对路径 +// Log.e("其他已知包名apk的安装路径", applicationInfo.sourceDir+ "&---&" + applicationInfo.publicSourceDir); +// } catch (PackageManager.NameNotFoundException e) { +// e.printStackTrace(); +// //第三中方法 +// path=context.getPackageResourcePath(); +// Log.e("在apk中获取自身安装路径", path); +// } + //第三中方法 + path=context.getPackageResourcePath(); + // Log.e("在apk中获取自身安装路径", path); + File apkFile = new File(path); if (apkFile != null && apkFile.exists()) { - Log.e("pppp",apkFile.getAbsolutePath()); + Log.e("包安装路径", apkFile.getAbsolutePath()); PackageManager pm = context.getPackageManager(); if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { PackageInfo pkgInfo = pm.getPackageArchiveInfo(apkFile.getAbsolutePath(), PackageManager.GET_SIGNING_CERTIFICATES); if (pkgInfo != null && pkgInfo.signingInfo != null && pkgInfo.signingInfo.getApkContentsSigners().length > 0) { -// sign = pkgInfo.signingInfo.getApkContentsSigners()[0].toCharsString(); - sign = AppSigning.getSignatureString(pkgInfo.signingInfo.getApkContentsSigners()[0],AppSigning.SHA1); + sign = AppSigning.getSignatureString(pkgInfo.signingInfo.getApkContentsSigners(), AppSigning.SHA1); } } else { PackageInfo pkgInfo = pm.getPackageArchiveInfo(apkFile.getAbsolutePath(), PackageManager.GET_SIGNATURES); if (pkgInfo != null && pkgInfo.signatures != null && pkgInfo.signatures.length > 0) { -// sign = pkgInfo.signatures[0].toCharsString(); - sign = AppSigning.getSignatureString(pkgInfo.signatures[0],AppSigning.SHA1); + sign = AppSigning.getSignatureString(pkgInfo.signatures, AppSigning.SHA1); } } } return sign; } - //获取已安装的app签名 - private static String getInstalledAPKSignature(Context context, String packageName) { + + + //需要读取应用列表权限 + public void getAppList(Context context) { PackageManager pm = context.getPackageManager(); -// String packageName="com.android.calendar"; - try { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { - PackageInfo appInfo = pm.getPackageInfo(packageName.trim(), PackageManager.GET_SIGNING_CERTIFICATES); - if (appInfo == null || appInfo.signingInfo == null) - return ""; -// return appInfo.signingInfo.getApkContentsSigners()[0].toCharsString(); - return AppSigning.getSignatureString(appInfo.signingInfo.getApkContentsSigners()[0],AppSigning.SHA1); - } else { - PackageInfo appInfo = pm.getPackageInfo(packageName.trim(), PackageManager.GET_SIGNATURES); - if (appInfo == null || appInfo.signatures == null) - return ""; -// return appInfo.signatures[0].toCharsString(); - return AppSigning.getSignatureString(appInfo.signatures[0],AppSigning.SHA1); - } - } catch (Exception e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - return ""; - } -//需要读取应用列表权限 - private void getAppList(Context context) { - PackageManager pm = context.getPackageManager(); - // Return a List of all packages that are installed on the device. List packages = pm.getInstalledPackages(0); for (PackageInfo packageInfo : packages) { // 判断系统/非系统应用 - if (( - packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) // 非系统应用 - { + if ((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {// 非系统应用 System.out.println("MainActivity.getAppList, packageInfo=" + packageInfo.packageName); } else { // 系统应用 } } } + static ArrayList list = new ArrayList<>(); + + /** + * 通过指令获取已安装的包 + * + * @return + */ private static ArrayList runCommand() { list.clear(); try { @@ -133,25 +160,91 @@ public class APISecurity { list.add(line.split(":")[1]); } } catch (IOException e) { - System.out.println("runCommand,e=" + e); + Log.e("runCommand", "e=" + e); } return list; } /** - * 检测动态调试 + * 检测动态调试检查应用是否处于调试状态 + * 这个也是借助系统的一个api来进行判断isDebuggerConnected() + * jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700,当连接成功之后,这个方法就会返回true */ - public void detectedDynamicDebug(){ - if (!BuildConfig.DEBUG){ - if (Debug.isDebuggerConnected()){ + public static void detectedDynamicDebug() { + if (!BuildConfig.DEBUG) { + if (Debug.isDebuggerConnected()) { //进程自杀 int myPid = android.os.Process.myPid(); android.os.Process.killProcess(myPid); - //异常退出虚拟机 System.exit(1); } } } + + /** + * 检查应用是否属于debug模式 + * 直接调用Android中的flag属性:ApplicationInfo.FLAG_DEBUGGABLE; + * 判断是否属于debug模式:防调试 + */ + public void checkDebug(Context context) { + int i = context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE; + if (0 != (context.getApplicationInfo().flags &= ApplicationInfo.FLAG_DEBUGGABLE)) { + /** + * + * 验证是否可以调试 + * i != 0 已经打开可调式 + */ + Log.e("debug", "被调试"); + + } + boolean debuggerConnected = Debug.isDebuggerConnected(); + Log.e("debug", "是否连接调试 : " + debuggerConnected); + /** + * + * 获取TracerPid来判断 + *获取获取TracerPid来判断(TracerPid正常情况是0,如果被调试这个是不为0的) + */ + int pid = android.os.Process.myPid(); + String info = null; + File file = new File("/proc/" + pid + "/status"); + try { + FileInputStream fileInputStream = new FileInputStream(file); + InputStreamReader reader = new InputStreamReader(fileInputStream); + BufferedReader bufferedReader = new BufferedReader(reader); + while ((info = bufferedReader.readLine()) != null) { + Log.e("debug", "proecc info : " + info); + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + + /*** + * 防代理 + */ + private boolean isProxy(Context context) { + String proxyAddress = ""; + int proxyPort = 0; + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + proxyAddress = System.getProperty("http.proxyHost"); + String proxyPortString = System.getProperty("http.proxyPort"); + proxyPort = Integer.parseInt((proxyPortString != null ? proxyPortString : "-1")); + } else { + proxyAddress = android.net.Proxy.getHost(context); + proxyPort = android.net.Proxy.getPort(context); + } + if (!TextUtils.isEmpty(proxyAddress) && proxyPort != -1) { + return true; + } + return false; + } +// 忽视代理 +// OkHttpClient okHttpClient = new OkHttpClient.Builder() +// .proxy(Proxy.NO_PROXY) +// .build(); } diff --git a/apisecurity/src/main/java/cn/android/security/AppSigning.java b/apisecurity/src/main/java/cn/android/security/AppSigning.java index f5ef7e4..0986050 100644 --- a/apisecurity/src/main/java/cn/android/security/AppSigning.java +++ b/apisecurity/src/main/java/cn/android/security/AppSigning.java @@ -1,24 +1,34 @@ package cn.android.security; -/** - * @author mahongyin - * @Project Android-API-Security-master - * @Package cn.android.sample - * @data 2020-04-11 22:20 - * @CopyRight mhy.work@qq.com - * @description: - */ - +import android.annotation.SuppressLint; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.Signature; import android.os.Build; +import android.util.DisplayMetrics; +import android.util.Log; +import androidx.annotation.StringDef; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Arrays; /** + * 签名验证 * * @author lWX537240 * @date 2019/6/13 @@ -33,67 +43,21 @@ public class AppSigning { public final static String SHA256 = "SHA256"; /** - * 返回一个签名的对应类型的字符串 - * - * @param context - * @param packageName - * @param type - * @return + * 注解限定String类型为指定 */ - public static String getSingInfo(Context context, String packageName, String type) { - String tmp = "error!"; - try { - Signature[] signs = getSignatures(context, packageName); -// Log.e(TAG, "signs = " + Arrays.asList(signs)); - Signature sig = signs[0]; - - if (MD5.equals(type)) { - tmp = getSignatureString(sig, MD5); - } else if (SHA1.equals(type)) { - tmp = getSignatureString(sig, SHA1); - } else if (SHA256.equals(type)) { - tmp = getSignatureString(sig, SHA256); - } - } catch (Exception e) { - e.printStackTrace(); - } - return tmp; + @StringDef({MD5, SHA1, SHA256}) + @Retention(RetentionPolicy.SOURCE) + @interface SigniType { } /** - * 返回对应包的签名信息 - * - * @param context - * @param packageName - * @return + * 获取相应的类型的签名信息(把签名的byte[]信息转换成16进制) */ - public static Signature[] getSignatures(Context context, String packageName){ - try { - - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { - PackageInfo appInfo = context.getPackageManager().getPackageInfo(packageName.trim(), PackageManager.GET_SIGNING_CERTIFICATES); - - return appInfo.signingInfo.getApkContentsSigners(); - } else { - PackageInfo appInfo = context.getPackageManager().getPackageInfo(packageName.trim(), PackageManager.GET_SIGNATURES); - - return appInfo.signatures; + public static String getSignatureString(Signature[] sigs, @SigniType String type) { + for (Signature sig : sigs) { + Log.e("mhyLog","getSignatureString:"+sig.toCharsString()); } - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - } - return null; - } - - /** - * 获取相应的类型的字符串(把签名的byte[]信息转换成16进制) - * - * @param sig - * @param type - * @return - */ - public static String getSignatureString(Signature sig, String type) { - byte[] hexBytes = sig.toByteArray(); + byte[] hexBytes = sigs[0].toByteArray(); String fingerprint = "error!"; try { StringBuffer buffer = new StringBuffer(); @@ -118,7 +82,219 @@ public class AppSigning { } + public static int getSignatureHash(Context context) { + PackageManager pm = context.getPackageManager(); + PackageInfo pi; + StringBuilder sb = new StringBuilder(); + int flags = PackageManager.GET_SIGNATURES; + try { + pi = pm.getPackageInfo(context.getPackageName(), flags); + Signature[] signatures = pi.signatures; + for (Signature signature : signatures) { + sb.append(signature.toCharsString()); + } + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + return sb.toString().hashCode(); + } + + //这个是获取SHA1的方法 上面那个方法是获取签名的hash值 这个和cmd里面获取的是一样的 + public static String getCertificateSHA1Fingerprint(Context context) { + //获取包管理器 + PackageManager pm = context.getPackageManager(); + //获取当前要获取SHA1值的包名,也可以用其他的包名,但需要注意, + //在用其他包名的前提是,此方法传递的参数Context应该是对应包的上下文。 + 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 (CertificateException e) { + e.printStackTrace(); + } + //X509证书,X.509是一种非常通用的证书格式 + X509Certificate c = null; + try { + c = (X509Certificate) cf.generateCertificate(input); + } catch (CertificateException e) { + e.printStackTrace(); + } + String hexString = null; + try { + //加密算法的类,这里的参数可以使MD4,MD5等加密算法 + MessageDigest md = MessageDigest.getInstance("SHA1"); + //获得公钥 + byte[] publicKey = md.digest(c.getEncoded()); + //字节到十六进制的格式转换 + hexString = byte2HexFormatted(publicKey); + } catch (NoSuchAlgorithmException e1) { + e1.printStackTrace(); + } catch (CertificateEncodingException e) { + e.printStackTrace(); + } + return hexString; + } + + //这里是将获取到得编码进行16进制转换 + private static String byte2HexFormatted(byte[] arr) { + StringBuilder str = new StringBuilder(arr.length * 2); + for (int i = 0; i < arr.length; i++) { + String h = Integer.toHexString(arr[i]); + int l = h.length(); + if (l == 1) + h = "0" + h; + if (l > 2) + h = h.substring(l - 2, l); + str.append(h.toUpperCase()); + if (i < (arr.length - 1)) + str.append(':'); + } + return str.toString(); + } + + /** 防破签名 3 使原生检测签名不被hook 助力防破签名1而存在 + * 通过重置PackageManager防止getPackageInfo方法被代理设置 + * 亲测MT管理器(当前2.9.1)的一键去签名校验(包括加强版)无效! + * 当然如果别人反编译把代码删除的话那就没办法了 + */ + public static void resetPackageManager(Context baseContext) { + try { + //重置全局sPackageManager对象 + reset1: + { + Class activityThreadClass = Class.forName("android.app.ActivityThread"); + Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager"); + sPackageManagerField.setAccessible(true); + sPackageManagerField.set(activityThreadClass, null); + //因为上面已经把sPackageManager变量设置为null了,调用这个方法重新赋值 + Method getPackageManagerMethod = activityThreadClass.getDeclaredMethod("getPackageManager"); + getPackageManagerMethod.setAccessible(true); + getPackageManagerMethod.invoke(activityThreadClass); + } + //重置当前上下文mPackageManager对象 + reset2: + { + Class baseContextClass = baseContext.getClass(); + Field mPackageManagerField = baseContextClass.getDeclaredField("mPackageManager"); + mPackageManagerField.setAccessible(true); + mPackageManagerField.set(baseContext, null); + //重新设置为已经重置好的sPackageManager + Method getPackageManagerMethod = baseContextClass.getDeclaredMethod("getPackageManager"); + getPackageManagerMethod.setAccessible(true); + getPackageManagerMethod.invoke(baseContext); + } + } catch (Throwable e) { + e.printStackTrace(); + } + } + /** + * 检测 PackageManager 代理 + */ + @SuppressLint("PrivateApi") + private boolean checkPMProxy(Context context){ + String truePMName = "android.content.pm.IPackageManager$Stub$Proxy"; + String nowPMName = ""; + try { + // 被代理的对象是 PackageManager.mPM + PackageManager packageManager = context.getPackageManager(); + Field mPMField = packageManager.getClass().getDeclaredField("mPM"); + mPMField.setAccessible(true); + Object mPM = mPMField.get(packageManager); + // 取得类名 + nowPMName = mPM.getClass().getName(); + } catch (Exception e) { + e.printStackTrace(); + } + // 类名改变说明被代理了 + return truePMName.equals(nowPMName); + } + + /** 防破签名 2 + * 安装路径获取签名 + */ + public static String getAPKSignatures(String apkPath) { + String PATH_PackageParser = "android.content.pm.PackageParser"; + try { + // apk包的文件路径 + // 这是一个Package 解释器, 是隐藏的 + // 构造函数的参数只有一个, apk文件的路径 + // PackageParser packageParser = new PackageParser(apkPath); + Class pkgParserCls = Class.forName(PATH_PackageParser); + Class[] typeArgs = new Class[1]; + typeArgs[0] = String.class; + // 这个是与显示有关的, 里面涉及到一些像素显示等等 + // 本来是用来根据屏幕大小解析对应资源,但是签名校验与屏幕大小无关, + // 所以setDefault来使用默认的情况 + DisplayMetrics metrics = new DisplayMetrics(); + metrics.setToDefaults(); + Constructor pkgParserCt = null; + Object pkgParser = null; + if (Build.VERSION.SDK_INT > 20) { + pkgParserCt = pkgParserCls.getConstructor(); + pkgParser = pkgParserCt.newInstance(); + Method pkgParser_parsePackageMtd = pkgParserCls.getDeclaredMethod("parsePackage", File.class, int.class); + Object pkgParserPkg = pkgParser_parsePackageMtd.invoke(pkgParser, new File(apkPath), PackageManager.GET_SIGNATURES); + + if (Build.VERSION.SDK_INT >= 28) { + Method pkgParser_collectCertificatesMtd = pkgParserCls.getDeclaredMethod("collectCertificates", pkgParserPkg.getClass(), Boolean.TYPE); + pkgParser_collectCertificatesMtd.invoke(pkgParser, pkgParserPkg, Build.VERSION.SDK_INT > 28); + +// Method pkgParser_collectCertificatesMtd = pkgParserCls.getDeclaredMethod("collectCertificates", pkgParserPkg.getClass(), Boolean.TYPE); +// pkgParser_collectCertificatesMtd.invoke(pkgParser, pkgParserPkg, false); + + Field mSigningDetailsField = pkgParserPkg.getClass().getDeclaredField("mSigningDetails"); // SigningDetails + mSigningDetailsField.setAccessible(true); + + Object mSigningDetails = mSigningDetailsField.get(pkgParserPkg); + Field infoField = mSigningDetails.getClass().getDeclaredField("signatures"); + infoField.setAccessible(true); + Signature[] info = (Signature[]) infoField.get(mSigningDetails); + return info[0].toCharsString(); + + } else { + Method pkgParser_collectCertificatesMtd = pkgParserCls.getDeclaredMethod("collectCertificates", pkgParserPkg.getClass(), Integer.TYPE); + pkgParser_collectCertificatesMtd.invoke(pkgParser, pkgParserPkg, PackageManager.GET_SIGNATURES); + + Field packageInfoFld = pkgParserPkg.getClass().getDeclaredField("mSignatures"); + Signature[] info = (Signature[]) packageInfoFld.get(pkgParserPkg); + return info[0].toCharsString(); + } + + + } else { + pkgParserCt = pkgParserCls.getConstructor(typeArgs); + pkgParser = pkgParserCt.newInstance(apkPath); + Method pkgParser_parsePackageMtd = pkgParserCls.getDeclaredMethod("parsePackage", File.class, String.class, DisplayMetrics.class, Integer.TYPE); + Object pkgParserPkg = pkgParser_parsePackageMtd.invoke(pkgParser, new File(apkPath), apkPath, metrics, PackageManager.GET_SIGNATURES); + Method pkgParser_collectCertificatesMtd = pkgParserCls.getDeclaredMethod("collectCertificates", pkgParserPkg.getClass(), Integer.TYPE); + pkgParser_collectCertificatesMtd.invoke(pkgParser, pkgParserPkg, PackageManager.GET_SIGNATURES); + // 应用程序信息包, 这个公开的, 不过有些函数, 变量没公开 + Field packageInfoFld = pkgParserPkg.getClass().getDeclaredField("mSignatures"); + Signature[] info = (Signature[]) packageInfoFld.get(pkgParserPkg); + return info[0].toCharsString(); + } + } catch (Exception e) { + Log.e("getAPKSignatures", e.getMessage()); + e.printStackTrace(); + } + return null; + } } diff --git a/app/build.gradle b/app/build.gradle index a548337..0c0be86 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,7 +10,7 @@ android { versionCode 1 versionName "1.0" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' // ndk{ // //生成so库名称 也是System.load // moduleName "apisecurity-lib" @@ -19,8 +19,10 @@ android { externalNativeBuild { cmake { cppFlags "" +// abiFilters 'x86', 'x86_64', 'armeabi', 'armeabi-v7a', 'arm64-v8a' } } + } signingConfigs { @@ -30,15 +32,18 @@ android { storeFile file("test.keystore") storePassword '123456' } + } buildTypes { release { + debuggable false//xml android:debuggable="false" minifyEnabled false signingConfig signingConfigs.release proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } debug{ + debuggable true signingConfig signingConfigs.release } } @@ -54,10 +59,7 @@ android { dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'com.android.support:appcompat-v7:28.0.0' - implementation 'com.android.support.constraint:constraint-layout:1.1.3' - testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support.test:runner:1.0.2' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + implementation 'androidx.appcompat:appcompat:1.0.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation project(':apisecurity') } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7a9ffa7..a8ecbc7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,6 +11,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme" + tools:ignore="GoogleAppIndexingWarning"> diff --git a/app/src/main/cpp/apisecurity-lib.cpp b/app/src/main/cpp/apisecurity-lib.cpp index 51cf881..160d482 100644 --- a/app/src/main/cpp/apisecurity-lib.cpp +++ b/app/src/main/cpp/apisecurity-lib.cpp @@ -1,6 +1,9 @@ #include #include #include +#include +#include +#include //log定义 #define LOG "APISECURITY" // 这个是自定义的LOG的TAG @@ -11,32 +14,32 @@ #define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,LOG,__VA_ARGS__) //此处改为你的APP签名 -//#define SHA1 "a8e3d91a4f77dd7ccb8d43ee5046a4b6833f4785"//区分小写 -#define SHA1 "04c1411b0662acd9e4aa300559677e5f106a5255" +//#define SHA1 "a8e3d91a4f77dd7ccb8d43ee5046a4b6833f4785"//真实test.keystore +#define SHA1 "04c1411b0662acd9e4aa300559677e5f106a5255"//区分da小写 #define ALGORITHM_SHA1 "SHA1" #define ALGORITHM_MD5 "MD5" //此处改为你的APP包名 #define APP_PKG "cn.android.sample" //此处填写API盐值 -#define API_SECRET "ABC1234567"//设置api 密钥 返回md5 +#define API_SECRET "ABC1234567"//设置api 密钥 MD5加盐 static bool isInit = false; static char *secret; -//void printByte(JNIEnv *env, jbyteArray jbytes) { -// //转换成char -// jsize array_size = env->GetArrayLength(jbytes); -// jbyte *sha1 = env->GetByteArrayElements(jbytes, NULL); -// -// char *hexA = new char[array_size * 2 + 1](); -// for (int i = 0; i < array_size; ++i) { -// sprintf(hexA + 2 * i, "%02x", (u_char) sha1[i]); -// } -// LOGD("printByte:%s", hexA); -//} +void printByte(JNIEnv *env, jbyteArray jbytes) { + //转换成char + jsize array_size = env->GetArrayLength(jbytes); + jbyte *sha1 = env->GetByteArrayElements(jbytes, nullptr); + + char *hexA = new char[array_size * 2 + 1](); + for (int i = 0; i < array_size; ++i) { + sprintf(hexA + 2 * i, "%02x", (u_char) sha1[i]); + } + LOGD("printByte:%s", hexA); +} char *digest(JNIEnv *env, const char *algorithm, jbyteArray cert_byte) { jclass message_digest_class = env->FindClass("java/security/MessageDigest"); @@ -51,7 +54,7 @@ char *digest(JNIEnv *env, const char *algorithm, jbyteArray cert_byte) { //转换成char jsize array_size = env->GetArrayLength(sha1_byte); - jbyte *sha1 = env->GetByteArrayElements(sha1_byte, NULL); + jbyte *sha1 = env->GetByteArrayElements(sha1_byte, nullptr); char *hex = new char[array_size * 2 + 1](); for (int i = 0; i < array_size; ++i) { sprintf(hex + 2 * i, "%02x", (unsigned char) sha1[i]); @@ -81,37 +84,71 @@ jstring getPackageName(JNIEnv *env, jclass context_class, jobject context_object } /** - * 获取PackageInfo对象 + * 获取PackageInfo对象 PackageManager.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES) */ jobject getPackageInfo(JNIEnv *env, jobject package_manager, jstring package_name) { jclass pack_manager_class = env->GetObjectClass(package_manager); jmethodID methodId = env->GetMethodID(pack_manager_class, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;"); env->DeleteLocalRef(pack_manager_class); - jobject package_info = env->CallObjectMethod(package_manager, methodId, package_name, 0x40);//安卓10 0x80 + + jobject package_info = env->CallObjectMethod(package_manager, methodId, package_name, + 0x40);//安卓10 0x80 return package_info; } -//目录获取PackageInfo -jobject getPackageArchiveInfo(JNIEnv *env, jobject package_manager, jstring absolutePath) { + +/** + * PackageInfo pkgInfo = pm.getPackageArchiveInfo(apkFile.getAbsolutePath(), PackageManager.GET_SIGNATURES); + * */ +jobject getPackageInfoArchive(JNIEnv *env, jobject package_manager, jstring apkPath) { jclass pack_manager_class = env->GetObjectClass(package_manager); jmethodID methodId = env->GetMethodID(pack_manager_class, "getPackageArchiveInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;"); env->DeleteLocalRef(pack_manager_class); - jobject package_info = env->CallObjectMethod(package_manager, methodId, absolutePath, 0x80); + jobject package_info = env->CallObjectMethod(package_manager, methodId, apkPath, 0x40); return package_info; } -//安装路径 -jstring getAbsolutePath(JNIEnv *env, jobject package_info) {//packageInfo - jclass package_info_class = env->GetObjectClass(package_info);//applicationInfo获取类对象 以获取方法 - //pack_manager_class.getApplicationInfo() - jfieldID field = env->GetFieldID(package_info_class,"applicationInfo","Landroid/content/pm/ApplicationInfo;");//获取此类中的sourceDir成员id - jobject apppack_info_object=(jstring)env ->GetObjectField(package_info,field);//applicationInfo +/**安装目录获取ApplicationInfo + * ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(packname, 0); + * path = applicationInfo.sourceDir; + * */ +jobject getApplicationInfo(JNIEnv *env, jobject package_manager, jstring package_name) { + jclass pack_manager_class = env->GetObjectClass(package_manager); + jmethodID methodId = env->GetMethodID(pack_manager_class, "getApplicationInfo", + "(Ljava/lang/String;I)Landroid/content/pm/ApplicationInfo;"); + env->DeleteLocalRef(pack_manager_class); + jobject applicationInfo = env->CallObjectMethod(package_manager, methodId, package_name, 0x00); + return applicationInfo; +} + +jstring getApkPath(JNIEnv *env, jobject applicationInfo_object) { + jclass applicationInfo_class = env->GetObjectClass(applicationInfo_object);//ApplicationInfo + jfieldID sourceDir = env->GetFieldID(applicationInfo_class, "sourceDir", + "Ljava/lang/String;");//获取此类中的sourceDir成员id P->publicSourceDir + env->DeleteLocalRef(applicationInfo_class); + jstring apkPath = (jstring) env->GetObjectField(applicationInfo_object, sourceDir); + return apkPath; +} +//context.getPackageResourcePath() +jstring getApkResPath(JNIEnv *env, jclass context_class, jobject context_object) { + jmethodID methodId = env->GetMethodID(context_class, "getPackageResourcePath", "()Ljava/lang/String;"); + jstring apkPath = (jstring) env->CallObjectMethod(context_object, methodId); + return apkPath; +} +//弃用 安装路径 package_info= pm.getPackageInfo(packname, 0x80).applicationInfo +jstring getAbsolutePath(JNIEnv *env, jobject package_info) { + jclass package_info_class = env->GetObjectClass(package_info);//packageInfo + jfieldID field = env->GetFieldID(package_info_class, "applicationInfo", + "Landroid/content/pm/ApplicationInfo;");//applicationInfo获取类对象 以获取方法 + env->DeleteLocalRef(package_info_class); + jobject apppack_info_object = (jstring) env->GetObjectField(package_info,field);//applicationInfo jclass apppack_info_class = env->GetObjectClass(apppack_info_object);//ApplicationInfo - jfieldID appfield = env->GetFieldID(apppack_info_class,"sourceDir","Ljava/lang/String;");//获取此类中的sourceDir成员id P->publicSourceDir -// env->DeleteLocalRef(pack_manager_class); - jstring absoluteapp=(jstring)env ->GetObjectField(apppack_info_object,appfield); - if (absoluteapp == NULL) + jfieldID appfield = env->GetFieldID(apppack_info_class, "sourceDir", + "Ljava/lang/String;");//获取此类中的sourceDir成员id P->publicSourceDir + env->DeleteLocalRef(apppack_info_class); + jstring absoluteapp = (jstring) env->GetObjectField(apppack_info_object, appfield); + if (absoluteapp == nullptr) return env->NewStringUTF(""); return absoluteapp; } @@ -125,8 +162,8 @@ jobject getSignature(JNIEnv *env, jobject package_info) { "[Landroid/content/pm/Signature;"); env->DeleteLocalRef(package_info_class); jobjectArray signature_object_array = (jobjectArray) env->GetObjectField(package_info, fieldId); - if (signature_object_array == NULL) - return NULL; + if (signature_object_array == nullptr) + return nullptr; return env->GetObjectArrayElement(signature_object_array, 0); } @@ -156,7 +193,55 @@ jbyteArray getSHA1(JNIEnv *env, jobject signature_object) { return cert_byte; } +/** + * apk安装路径 获取签名信息 getApkPathSignatures(Context) + */ +jboolean getApkPathSignatures(JNIEnv *env, jobject context_object) { + //上下文对象 + jclass context_class = env->GetObjectClass(context_object); + //反射获取PackageManager + jobject package_manager = getPackageManager(env, context_object, context_class); + if (package_manager == nullptr) + return JNI_FALSE; + //反射获取包名 + jstring package_name = getPackageName(env, context_class, context_object); + if (package_name == nullptr) + return JNI_FALSE; +//插空 替换下面 214-218 + jstring apkPath = getApkResPath(env, context_class, context_object); + env->DeleteLocalRef(context_class);//等用完再释放啦 +// //获取applictionInfo +// jobject applicationInfo = getApplicationInfo(env, package_manager, package_name); +// if (applicationInfo == nullptr) +// return JNI_FALSE; +// jstring apkPath = getApkPath(env, applicationInfo); + if (apkPath == nullptr) + return JNI_FALSE; + //获取PackageInfo对象 + jobject package_info = getPackageInfoArchive(env, package_manager, apkPath); + if (package_info == nullptr) + return JNI_FALSE; + env->DeleteLocalRef(package_manager); + //获取签名信息 + jobject signature_object = getSignature(env, package_info); + if (signature_object == nullptr) + return JNI_FALSE; + env->DeleteLocalRef(package_info); + jbyteArray cert_byte = getSHA1(env, signature_object); + + char *hex_sha = digest(env, ALGORITHM_SHA1, cert_byte); + + if (strcmp(hex_sha, SHA1) != 0) {//签名不同 + LOGE("非法调用4,SHA1: %s", hex_sha); + return JNI_FALSE; + } + return JNI_TRUE; +} + +/** + * 调用 + */ extern "C" JNIEXPORT jboolean JNICALL Java_cn_android_security_APISecurity_init( JNIEnv *env, @@ -168,24 +253,24 @@ Java_cn_android_security_APISecurity_init( //反射获取PackageManager jobject package_manager = getPackageManager(env, context_object, context_class); - if (package_manager == NULL) + if (package_manager == nullptr) return JNI_FALSE; //反射获取包名 jstring package_name = getPackageName(env, context_class, context_object); - if (package_name == NULL) + if (package_name == nullptr) return JNI_FALSE; env->DeleteLocalRef(context_class); //获取PackageInfo对象 jobject package_info = getPackageInfo(env, package_manager, package_name); - if (package_info == NULL) + if (package_info == nullptr) return JNI_FALSE; env->DeleteLocalRef(package_manager); //获取签名信息 jobject signature_object = getSignature(env, package_info); - if (signature_object == NULL) + if (signature_object == nullptr) return JNI_FALSE; env->DeleteLocalRef(package_info); jbyteArray cert_byte = getSHA1(env, signature_object); @@ -193,50 +278,54 @@ Java_cn_android_security_APISecurity_init( char *hex_sha = digest(env, ALGORITHM_SHA1, cert_byte); if (strcmp(hex_sha, SHA1) != 0) {//签名不对 - LOGE("非法调用1,SHA1: %s:%s", hex_sha, SHA1); + LOGE("非法调用1,SHA1: %s", hex_sha); return JNI_FALSE; } - - const char *pkgName = env->GetStringUTFChars(package_name, NULL); - +//包名验证 + const char *pkgName = env->GetStringUTFChars(package_name, nullptr); if (strcmp(pkgName, APP_PKG) == 0) { secret = API_SECRET;//包名匹配 拿取api } else { - LOGE("非法调用2,Package: %s:%s", pkgName, APP_PKG); + LOGE("非法调用2,Package: %s", pkgName); return JNI_FALSE; } -// 接着调用Java方法验证 安装目录文件签名 - jclass cls_util = env->FindClass( - "cn/android/security/APISecurity");//注意,这里的使用的斜杠而不是点 - if (cls_util == NULL) { +/*********接着调用Java方法验证 安装目录apk文件de签名**/ +// jclass cls_util = env->FindClass( +// "cn/android/security/APISecurity"); +// //注意,这里的使用的斜杠而不是点 +// if (cls_util == nullptr) { +// return JNI_FALSE; +// } +// jobject j_obj = env->AllocObject(cls_util); +// //**这里是关键**类,方法,(参数类型)返回类型 +// jmethodID mtd_static_method = env->GetStaticMethodID(cls_util, +// "getApkSignatures", +// "(Landroid/content/Context;Ljava/lang/String;)Ljava/lang/String;"); +// if (mtd_static_method == nullptr) { +// return JNI_FALSE; +// } +// //调用Java方法 +// jstring sigin = static_cast(env->CallStaticObjectMethod(cls_util, mtd_static_method, +// context_object, package_name)); +// const char *ss = env->GetStringUTFChars(sigin, nullptr); +// //删除引用 +// env->DeleteLocalRef(cls_util); +// env->DeleteLocalRef(j_obj); +////调用Java方法结束 +// if (strcmp(ss, SHA1) != 0) { +// LOGE("非法调用3,SHA1: %s", ss); +// return JNI_FALSE; +// } +/*******************调用Java方法结束***/ + //加强验证 + if (!getApkPathSignatures(env, context_object)) { return JNI_FALSE; } - jobject j_obj = env->AllocObject(cls_util);//**这里是关键**类,方法,(参数类型)返回类型 - jmethodID mtd_static_method = env->GetStaticMethodID(cls_util, - "getApkSignatures", - "(Landroid/content/Context;Ljava/lang/String;)Ljava/lang/String;"); - if (mtd_static_method == NULL) { - return JNI_FALSE; - } - //调用Java方法 - jstring sigin= static_cast(env->CallStaticObjectMethod(cls_util, mtd_static_method, - context_object, package_name)); - const char *ss=env->GetStringUTFChars(sigin,NULL); - //删除引用 - env->DeleteLocalRef(cls_util); - env->DeleteLocalRef(j_obj); -//调用Java方法结束 - if (strcmp(ss, SHA1) != 0) { - LOGE("非法调用3,SHA1: %s:%s", ss, SHA1); - return JNI_FALSE; - } - // isInit = true; LOGI("初始化成功!"); return JNI_TRUE; } - extern "C" JNIEXPORT jstring JNICALL Java_cn_android_security_APISecurity_sign( JNIEnv *env, @@ -246,34 +335,37 @@ Java_cn_android_security_APISecurity_sign( if (!isInit) { LOGE("请先初始化!"); - jclass cls_util = env->FindClass( - "cn/android/security/APISecurity");//注意,这里的使用的斜杠而不是点 - if (cls_util == NULL) { - return env->NewStringUTF(""); - } - jobject j_obj = env->AllocObject(cls_util);//**这里是关键** - jmethodID mtd_static_method = env->GetMethodID(cls_util, - "javaMethod", - "(Ljava/lang/String;)V"); - if (mtd_static_method == NULL) { - return env->NewStringUTF(""); - } + jclass cls_util = env->FindClass( + "cn/android/security/APISecurity");//注意,这里的使用的斜杠而不是点 + if (cls_util == nullptr) { + return env->NewStringUTF(""); + } + //调用Java 方法 + jobject j_obj = env->AllocObject(cls_util); + //**这里是关键**GetMethodID是普通方法 GetStaticMethodID静态方法 + jmethodID mtd_static_method = env->GetMethodID(cls_util, + "javaMethod", + "(Ljava/lang/String;)V"); + if (mtd_static_method == nullptr) { + return env->NewStringUTF(""); + } - jstring data = env->NewStringUTF("noinit"); + jstring data = env->NewStringUTF("验证失败"); - if (data == NULL) {}//调用Java方法 - env->CallVoidMethod(/*context_object*/j_obj, mtd_static_method, data); + if (data != nullptr) {//nullptr + //调用Java方法 + env->CallVoidMethod(j_obj, mtd_static_method, data); //删除引用 env->DeleteLocalRef(cls_util); env->DeleteLocalRef(j_obj); env->DeleteLocalRef(data); - + } return env->NewStringUTF(""); } //已经通过初始化 const char *sx; - sx = env->GetStringUTFChars(str, NULL); + sx = env->GetStringUTFChars(str, nullptr); //通过传来计算 char *full = new char[strlen(sx) + strlen(secret) + 1](); strcat(full, sx); @@ -293,22 +385,80 @@ Java_cn_android_security_APISecurity_sign( //JNIEXPORT jboolean JNICALL //Java_cn_android_security_APISecurity_check(JNIEnv *env, jclass clazz, jstring str) { // const char *sx; -// sx = env->GetStringUTFChars(str, NULL); +// sx = env->GetStringUTFChars(str, nullptr); // char name[512]; // strcpy(name, sx); // // return 0 == strcmp(SHA1, "md5"); //} + //APISecurity.adbshell("pm list package -3",getFilesDir().getPath() + File.separator + "files"+File.separator + "adbshell.txt"); -//extern "C" -//JNIEXPORT jstring JNICALL -//Java_cn_android_security_APISecurity_adbshell(JNIEnv *env, jclass clazz, jstring str,jstring path) { -// // int ret= system("pm list package -3");//获取安装应用 -// -// char* str2=(char *)env->GetStringUTFChars(str, NULL); -// char* path2=(char *)env->GetStringUTFChars(path, NULL); -// strcat(str2," > "); -// strcat(str2,path2); +//extern "C" JNIEXPORT jstring JNICALL +//Java_cn_android_security_APISecurity_adbshell(JNIEnv *env, jclass clazz, jstring str, +// jstring path) { +// int ret = system("pm list package -3");//获取安装应用 +// char *str2 = (char *) env->GetStringUTFChars(str, nullptr); +// char *path2 = (char *) env->GetStringUTFChars(path, nullptr); +// strcat(str2, " > "); +// strcat(str2, path2); // system(str2); // return env->NewStringUTF(str2); //} + +/*1.调试端口检测 +读取/proc/net/tcp,查找IDA远程调试所用的23946端口,若发现说明进程正在被IDA调试。*/ +void CheckPort23946ByTcp() { + FILE *pfile = nullptr; + char buf[0x1000] = {0}; +// 执行命令 + char *strCatTcp = "cat /proc/net/tcp |grep :5D8A";//5D8A转化成十进制就是23946 +//char* strNetstat="netstat |grep :23946"; + pfile = popen(strCatTcp, "r"); + if (nullptr == pfile) { + LOGD("CheckPort23946ByTcp popen打开命令失败!\n"); + return; + } +// 获取结果 + while (fgets(buf, sizeof(buf), pfile)) { +// 执行到这里,判定为调试状态 + LOGD("执行cat /proc/net/tcp |grep :5D8A的结果:\n"); + LOGD("%s", buf); + }//while + pclose(pfile); +} + + +/*5.APK线程检测 +正常apk进程一般会有十几个线程在运行(比如会有jdwp线程), +自己写可执行文件加载so一般只有一个线程, +可以根据这个差异来进行调试环境检测*/ + +void CheckTaskCount() { + char buf[0x100] = {0}; + char *str = "/proc/%d/task"; + snprintf(buf, sizeof(buf), str, getpid()); +// 打开目录: + DIR *pdir = opendir(buf); + if (!pdir) { + perror("CheckTaskCount open() fail.\n"); + return; + } +// 查看目录下文件个数: + struct dirent *pde = nullptr; + int Count = 0; + while ((pde = readdir(pdir))) { +// 字符过滤 + if ((pde->d_name[0] <= '9') && (pde->d_name[0] >= '0')) { + ++Count; + LOGD("%d 线程名称:%s\n", Count, pde->d_name); + } + } + LOGD("线程个数为:%d", Count); + if (1 >= Count) { +// 此处判定为调试状态. + LOGD("调试状态!\n"); + } + int i = 0; + return; +} + diff --git a/app/src/main/java/cn/android/sample/HookApplication.java b/app/src/main/java/cn/android/sample/HookApplication.java index d13ca5c..e35f9a0 100644 --- a/app/src/main/java/cn/android/sample/HookApplication.java +++ b/app/src/main/java/cn/android/sample/HookApplication.java @@ -23,6 +23,8 @@ import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; + +import cn.android.security.AppSigning; //继承原application public class HookApplication extends Application implements InvocationHandler { @@ -35,12 +37,15 @@ public class HookApplication extends Application implements InvocationHandler { @Override public void onCreate() { super.onCreate(); - app=this; + app = this; + //在签名校验被hook 之后重置PackageManager + /*在这里 重置PackageManager 只要在验证前重置即可*/ + AppSigning.resetPackageManager(getBaseContext()); } public static Context getContext() { - if (app==null){ - app=new HookApplication(); + if (app == null) { + app = new HookApplication(); app.onCreate(); } return app; @@ -49,16 +54,22 @@ public class HookApplication extends Application implements InvocationHandler { @Override protected void attachBaseContext(Context context) { + //在这里hook 签名校验被 hook(context); super.attachBaseContext(context); } + /** * 全局hook 签名校验 + * * @param context */ private void hook(Context context) { try { - DataInputStream dataInputStream = new DataInputStream(new ByteArrayInputStream(Base64.decode("AQAAAjAwggIsMIIBlaADAgECAgMY2gowDQYJKoZIhvcNAQEFBQAwWzELMAkGA1UEBhMCQ04xCzAJ\nBgNVBAgTAmhlMQwwCgYDVQQHEwNzanoxDDAKBgNVBAoTA2VkdTEPMA0GA1UECxMGc2Nob29sMRIw\nEAYDVQQDEwltYWhvbmd5aW4wHhcNMTgxMDExMDcwMjE1WhcNNDMxMDA1MDcwMjE1WjBbMQswCQYD\nVQQGEwJDTjELMAkGA1UECBMCaGUxDDAKBgNVBAcTA3NqejEMMAoGA1UEChMDZWR1MQ8wDQYDVQQL\nEwZzY2hvb2wxEjAQBgNVBAMTCW1haG9uZ3lpbjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA\nr0aFNvrxBnBEEbAANDcsrmBlcQBGJKsvT5onXngek2ZbkWZx8/1o8nbgCBSjAZvnXEYYjjkC5k+A\nIne1PJUF5bPKTjIQepNmtK+KVHsAJLjn6rG4fQ3oaeu0vvNBehuzt54bACbzkXZj9nV5rs8OllD9\nRronLsOb3DVJ95DyLIMCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCpF6kB++zR0FW4eZaJCEAnQNP0\nGtwAnrXEpvP7ePcakk/JT/e56uTS/OAbpmM/tWETvPtx9hOB4RoPwRl3Q0G1ieCMeVyIABmGAeOk\ntARqtiExfHvorrmk4mxVIiPTwUJSWzAKuhLV93pMxTFZSZK0iTJFVVM/l8Wh3CTdFtpW+w==\n", 0))); + String relay="308203273082020fa003020102020477d6d1f6300d06092a864886f70d01010b05003044310c300a06035504061303303231310e300c060355040813056368696e613111300f060355040713087368616e676861693111300f0603550403130877757a6f6e67626f301e170d3139303330353032343132345a170d3434303232373032343132345a3044310c300a06035504061303303231310e300c060355040813056368696e613111300f060355040713087368616e676861693111300f0603550403130877757a6f6e67626f30820122300d06092a864886f70d01010105000382010f003082010a0282010100a3ac52268a32e8420a20a727c184c133d513998a207e198f5a535d628a436ba5e095e7ba3f92535234a83fb6272e70ed6113d8f6facc3dee2cfc076a3bd93dad3520fd5d9d9ae4c48afe56e7b421f5de2adfbc23e450f7a5f71e0afdec047b1ce8d7be62ef754a9d43bf36d9b9e0728fc268cb845b464cce1370573dfafd6c40b2efb98ba1f20c5a63c417264b69d86adb839241dc37d1a7113295a9c51623e51e9408f9623ed49a63a3ba6269172872088213332f38370af530d5be56e54115b0884ace6813911bfc6873bea28207741f4b2471b797bab156e4c6ead91659076553cee1db82c0cebdd17b64802a20c7ee6a3414f959133e6c435efe9241ab7d0203010001a321301f301d0603551d0e041604143806aea351c74f2a8b83fa26c0a9e3d3820b6699300d06092a864886f70d01010b050003820101006ebcc664b996f15c1e03d041eebbdf74a0976d117d68f34d21ef67855b614f5a2bfede66c9d4ea78fe3b50e3673890dfa2eb9eaf4321b30eb76be6f5944004b6501b2629ae4f2c6750f784ea2f9be6c26318258f98772fd3ff0c6ea817fb76d9ae02daa1fa1b91653d531db345f52aa4e7b21e8f92387a2d15d1afd5556213b0c32aadd529bac330516536948bcf85398fb86a65dbae95ef0e5582a87e26b1dbcceeaf77e6e93c63042acdf49c74927561df508020547426ad37776e360feb219523ef4e2a6f5f41a43cd0c0514c53f8644c71014080cfbe036f120a6daad6e12d6b1a07939ca840af2b3373388c0ed6b18594dd838122174304d5eb720f1cef"; + String singnStr = "AQAAAjAwggIsMIIBlaADAgECAgMY2gowDQYJKoZIhvcNAQEFBQAwWzELMAkGA1UEBhMCQ04xCzAJ\nBgNVBAgTAmhlMQwwCgYDVQQHEwNzanoxDDAKBgNVBAoTA2VkdTEPMA0GA1UECxMGc2Nob29sMRIw\nEAYDVQQDEwltYWhvbmd5aW4wHhcNMTgxMDExMDcwMjE1WhcNNDMxMDA1MDcwMjE1WjBbMQswCQYD\nVQQGEwJDTjELMAkGA1UECBMCaGUxDDAKBgNVBAcTA3NqejEMMAoGA1UEChMDZWR1MQ8wDQYDVQQL\nEwZzY2hvb2wxEjAQBgNVBAMTCW1haG9uZ3lpbjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA\nr0aFNvrxBnBEEbAANDcsrmBlcQBGJKsvT5onXngek2ZbkWZx8/1o8nbgCBSjAZvnXEYYjjkC5k+A\nIne1PJUF5bPKTjIQepNmtK+KVHsAJLjn6rG4fQ3oaeu0vvNBehuzt54bACbzkXZj9nV5rs8OllD9\nRronLsOb3DVJ95DyLIMCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCpF6kB++zR0FW4eZaJCEAnQNP0\nGtwAnrXEpvP7ePcakk/JT/e56uTS/OAbpmM/tWETvPtx9hOB4RoPwRl3Q0G1ieCMeVyIABmGAeOk\ntARqtiExfHvorrmk4mxVIiPTwUJSWzAKuhLV93pMxTFZSZK0iTJFVVM/l8Wh3CTdFtpW+w==\n"; + DataInputStream dataInputStream = new DataInputStream( + new ByteArrayInputStream(Base64.decode(singnStr, 0))); byte[][] bArr = new byte[(dataInputStream.read() & 255)][]; for (int i = 0; i < bArr.length; i++) { bArr[i] = new byte[dataInputStream.readInt()]; @@ -89,7 +100,7 @@ public class HookApplication extends Application implements InvocationHandler { @Override public Object invoke(Object obj, Method method, Object[] objArr) throws Throwable { - if ("getPackageInfo".equals(method.getName())) { + if ("getPackageInfo".equals(method.getName())) {//方法名对上 String str = (String) objArr[0]; if ((((Integer) objArr[1]).intValue() & 64) != 0 && this.appPkgName.equals(str)) { PackageInfo packageInfo = (PackageInfo) method.invoke(this.base, objArr); diff --git a/app/src/main/java/cn/android/sample/MainActivity.java b/app/src/main/java/cn/android/sample/MainActivity.java index 037e890..77681a5 100644 --- a/app/src/main/java/cn/android/sample/MainActivity.java +++ b/app/src/main/java/cn/android/sample/MainActivity.java @@ -2,7 +2,7 @@ package cn.android.sample; import android.annotation.SuppressLint; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatActivity; import android.view.View; import android.widget.TextView; @@ -23,8 +23,10 @@ public class MainActivity extends AppCompatActivity { if(APISecurity.init(this)){ tv.setText("初始化ok"); + }else { + tv.setText("初始化fail"); } - + APISecurity.verify(this); findViewById(R.id.btnTest).setOnClickListener(new View.OnClickListener() { @SuppressLint("SetTextI18n") @Override @@ -39,4 +41,9 @@ public class MainActivity extends AppCompatActivity { } + @Override + protected void onResume() { + super.onResume(); + APISecurity.detectedDynamicDebug(); + } } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 410124b..57b9d52 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,10 +1,10 @@ - + > - \ No newline at end of file + \ No newline at end of file diff --git a/build.gradle b/build.gradle index d81e151..46bf6ae 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.6.2' + classpath 'com.android.tools.build:gradle:3.6.3' // NOTE: Do not place your application dependencies here; they belong diff --git a/gradle.properties b/gradle.properties index 388e46d..d6d4dc6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,3 +15,5 @@ android.useDeprecatedNdk=true ANDROID_BUILD_MIN_SDK_VERSION=16 ANDROID_BUILD_TARGET_SDK_VERSION=28 ANDROID_BUILD_SDK_VERSION=28 +android.useAndroidX=true +android.enableJetifier=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a196440..d651a9f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -#Tue Mar 05 11:25:17 CST 2019 +#Sat Aug 01 16:28:14 CST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/防止被一键去除签名校验.zip b/防止被一键去除签名校验.zip new file mode 100644 index 0000000000000000000000000000000000000000..2603b0a9a0272077c25e91621ffbe636ee049c6b GIT binary patch literal 18871 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 literal 0 HcmV?d00001