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 原创,转载请注明来自看雪社区
第十届安全开发者峰会【议题征集】-欢迎投稿
# 往期推荐
AI静态分析,内核模块隐藏 Frida 特征,绕过linker私有结构遍历崩溃链
球分享
球点赞
球在看
戳“阅读原文”一起来充电吧!
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论