# java 层签名检测
java 层通常使用 PackageManager 进行签名校验
在 Android9 以上,签名可以通过 PackageInfo -> signingInfo -> signatures 获得,以下则是通过 PackageInfo -> signatures
无论是哪种,获取 PackageInfo 都会使用 getPackageInfo 这个 api
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 package com.example.mysecurityapp;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.os.Message;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;public class SignatureVerificationUtils { public static final String expected_signature_hash = "74:0F:46:6F:D4:7E:F5:2A:38:10:CE:AB:92:F0:46:45:B9:4D:94:65:DF:F2:DC:D6:99:51:B5:86:7A:F6:10:E1" ; public static String getAppSignatureHash (Context context, String packageName) { try { PackageManager pm = context.getPackageManager(); Signature[] signatures; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES); if (packageInfo == null || packageInfo.signingInfo == null ) { return null ; } if (packageInfo.signingInfo.hasMultipleSigners()) { signatures = packageInfo.signingInfo.getApkContentsSigners(); } else { signatures = packageInfo.signingInfo.getSigningCertificateHistory(); } } else { PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES); if (packageInfo == null || packageInfo.signatures == null ) { return null ; } signatures = packageInfo.signatures; } if (signatures != null && signatures.length > 0 ) { return getSHA256(signatures[0 ].toByteArray()); } } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } return null ; } private static String getSHA256 (byte [] signatureBytes) { try { MessageDigest md = MessageDigest.getInstance("SHA-256" ); md.update(signatureBytes); byte [] digest = md.digest(); return bytesToHex(digest); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return null ; } private static String bytesToHex (byte [] bytes) { StringBuilder hexString = new StringBuilder (); for (int i = 0 ; i < bytes.length; i++) { String hex = Integer.toHexString(0xFF & bytes[i]); if (hex.length() == 1 ) { hexString.append('0' ); } hexString.append(hex.toUpperCase()); if (i < bytes.length - 1 ) { hexString.append(":" ); } } return hexString.toString(); } public static boolean verifySignature (Context context) { String signature = getAppSignatureHash(context, context.getPackageName()); return expected_signature_hash.equals(signature); } }
在 Android9 引入了密钥轮换机制,因此废弃了 GET_SIGNATURES ,转而推荐使用 GET_SIGNING_CERTIFICATES 获取证书
但是单纯 java 层检测并不安全:PackageManager 的 getPackageInfo 可能会被 hook、用来比对的签名值也可能被篡改
# 伪造 PackageInfo
在 MainActivity 调用这个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button button = findViewById(R.id.signature_java); button.setOnClickListener(this ); textView = findViewById(R.id.signature_text); } @Override public void onClick (View v) { boolean verify_res = SignatureVerificationUtils.verifySignature(this ); if (verify_res == false ) { textView.setText("签名校验失败" ); } else { textView.setText("签名校验成功" ); } }
并对应用重新签名
此时签名校验失败,尝试通过 hook PackageManger 返回正确校验值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 function fakeSignatures ( ){ var signature = Java .use ("android.content.pm.Signature" ); var mySignative = "308202e4308201cc020101300d06092a864886f70d01010b050030373116301406035504030c0d416e64726f69642044656275673110300e060355040a0c07416e64726f6964310b30090603550406130255533020170d3234313131363134343832385a180f32303534313130393134343832385a30373116301406035504030c0d416e64726f69642044656275673110300e060355040a0c07416e64726f6964310b300906035504061302555330820122300d06092a864886f70d01010105000382010f003082010a02820101009042f3b7fbe3164cac331f89ca5eefa713b17a3bc7385f7c740646feb823099717b5c83f4d466415e09cdc0aa1f195419b8b485c69c5da7c2a77b4252527188df70e99f842729e7e6aa6ed3b93e021035c740d53b2f8e65157b6994e4d054091b984c48a0b5161e63e7104e858e995117c884dfb8337e129b8b65af7ddbbca501261cd043c9b6105956b569f50d58c2b2fba2c81c30fbb022c30043f9ca13230d437b5d304cc6e4be6fa9e7ca85b8b4945093c5bdb13df5f1936ac25ad3c3a102b18aaaf118d8dc9e93fe8637ba37b09b232d3923eb257d451c7bb9da68a8ae189766a917f76ae0a864669656ee6f6c869553555d96e05b519e899d04a8b121d0203010001300d06092a864886f70d01010b050003820101002042005e9bdb007e94c01a6dc74bf56f16ddcd7a85b5407bdd8a20494c8949cbecff0ebf213e415fff05a8141c84273a79ce14387487ca1e449f1a18a50c38dd254dc3e8f29f987a0ba550e78572afa9bad0eabeac03609f74d6575456444fd3f5d35a94abf167b96ed3a774a12d0cebbfb7c9b6a7821e3c11ad940eed2c63647e9b7570e5aa609dbec0678d7ebba8acd4213c28dc5a01dfe6a1f438c5ee8c94ef573acd565334fe9287d74c94a934bc3959a9e7d69e7ac1074f8667a58b8f3e51e29564b164854610a983a4fdcc516b9b48726e6feee74247deb407e335ba411af80c5216d59815b689f398e13d4aa900f589960df72dd73e565d0b592480a0" ; var fakeSignature = signature.$new(mySignative); return Java .array ('android.content.pm.Signature' ,[fakeSignature]); } function fakeSigningInfo ( ){ var SigningDetails = Java .use ("android.content.pm.SigningDetails" ); var SigningInfo = Java .use ("android.content.pm.SigningInfo" ); var ArraySet = Java .use ("android.util.ArraySet" ); var fakeDetails = SigningDetails .$new(fakeSignatures (), 3 , ArraySet .$new(), fakeSignatures ()); return SigningInfo .$new(fakeDetails); } function patchPackageInfo (packageInfo ){ if (packageInfo.signatures .value != null ) { packageInfo.signatures .value = fakeSignatures (); } if (packageInfo.signingInfo .value != null ) { packageInfo.signingInfo .value = fakeSigningInfo (); } return packageInfo; } function hookSignative ( ){ var PackageManager = Java .use ("android.app.ApplicationPackageManager" ); var MessageDigest = Java .use ("java.security.MessageDigest" ); var getPackageInfoInt = PackageManager .getPackageInfo .overload ('java.lang.String' , 'int' ); getPackageInfoInt.implementation = function (packageName, flags ) { var packageInfo = getPackageInfoInt.call (this , packageName, flags); patchPackageInfo (packageInfo); console .log ("getPackageInfo hooked: " + packageName + ", flags=" + flags + ", signingInfo=" + (packageInfo.signingInfo .value != null )); return packageInfo; } try { var getPackageInfoFlags = PackageManager .getPackageInfo .overload ('java.lang.String' , 'android.content.pm.PackageManager$PackageInfoFlags' ); getPackageInfoFlags.implementation = function (packageName, flags ) { var packageInfo = getPackageInfoFlags.call (this , packageName, flags); patchPackageInfo (packageInfo); console .log ("getPackageInfo hooked: " + packageName + ", flags=" + flags + ", signingInfo=" + (packageInfo.signingInfo .value != null )); return packageInfo; } } catch (e) { } } function main ( ){ Java .perform (function ( ){ console .log ("hook start" ); hookSignative (); }); } setImmediate (main);
脚本 hook 了 getPackageInfo 方法,返回了伪造的 PackageInfo -> SigningInfo -> SigningDetails 值
此时签名校验失败。
如何防止这种方式 hook getPackageInfo 呢
# 构造 IPC 请求向系统服务获取应用签名
最直接的思路就是不使用 getPackageInfo,可以通过反射获取 Binder 对象,直接和系统服务进行通信
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 public static String IPCGetSignatureHash (Context context) { Parcel _data = Parcel.obtain(); Parcel _reply = Parcel.obtain(); try { PackageManager packageManager = context.getPackageManager(); Field mPMFile = packageManager.getClass().getDeclaredField("mPM" ); mPMFile.setAccessible(true ); Object mPM = mPMFile.get(packageManager); IBinder mRemote = ((IInterface) mPM).asBinder(); if (mRemote == null ) { Log.e("YvY_security" ,"获取IBinder失败" ); return null ; } _data.writeInterfaceToken("android.content.pm.IPackageManager" ); _data.writeString(context.getPackageName()); long flags = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)?134217728L :64L ; if (Build.VERSION.SDK_INT >= 33 ) { _data.writeLong(flags); } else { _data.writeInt((int )flags); } int userId = 0 ; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { userId = android.os.Process.myUid() / 100000 ; } _data.writeInt(userId); int transactCode = getTransactionCode(); boolean _status = mRemote.transact(transactCode,_data,_reply,0 ); if (!_status) { return null ; } _reply.readException(); PackageInfo packageInfo = null ; if (_reply.readInt() != 0 ) { packageInfo = PackageInfo.CREATOR.createFromParcel(_reply); } if (packageInfo != null ) { byte [] signatureBytes = null ; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && packageInfo.signingInfo != null ) { if (packageInfo.signingInfo.hasMultipleSigners()) { signatureBytes = packageInfo.signingInfo.getApkContentsSigners()[0 ].toByteArray(); } else { signatureBytes = packageInfo.signingInfo.getSigningCertificateHistory()[0 ].toByteArray(); } } else { signatureBytes = packageInfo.signatures[0 ].toByteArray(); } if (signatureBytes != null ) { return getSHA256(signatureBytes); } } }catch (Throwable e) { Log.e("YvY_security" ,"IPC signature check error" , e); } finally { _data.recycle(); _reply.recycle(); } return null ; } private static int getTransactionCode () { int TRANSACTION_getPackageInfo = 0 ; if (Build.VERSION.SDK_INT >= 33 ) { switch (Build.VERSION.SDK_INT) { case 33 : TRANSACTION_getPackageInfo = 3 ; break ; } } else { try { Class<?> pkmIPCClazz = Class.forName("android.content.pm.IPackageManager$Stub" ); Field field = pkmIPCClazz.getDeclaredField("TRANSACTION_getPackageInfo" ); field.setAccessible(true ); TRANSACTION_getPackageInfo = field.getInt(null ); } catch (Throwable e) { e.printStackTrace(); } } return TRANSACTION_getPackageInfo; } public static boolean verifySignature_IPC (Context context) { String signature = IPCGetSignatureHash(context); return expected_signature_hash.equals(signature); }
不使用 getPackageInfo,减少 hook 点。运行应用观察,发现此方法的签名校验失败
但这种方法同样有缺点
# 替换 PackageInfo.CREATOR
上面这段代码中
1 2 3 4 5 if (_reply.readInt() != 0 ){ packageInfo = PackageInfo.CREATOR.createFromParcel(_reply); }
解析 PackageInfo.CREATOR 为 PackageInfo 对象,所以可以通过反射替换此静态变量,伪造签名信息
hook 方法如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 var TARGET_PACKAGE = "com.example.mysecurityapp" ; function fakeSignatures () { var mySignative = "..." ; var Signature = Java.use ("android.content.pm.Signature" ); return Java.array ("android.content.pm.Signature" , [ Signature.$new (mySignative) ]); } function isFromIPCGetSignatureHash () { var Exception = Java.use ("java.lang.Exception" ); var Log = Java.use ("android.util.Log" ); var stack = Log.getStackTraceString (Exception.$new ()); return stack.indexOf ("com.example.mysecurityapp.SignatureVerificationUtils.IPCGetSignatureHash" ) !== -1 ; } function patchPackageInfoForIpc (packageInfo) { if (packageInfo == null) { return packageInfo; } var packageName = packageInfo.packageName.value; if (packageName !== TARGET_PACKAGE) { return packageInfo; } packageInfo.signatures.value = fakeSignatures (); if (packageInfo.signingInfo !== undefined) { packageInfo.signingInfo.value = null; } return packageInfo; } function hookSignativeIPC () { var PackageInfo = Java.use ("android.content.pm.PackageInfo" ); var ParcelableCreator = Java.use ("android.os.Parcelable$Creator" ); var originalCreator = PackageInfo.CREATOR.value; var className = "com.example.mysecurityapp.PackageInfoCreatorProxy" + Date.now (); var ProxiedCreator = Java.registerClass ({ name: className, implements: [ParcelableCreator], methods: { createFromParcel: [{ returnType: "java.lang.Object" , argumentTypes: ["android.os.Parcel" ], implementation: function (source) { var packageInfo = Java.cast ( originalCreator.createFromParcel (source), PackageInfo ); if (isFromIPCGetSignatureHash ()) { return patchPackageInfoForIpc (packageInfo); } return packageInfo; } }], newArray: [{ returnType: "[Ljava.lang.Object;" , argumentTypes: ["int" ], implementation: function (size) { return originalCreator.newArray (size); } }] } }); PackageInfo.CREATOR.value = ProxiedCreator.$new (); } function main () { Java.perform (function () { console.log ("hook start" ); hookSignativeIPC (); }); } setImmediate (main);
CREATOR 里包含两个方法
1.createFromParcel (Parcel source) 从二进制字节流里读取数据 拼装还原出一个完整的 Java 对象
2.newArray (int size) 用来创建该对象的数组
1 2 3 4 5 6 7 8 9 10 var packageInfo = Java.cast ( originalCreator.createFromParcel (source), PackageInfo ); if (isFromIPCGetSignatureHash ()) { return patchPackageInfoForIpc (packageInfo); } return packageInfo;
在我们自己创建的 CREATOR 中,截取到数据之后将 PackageInfo 替换即可成功替换
在 Java 代码中新建一个方法检测 PackageInfo.CREATOR 是否被替换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public static boolean checkIsPackageInfoReplace () { try { Field creatorField = PackageInfo.class.getField("CREATOR" ); creatorField.setAccessible(true ); Object creator = creatorField.get(null ); if (creator != null ) { ClassLoader creatorClassloader = creator.getClass().getClassLoader(); ClassLoader sysClassloader = PackageInfo.class.getClassLoader(); if (creatorClassloader == null || sysClassloader == null ) { return false ; } if (sysClassloader != creatorClassloader) { return true ; } } } catch (Throwable e) { e.printStackTrace(); } return false ; }
现在运行完整脚本,发现此方法被检测到
# native 层检测
先来写一个简单的 native 检测
native-lib.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 #include <jni.h> #include <string> #include <sstream> #include <iomanip> #include "Utils.h" #include "android/log.h" #define LOG_TAG "YvY_Security" #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) extern "C" JNIEXPORT jstring JNICALL Java_com_example_mysecurityapp_MainActivity_stringFromJNI ( JNIEnv* env, jobject ) { std::string hello = "Hello from C++" ; return env->NewStringUTF (hello.c_str ()); } std::string expected_signature = "308202e4308201cc020101300d06092a864886f70d01010b050030373116301406035504030c0d416e64726f69642044656275673110300e060355040a0c07416e64726f6964310b30090603550406130255533020170d3234313131363134343832385a180f32303534313130393134343832385a30373116301406035504030c0d416e64726f69642044656275673110300e060355040a0c07416e64726f6964310b300906035504061302555330820122300d06092a864886f70d01010105000382010f003082010a02820101009042f3b7fbe3164cac331f89ca5eefa713b17a3bc7385f7c740646feb823099717b5c83f4d466415e09cdc0aa1f195419b8b485c69c5da7c2a77b4252527188df70e99f842729e7e6aa6ed3b93e021035c740d53b2f8e65157b6994e4d054091b984c48a0b5161e63e7104e858e995117c884dfb8337e129b8b65af7ddbbca501261cd043c9b6105956b569f50d58c2b2fba2c81c30fbb022c30043f9ca13230d437b5d304cc6e4be6fa9e7ca85b8b4945093c5bdb13df5f1936ac25ad3c3a102b18aaaf118d8dc9e93fe8637ba37b09b232d3923eb257d451c7bb9da68a8ae189766a917f76ae0a864669656ee6f6c869553555d96e05b519e899d04a8b121d0203010001300d06092a864886f70d01010b050003820101002042005e9bdb007e94c01a6dc74bf56f16ddcd7a85b5407bdd8a20494c8949cbecff0ebf213e415fff05a8141c84273a79ce14387487ca1e449f1a18a50c38dd254dc3e8f29f987a0ba550e78572afa9bad0eabeac03609f74d6575456444fd3f5d35a94abf167b96ed3a774a12d0cebbfb7c9b6a7821e3c11ad940eed2c63647e9b7570e5aa609dbec0678d7ebba8acd4213c28dc5a01dfe6a1f438c5ee8c94ef573acd565334fe9287d74c94a934bc3959a9e7d69e7ac1074f8667a58b8f3e51e29564b164854610a983a4fdcc516b9b48726e6feee74247deb407e335ba411af80c5216d59815b689f398e13d4aa900f589960df72dd73e565d0b592480a0" ; extern "C" JNIEXPORT jboolean JNICALL Java_com_example_mysecurityapp_MainActivity_SignatureVerificationNative (JNIEnv *env, jclass clazz) { const std::string path = Utils::getBaseApkPath (); if (path.empty ()) { LOGE ("APK签名验证失败 原因: getBaseApkPath fail" ); return JNI_FALSE; } int fd = open (path.c_str (),O_RDONLY | O_CLOEXEC); if (fd < 0 ) { LOGE ("APK签名验证失败 原因: open fd fail" ); return JNI_FALSE; } std::vector<unsigned char > cert_stream = Utils::read_certificate (fd); if (cert_stream.empty ()) { LOGE ("APK签名验证失败 原因: read_certificate fail" ); close (fd); return JNI_FALSE; } std::stringstream stream; stream << std::hex << std::setfill ('0' ); for (unsigned char byte : cert_stream) { stream << std::setw (2 ) << static_cast <int >(byte); } std::string APK_sign = stream.str (); jboolean is_valid = JNI_FALSE; if (APK_sign == expected_signature) { is_valid = JNI_TRUE; } else { is_valid = JNI_FALSE; } close (fd); return is_valid; }
(有些工具类函数,比如解析 v2 v3 签名的函数,使用 ai 辅助生成)
# libc 函数重定向
在上面这个例子中,完全可以 hook open 函数,将参数一的地址替换成原始 apk,这样签名校验一定会成功
hook 脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 const original_apk = "/data/user/0/com.example.mysecurityapp/files/app-debug.apk" ;const target_so = "libmysecurityapp.so" ;const original_apk_ptr = Memory .allocUtf8String (original_apk);function isTargetFile (path ){ if (!path) return false ; return (path.indexOf ("base.apk" ) !== -1 ) && path != original_apk; } function isTargerCaller (context ){ const caller = DebugSymbol .fromAddress (context.lr ); if (caller.moduleName .indexOf (target_so) !== -1 ) { return true ; } return false ; } function attachOpenLike (name,argsIndex ){ const addr = Module .findExportByName ("libc.so" ,name); Interceptor .attach (addr,{ onEnter (args ) { const pathPtr = args[argsIndex]; const path = Memory .readUtf8String (pathPtr); if (isTargetFile (path) && isTargerCaller (this .context )) { args[argsIndex] = original_apk_ptr; console .log (name + "重定向成功" ); } } }); } function hookOpen ( ) { attachOpenLike ("open" , 0 ); attachOpenLike ("open64" , 0 ); attachOpenLike ("__open_2" , 0 ); attachOpenLike ("openat" , 1 ); attachOpenLike ("openat64" , 1 ); } function main ( ) { console .log ("hook start" ); hookOpen (); } setImmediate (main);
编译保护将 open 函数自动替换为__open_2,所以这里不能只 hook open 函数,效果如下
为了防止对 libc 函数进行 hook,可以考虑使用 SVC 指令进行代替
在 ARM 架构中,SVC 指令用于从用户态切换到内核态,执行系统调用。
使用 SVC 指令比普通函数更加安全
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 #include <jni.h> #include <string> #include <sstream> #include <iomanip> #include <cerrno> #include "Utils.h" #include "android/log.h" #define LOG_TAG "YvY_Security" #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) namespace {constexpr int SVC_AT_FDCWD = -100 ;constexpr int SVC_OPENAT_ARM64 = 56 ;constexpr int SVC_OPENAT_ARM = 322 ;int svc_openat (const char *path, int flags, int mode) {#if defined(__aarch64__) register long x0 asm ("x0" ) = SVC_AT_FDCWD; register long x1 asm ("x1" ) = reinterpret_cast <long >(path); register long x2 asm ("x2" ) = flags; register long x3 asm ("x3" ) = mode; register long x8 asm ("x8" ) = SVC_OPENAT_ARM64; asm volatile ( "svc #0" : "+r" (x0) : "r" (x1), "r" (x2), "r" (x3), "r" (x8) : "memory" ) ; return static_cast <int >(x0); #elif defined(__arm__) register long r0 asm ("r0" ) = SVC_AT_FDCWD; register long r1 asm ("r1" ) = reinterpret_cast <long >(path); register long r2 asm ("r2" ) = flags; register long r3 asm ("r3" ) = mode; asm volatile ( "push {r7}\n" "mov r7, %4\n" "svc #0\n" "pop {r7}" : "+r" (r0) : "r" (r1), "r" (r2), "r" (r3), "r" (SVC_OPENAT_ARM) : "memory" ) ; return static_cast <int >(r0); #else return open (path, flags, mode); #endif } int open_apk_by_svc (const char *path, int flags) { int fd = svc_openat (path, flags, 0 ); if (fd < 0 ) { errno = -fd; return -1 ; } return fd; } } }
# fd 替换
此时再去 hook open 函数难度较高,但可以使用 fd 替换的方式来绕过签名校验,重打包后的应用 fd 指向 bask.apk,将指向的文件改为原始 apk 即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 const original_apk = "/data/user/0/com.example.mysecurityapp/files/app-debug.apk" ;const target_so = "libmysecurityapp.so" ;const target_symbol = "_ZN5Utils16read_certificateEi" ;const O_RDONLY = 0 ;const O_CLOEXEC = 0x80000 ;const openPtr = Module .findExportByName ("libc.so" ,"open" );const dup2Ptr = Module .findExportByName ("libc.so" ,"dup2" );const closePtr = Module .findExportByName ("libc.so" ,"close" );const openFn = new SystemFunction (openPtr,"int" ,["pointer" ,"int" ]);const dup2Fn = new SystemFunction (dup2Ptr,"int" ,["int" ,"int" ]);const closeFn = new NativeFunction (closePtr,"int" ,["int" ]);const originalApkPtr = Memory .allocUtf8String (original_apk);function hookFd ( ){ const targetModule = Process .findModuleByName (target_so); if (targetModule === null ) return ; const target = targetModule.findExportByName (target_symbol); Interceptor .attach (target,{ onEnter (args ) { const realFd = args[0 ].toInt32 (); const openResult = openFn (originalApkPtr,O_RDONLY | O_CLOEXEC ); const fakeFd = openResult.value ; const dup2Result = dup2Fn (fakeFd,realFd); if (dup2Result.value < 0 ) { closeFn (fakeFd); return ; } this .shadowFd = fakeFd; }, onLeave ( ) { if (this .shadowFd >= 0 ) { closeFn (this .shadowFd ); console .log ("替换成功" ); } } }); } function main ( ) { console .log ("hook start" ); var isHooked = false ; var timer = setInterval (function ( ){ var module = Process .findModuleByName (target_so); if (module !== null && !isHooked) { hookFd (); clearInterval (timer); } },10 ); } setImmediate (main);
这里使用 dup2 函数替换了原始的 fd,这时 fd 指向的不再是当前应用的 base.apk,而是我们早就准备好的原始 apk,所以校验一定会成功。
运行脚本,同样可以成功绕过
当然这种方法也有对应的保护措施
可以新建一个函数,用来判断 fd 是否被替换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 if (!Utils::isExpectedFdPath (fd, path)) { LOGE ("APK签名验证失败 原因: fd path mismatch" ); close (fd); return 2 ; } bool Utils::isExpectedFdPath (int fd, const std::string& expected_path) { if (fd < 0 || expected_path.empty ()) { return false ; } char fd_link[64 ] = {}; std::snprintf (fd_link, sizeof (fd_link), "/proc/self/fd/%d" , fd); char resolved_path[PATH_MAX] = {}; ssize_t len = readlink (fd_link, resolved_path, sizeof (resolved_path) - 1 ); if (len <= 0 ) { return false ; } resolved_path[len] = '\0' ; return expected_path == resolved_path; }
假如 fd 指向的不再是原始文件,证明被重定向
这里修改了一下函数类型,如果是被替换则返回 2,MainActivity 调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 case "signature_native_button" : native_res = SignatureVerificationNative(); if (native_res == 1 ) { native_textView.setText("签名校验成功" ); } else { native_textView.setText("签名校验失败" ); } if (native_res == 2 ) { native_textView.setText("检测到fd被替换" ); }
再次 hook,观察日志
# 对比 Inode 值
除了上面检测 fd 路径的方法,还可以对比 Inode 的值
Inode 就像每个文件的身份证,里面不存储具体数据,而是存文件的元数据。每个文件都有一个唯一的 Inode 值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 bool Utils::isExpectedFdInode (int fd, const std::string& expected_path) { if (fd < 0 || expected_path.empty ()) return false ; struct stat64 fd_stat; if (fstat64 (fd,&fd_stat) != 0 ) return false ; std::ifstream maps_files ("/proc/self/maps" ) ; std::string line,dummy,path; unsigned long long maps_inode; while (std::getline (maps_files,line)) { if (line.find (expected_path) == std::string ::npos) continue ; std::istringstream iss (line) ; if (iss >> dummy >> dummy >> dummy >> dummy >> maps_inode >> path) { if (path == expected_path) { return maps_inode == static_cast <unsigned long long >(fd_stat.st_ino); } } } return false ; }
注释掉刚才的 fd 检测,新加上这个检测 Inode 值的函数,运行
成功检测到。fd 检测可以通过 magisk 挂载等方式解决,比对 Inode 号的检测方法更加直接