前言
最近在阅读《Android设备指纹攻防与风险环境检测》时,发现光靠阅读书中的思路和代码片段不能很好的理解,所以打算自己写一个app,实践一下
java层签名检测
java层通常使用PackageManager进行签名校验
在Android9以上,签名可以通过PackageInfo -> signingInfo -> signatures获得,以下则是通过PackageInfo -> signatures
无论是哪种,获取PackageInfo都会使用getPackageInfo这个api
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用来储存package中的一些信息 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调用这个方法
@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返回正确校验值
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对象,直接和系统服务进行通信
public static String IPCGetSignatureHash(Context context) { // 从对象池中获取两个 Parcel 对象 // Parcel 是 Android 中用于在不同组件之间传递数据的容器,它可以包含基本数据类型、对象引用以及实现了 Parcelable 接口的对象 // Parcel 的主要用途是进行进程间通信(IPC),特别是在使用 Binder 机制时 // _data 用于存放发送给系统服务的数据 // _reply 用于接收系统服务返回的数据结果 Parcel _data = Parcel.obtain(); Parcel _reply = Parcel.obtain(); try{ // 通过反射获取底层 IBinder 对象 // 获取 PackageManager 对象(ApplicationPackageManager 实例) PackageManager packageManager = context.getPackageManager(); // 反射获取 ApplicationPackageManager 内部隐藏的 mPM 变量 // 获取 mPM 字段对象 Field mPMFile = packageManager.getClass().getDeclaredField("mPM"); mPMFile.setAccessible(true); // get packageManager 中的 mPM Object mPM = mPMFile.get(packageManager); // mPM 是 IPackageManager 的 Proxy,直接通过 asBinder 获取 IBinder IBinder mRemote = ((IInterface) mPM).asBinder(); if(mRemote == null) { Log.e("YvY_security","获取IBinder失败"); return null; } // 写入接口 Token,告诉系统调用的接口 _data.writeInterfaceToken("android.content.pm.IPackageManager"); // a1 : 包名 _data.writeString(context.getPackageName()); // Android 13 之前使用 writeInt,Android 13 及以上 flags 参数类型为 long // a2 : 获取信息标志位 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); } // a3 : userId 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(); // 调用 transact 触发底层 Binder 驱动通信 boolean _status = mRemote.transact(transactCode,_data,_reply,0); if(!_status) { return null; } _reply.readException(); PackageInfo packageInfo = null; if(_reply.readInt() != 0) { // 从数据包里还原出 PackageInfo 实例 packageInfo = PackageInfo.CREATOR.createFromParcel(_reply); } if(packageInfo != null) { byte[] signatureBytes = null; // Android 9 以上 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 { // Android 9 以下使用 signatures 数组获取签名 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; } // Android 13 及以上无法通过反射获取隐藏 API,只能硬编码或使用第三方库解除限制 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
上面这段代码中
if(_reply.readInt() != 0) { // 从数据包里还原出 PackageInfo 实例 packageInfo = PackageInfo.CREATOR.createFromParcel(_reply); }解析PackageInfo.CREATOR为PackageInfo对象,所以可以通过反射替换此静态变量,伪造签名信息
hook方法如下
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) 用来创建该对象的数组
var packageInfo = Java.cast( originalCreator.createFromParcel(source), PackageInfo ); if (isFromIPCGetSignatureHash()) { return patchPackageInfoForIpc(packageInfo); } return packageInfo;在我们自己创建的CREATOR中,截取到数据之后将PackageInfo替换即可成功替换
在Java代码中新建一个方法检测PackageInfo.CREATOR是否被替换
// 检测替换PackageInfo.CREATOR public static boolean checkIsPackageInfoReplace() { try{ Field creatorField = PackageInfo.class.getField("CREATOR"); creatorField.setAccessible(true); // 反射获取creator对象 Object creator = creatorField.get(null); if(creator != null) { // 系统默认的CREATOR加载器为BootClassLoader 但是替换后的通常不是 ClassLoader creatorClassloader = creator.getClass().getClassLoader(); // 获取PackageInfo的加载器 此加载器为BootClassLoader 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
#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 /* this */) { 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脚本如下:
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指令比普通函数更加安全
#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; // openat的系统调用号 constexpr int SVC_OPENAT_ARM64 = 56; constexpr int SVC_OPENAT_ARM = 322; int svc_openat(const char *path, int flags, int mode) { // arm64 #if defined(__aarch64__) register long x0 asm("x0") = SVC_AT_FDCWD; // a1 : 目录文件描述符 register long x1 asm("x1") = reinterpret_cast<long>(path); // a2 : 文件路径的指针 register long x2 asm("x2") = flags; // a3 : 标志 register long x3 asm("x3") = mode; // a4 : 权限模式 register long x8 asm("x8") = SVC_OPENAT_ARM64; // x8寄存器存放系统调用号 asm volatile( "svc #0" : "+r"(x0) : "r"(x1), "r"(x2), "r"(x3), "r"(x8) : "memory"); return static_cast<int>(x0); // 将返回值转为int返回 #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; } } // namespace }fd替换
此时再去hook open函数难度较高,但可以使用fd替换的方式来绕过签名校验,重打包后的应用fd指向bask.apk,将指向的文件改为原始apk即可
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(); // open函数打开原始apk 获得fakefd const openResult = openFn(originalApkPtr,O_RDONLY | O_CLOEXEC); const fakeFd = openResult.value; // 使用dup2函数复制fakefd 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"); // 轮询查找目标so文件 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是否被替换。
// native-lib.cpp if (!Utils::isExpectedFdPath(fd, path)) { LOGE("APK签名验证失败 原因: fd path mismatch"); close(fd); return 2; } // Utils.cpp 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调用
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值。
bool Utils::isExpectedFdInode(int fd, const std::string& expected_path) { if(fd < 0 || expected_path.empty()) return false; // stat64结构体储存文件系统元数据 struct stat64 fd_stat; // 获取文件系统元数据 if(fstat64(fd,&fd_stat) != 0) return false; // Linux 特殊的虚拟文件/proc/self/maps记录了当前 App 进程加载到内存里的所有模块和文件 std::ifstream maps_files("/proc/self/maps"); std::string line,dummy,path; unsigned long long maps_inode; // 逐行读取maps内容 while (std::getline(maps_files,line)) { if(line.find(expected_path) == std::string ::npos) continue; std::istringstream iss(line); // maps格式通常为 : 地址 权限 偏移 设备号 Inode 路径 // 这里将line按流解析 直接获得第五个Inode if(iss >> dummy >> dummy >> dummy >> dummy >> maps_inode >> path) { if(path == expected_path) { // 将当前fd的Inode与原始apk的Inode值进行比较 return maps_inode == static_cast<unsigned long long>(fd_stat.st_ino); } } } return false; }注释掉刚才的fd检测,新加上这个检测Inode值的函数,运行
成功检测到。fd检测可以通过magisk挂载等方式解决,比对Inode号的检测方法更加直接