Android风险环境检测——签名校验"

admin 2026-10-20 22:43:48 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

cover_image

Android风险环境检测——签名校验

mb\_wckjnnha
mb\_wckjnnha

看雪学苑

2026年5月29日 18:00
上海

在小说阅读器读本章

去阅读

最近在阅读《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}\
\"
            \"mov r7, %4\
\"
            \"svc #0\
\"
            \"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号的检测方法更加直接。

看雪ID:mb\_wckjnnha

https://bbs.kanxue.com/user-home-1008445.htm

\*本文为看雪论坛精华文章,由 mb\_wckjnnha 原创,转载请注明来自看雪社区

第十届安全开发者峰会【议题征集】-欢迎投稿

# 往期推荐

安卓逆向基础知识之frida Hook

2025 强网杯和强网拟态部分题解

在逆向分析方面-unidbg真的适合 MCP 吗?

AI静态分析,内核模块隐藏 Frida 特征,绕过linker私有结构遍历崩溃链

某安全so库深度解析

球分享

球点赞

球在看

戳“阅读原文”一起来充电吧!


Android风险环境检测——签名校验" 网络安全文章

Android风险环境检测——签名校验"

该文档详细介绍了Android应用签名校验的Java层实现方法,包括通过PackageManager获取签名信息、处理不同Android版本差异以及SHA-256哈希计算。文章指出单纯Java层检测易被Hook绕过,并演示了通过Frida伪造PackageInfo和SigningInfo的实战方法,最后提到后续将介绍Native层加固方案。
通知|国家数据局等三部门联合印发《2026年交通运输、气象服务领域数据流通安全治理典型案例》" 网络安全文章

通知|国家数据局等三部门联合印发《2026年交通运输、气象服务领域数据流通安全治理典型案例》"

国家数据局联合交通运输部、中国气象局发布2026年交通运输与气象服务领域数据流通安全治理典型案例,涵盖ETC门架数据安全应用、船舶碳核算数据沙箱技术、港口集装箱业务数据流通等11个案例。案例聚焦跨主体数据流通中的合规挑战、全链路安全防护及标准化协同机制,通过明权责定标准、技术赋能和常态化监测等措施,实现数据安全与价值释放的平衡。该文件为相关领域提供了可复制的安全治理实践范式。
评论|依法向非法网络招徕亮剑" 网络安全文章

评论|依法向非法网络招徕亮剑"

文化和旅游部联合中央网信办开展非法网络招徕专项整治,针对旅游市场存在的虚假宣传、强迫购物等乱象采取全链条治理措施,旨在规范市场秩序、保护游客权益,为即将到来的五一假期营造安全优质的旅游环境。
2025年度重庆市科学技术奖(网络空间安全领域)" 网络安全文章

2025年度重庆市科学技术奖(网络空间安全领域)"

2025年度重庆市科学技术奖网络空间安全相关项目涵盖通信安全、物联网安全、车联网安全等领域,涉及基础理论到前沿应用的全链条创新。重庆大学、重庆邮电大学等高校联合科研院所和企业形成产学研用结合模式,项目聚焦智能网联汽车、智慧城市等产业需求,体现安全技术与地方发展深度融合的导向。
评论:0   参与:  0