# 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用来储存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 调用这个方法

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 对象
// 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

上面这段代码中

1
2
3
4
5
if(_reply.readInt() != 0)
{
// 从数据包里还原出 PackageInfo 实例
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
// 检测替换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

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 /* 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 脚本如下:

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;
// 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 即可

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();
// 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 是否被替换。

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
// 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 调用

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;
// 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 号的检测方法更加直接