安卓签名校验-探讨

admin 2025年3月3日19:38:39评论10 views字数 18683阅读62分16秒阅读模式

基础签名校验-建立java层理论基础

    private boolean doNormalSignCheck() {
        String trueSignMD5 = "7d1e7be834bb349eb0694c524353ba3c";
        String nowSignMD5 = "";
        try {
            PackageInfo packageInfo = getPackageManager().getPackageInfo(
                    getPackageName(),
                    PackageManager.GET_SIGNATURES);
            Signature[] signs = packageInfo.signatures;
            if (signs != null && signs.length > 0) { // 检查 signs 是否为空
                byte[] signature = signs[0].toByteArray();
                String signBase64 = Base64.encodeToString(signature, Base64.DEFAULT).trim();
                nowSignMD5 = md5(signBase64);
                Log.d(TAG, "doNormalSignCheck: " + nowSignMD5);
            }
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return trueSignMD5.equals(nowSignMD5);
    }

这里通过如果我直接hook getPackageInfo()  使他返回我们包装的packageInfo 不就解决了?

但是,我想了解整个过程。

平时逆向分析都是通过hook关键函数,打印堆栈,不就知道流程?

说干就干

我们在安卓源码找到  PackageInfo  的构造函数,直接用xposed hook就完事了。

hook代码:

 XposedHelpers.findAndHookConstructor(classLoader.loadClass("android.content.pm.PackageInfo"),Parcel.class, new XC_MethodHook() {
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                        super.beforeHookedMethod(param);
                    }

@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
dumpStackTrace();
super.afterHookedMethod(param);
}
});

打印的堆栈:

2025-02-19 11:46:25.628 22548-22548 风控                      com.calvin.sigcheck                  I  android.content.pm.PackageInfo----PackageInfo.java----29----<init>
2025-02-19 11:46:25.628 22548-22548 风控                      com.calvin.sigcheck                  I  android.content.pm.PackageInfo$1----PackageInfo.java----519----createFromParcel
2025-02-19 11:46:25.628 22548-22548 风控                      com.calvin.sigcheck                  I  android.content.pm.PackageInfo$1----PackageInfo.java----516----createFromParcel
2025-02-19 11:46:25.628 22548-22548 风控                      com.calvin.sigcheck                  I  android.content.pm.IPackageManager$Stub$Proxy----IPackageManager.java----4901----getPackageInfo
2025-02-19 11:46:25.628 22548-22548 风控                      com.calvin.sigcheck                  I  android.content.pm.PackageManager----PackageManager.java----9207----getPackageInfoAsUserUncached
2025-02-19 11:46:25.628 22548-22548 风控                      com.calvin.sigcheck                  I  android.content.pm.PackageManager----PackageManager.java----113----access$100
2025-02-19 11:46:25.628 22548-22548 风控                      com.calvin.sigcheck                  I  android.content.pm.PackageManager$2----PackageManager.java----9220----recompute
2025-02-19 11:46:25.628 22548-22548 风控                      com.calvin.sigcheck                  I  android.content.pm.PackageManager$2----PackageManager.java----9217----recompute
2025-02-19 11:46:25.628 22548-22548 风控                      com.calvin.sigcheck                  I  android.app.PropertyInvalidatedCache----PropertyInvalidatedCache.java----562----query
2025-02-19 11:46:25.628 22548-22548 风控                      com.calvin.sigcheck                  I  android.content.pm.PackageManager----PackageManager.java----9235----getPackageInfoAsUserCached
2025-02-19 11:46:25.628 22548-22548 风控                      com.calvin.sigcheck                  I  android.app.ApplicationPackageManager----ApplicationPackageManager.java----236----getPackageInfoAsUser
2025-02-19 11:46:25.628 22548-22548 风控                      com.calvin.sigcheck                  I  android.app.ApplicationPackageManager----ApplicationPackageManager.java----213----getPackageInfo
2025-02-19 11:46:25.629 22548-22548 风控                      com.calvin.sigcheck                  I  com.calvin.sigcheck.MainActivity----MainActivity.java----370----doNormalSignCheck

从堆栈来讲 ,我 hook上面任何一种方法都可以对签名信息进行替换,达到去签名的效果。

这里是我们java层的理论基础!

观摩前辈

MT 去除签名校验--https://github.com/L-JINBIN/ApkSignatureKillerEx

关键代码:

    private static void killPM(String packageName, String signatureData) {
        Signature fakeSignature = new Signature(Base64.decode(signatureData, Base64.DEFAULT));
        Parcelable.Creator<PackageInfo> originalCreator = PackageInfo.CREATOR;
        Parcelable.Creator<PackageInfo> creator = new Parcelable.Creator<PackageInfo>() {
            @Override
            public PackageInfo createFromParcel(Parcel source) {
                PackageInfo packageInfo = originalCreator.createFromParcel(source);
                if (packageInfo.packageName.equals(packageName)) {
                    if (packageInfo.signatures != null && packageInfo.signatures.length > 0) {
                        packageInfo.signatures[0] = fakeSignature;
                    }
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                        if (packageInfo.signingInfo != null) {
                            Signature[] signaturesArray = packageInfo.signingInfo.getApkContentsSigners();
                            if (signaturesArray != null && signaturesArray.length > 0) {
                                signaturesArray[0] = fakeSignature;
                            }
                        }
                    }
                }
                return packageInfo;
            }

@Override
public PackageInfo[] newArray(int size) {
return originalCreator.newArray(size);
}
};
try {
findField(PackageInfo.class, "CREATOR").set(null, creator);
} catch (Exception e) {
throw new RuntimeException(e);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
HiddenApiBypass.addHiddenApiExemptions("Landroid/os/Parcel;", "Landroid/content/pm", "Landroid/app");
}
try {
Object cache = findField(PackageManager.class, "sPackageInfoCache").get(null);
//noinspection ConstantConditions
cache.getClass().getMethod("clear").invoke(cache);
} catch (Throwable ignored) {
}
try {
Map<?, ?> mCreators = (Map<?, ?>) findField(Parcel.class, "mCreators").get(null);
//noinspection ConstantConditions
mCreators.clear();
} catch (Throwable ignored) {
}
try {
Map<?, ?> sPairedCreators = (Map<?, ?>) findField(Parcel.class, "sPairedCreators").get(null);
//noinspection ConstantConditions
sPairedCreators.clear();
} catch (Throwable ignored) {
}
}

这个代码的功能就是使用反射,替换 CREATOR 变量,而体现出来的就是 :hook createFromParcel 这个方法,从而替换了签名信息。

而从上面我说所的基础,体现出来,确实存在 createFromParcel 这个方法。而且在距离构造函数很近,真是感叹前辈技术的精妙!

然后对 libc进行 open相关的函数进行hook:

JNIEXPORT void JNICALL
Java_bin_mt_signature_KillerApplication_hookApkPath(JNIEnv *env, __attribute__((unused)) jclass clazz, jstring apkPath, jstring repPath) {
    apkPath__ = (*env)->GetStringUTFChars(env, apkPath, 0);
    repPath__ = (*env)->GetStringUTFChars(env, repPath, 0);

xhook_register(".*\.so$", "openat64", openat64Impl, (void **) &old_openat64);
xhook_register(".*\.so$", "openat", openatImpl, (void **) &old_openat);
xhook_register(".*\.so$", "open64", open64Impl, (void **) &old_open64);
xhook_register(".*\.so$", "open", openImpl, (void **) &old_open);

xhook_refresh(0);
}

就很轻松的实现了对文件的io重定向

还有高手?对的

LSPatch 打包也是很强的!

https://github.com/LSPosed/LSPatch/blob/master/patch-loader/src/main/java/org/lsposed/lspatch/loader/SigBypass.java

关键代码:

private static void proxyPackageInfoCreator(Context context) {
        Parcelable.Creator<PackageInfo> originalCreator = PackageInfo.CREATOR;
        Parcelable.Creator<PackageInfo> proxiedCreator = new Parcelable.Creator<>() {
            @Override
            public PackageInfo createFromParcel(Parcel source) {
                PackageInfo packageInfo = originalCreator.createFromParcel(source);
                replaceSignature(context, packageInfo);
                return packageInfo;
            }

@Override
public PackageInfo[] newArray(int size) {
return originalCreator.newArray(size);
}
};
XposedHelpers.setStaticObjectField(PackageInfo.class, "CREATOR", proxiedCreator);
try {
Map<?, ?> mCreators = (Map<?, ?>) XposedHelpers.getStaticObjectField(Parcel.class, "mCreators");
mCreators.clear();
} catch (NoSuchFieldError ignore) {
} catch (Throwable e) {
Log.w(TAG, "fail to clear Parcel.mCreators", e);
}
try {
Map<?, ?> sPairedCreators = (Map<?, ?>) XposedHelpers.getStaticObjectField(Parcel.class, "sPairedCreators");
sPairedCreators.clear();
} catch (NoSuchFieldError ignore) {
} catch (Throwable e) {
Log.w(TAG, "fail to clear Parcel.sPairedCreators", e);
}
}

有木有发现相同的方式? 都是对 CREATOR  这个变量进行替换  达到 hook createFromParcel 这个方法目的  代码写的是相当优雅  反射替换 稳定性相当高!

然后对so层进行 io重定向:

    LSP_DEF_NATIVE_METHOD(void, SigBypass, enableOpenatHook, jstring origApkPath, jstring cacheApkPath) {
        auto sym_openat = SandHook::ElfImg("libc.so").getSymbAddress<void *>("__openat");
        auto r = HookSymNoHandle(handler, sym_openat, __openat);
        if (!r) {
            LOGE("Hook __openat fail");
            return;
        }
        lsplant::JUTFString str1(env, origApkPath);
        lsplant::JUTFString str2(env, cacheApkPath);
        apkPath = str1.get();
        redirectPath = str2.get();
        LOGD("apkPath %s", apkPath.c_str());
        LOGD("redirectPath %s", redirectPath.c_str());
    }

也是 对 libc.so的 hook  open相关的函数,实现io重定向。

去签名校验的流程了解的很清楚。

我们破坏或者检测,这里面的任何流程去签就失效了

攻防之战-开始!

对本地的apk的签名文件进行检测

    private byte[] signatureFromAPK() {
        try (ZipFile zipFile = new ZipFile(getPackageResourcePath())) {
            Enumeration<? extends ZipEntry> entries = zipFile.entries();
            while (entries.hasMoreElements()) {
                ZipEntry entry = entries.nextElement();
                if (entry.getName().matches("(META-INF/.*)\.(RSA|DSA|EC)")) {
                    InputStream is = zipFile.getInputStream(entry);
                    CertificateFactory certFactory = CertificateFactory.getInstance("X509");
                    X509Certificate x509Cert = (X509Certificate) certFactory.generateCertificate(is);
                    return x509Cert.getEncoded();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

这个方式对 mt的去签名方式,在不同的安卓系统里有不一样的效果,因为 io重定向 诡异的失效了。

原因可以看这篇文章 :https://bbs.kanxue.com/thread-278195.htm

对于某些去签名方式,他们会对application 进行替换。

这里我们可以获取application,然后对application 进行一些判断:

 public Application getmyApplication() {
        Class<?> aaaaaaa = null;
        try {
            aaaaaaa = Class.forName("android.app.ActivityThread");
            Method currentActivityThreadMethod = aaaaaaa.getDeclaredMethod("currentActivityThread");
            Object activityThread = currentActivityThreadMethod.invoke(null);
            Application application =(Application) getObjectField(activityThread, "mInitialApplication");
            Log.d(TAG, "ActivityThread 获取 Application: "+application.getClass().getName());
            return application;
        } catch (Exception e) {
            Log.d(TAG, "报错!: "+e.getMessage());
            return null;
        }
    }

这里使用更深入的的方式获取 Application,当然还有更深入的方式:比如反射 LoadedApk 获取唯一单例的Application,更为底层但是考虑到安卓版本的兼容性,这里就这样获取了。

对application 进行一些简单的判断,当然你可以继续深入!

   private boolean checkApplication(){
        Application nowApplication = getmyApplication();
        String trueApplicationName = "com.calvin.sigcheck.MYapp";
        String nowApplicationName = nowApplication.getClass().getName();
        Log.d(TAG, "checkApplication: "+trueApplicationName+"  "+nowApplicationName);
        return trueApplicationName.equals(nowApplicationName);
    }

这里就完成的对apk入口的简单判断,在mt去签名的早期版本是对 mPM 进行的替换。

这里顺便写一个吧,顺便检测了。

   private boolean checkPMProxy(){
        String truePMName = "android.content.pm.IPackageManager$Stub$Proxy";
        String nowPMName = "";
        try {
            // 被代理的对象是 PackageManager.mPM
            PackageManager packageManager = getPackageManager();
            Field mPMField = packageManager.getClass().getDeclaredField("mPM");
            mPMField.setAccessible(true);
            Object mPM = mPMField.get(packageManager);
            // 取得类名
            nowPMName = mPM.getClass().getName();
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 类名改变说明被代理了
        return truePMName.equals(nowPMName);
    }

当然在一定程度上可以检测是否在虚拟环境,毕竟virtrualapp的老版本,也是通过 反射hook进行多开的。

前面说的都是对付老版本的方法,现在分析刚刚我们看到的去签名方式,针对性的分析可以得到。

 

我们的目标就是检测 Creator  是否被替换。

https://bbs.kanxue.com/thread-277402.htm#%E6%A3%80%E6%B5%8Bcreator%E6%98%AF%E5%90%A6%E8%A2%AB%E6%9B%BF%E6%8D%A2

这里大佬珍惜分享出比较 classloader 的属性来判断。

    private boolean checkCreator3(){
        try {
            Field creatorField = PackageInfo.class.getField("CREATOR");
            creatorField.setAccessible(true);
            Object creator = creatorField.get(null);

int i = creator.hashCode();
Log.d(TAG, "checkCreator3: hashcode :"+i);
if (creator != null) {
ClassLoader creatorClassloader = creator.getClass().getClassLoader();
ClassLoader sysClassloader = ClassLoader.getSystemClassLoader();
if (creatorClassloader == null || sysClassloader == null) {
return false;
}
//系统的是bootclassloader
//用户创建的都是pathclassloader
//如果相等则认为系统的被替换
if (sysClassloader.getClass().getName().
equals(creatorClassloader.getClass().getName())) {
return false;
}
return true;
}
} catch (Throwable e) {
Log.d(TAG, "checkCreator3: "+e);
}
return false;
}

但我测试发现好像lsp的去签名方式,检测不出来。

没关系,我们继往圣之绝学,继续拓展。

通过珍惜大佬的思路,判断的就是 CREATOR 是否被改变---- 那么任何细微的差距都可。

我在这演示一点,剩下的交给后来人吧!

  private boolean checkCreator(){
        String trueName = "android.content.pm.PackageInfo$1";
        String nowName = "";
        try {

Object mPM = PackageInfo.CREATOR;
// 取得类名
nowName = mPM.getClass().getName();
} catch (Exception e) {
e.printStackTrace();
}
return trueName.equals(nowName);
}

这里我们就获取的CREATOR 的类名,如果不一样就被替换了。

当然如果被hook了呢?

一般去签名的软件是不会对app进行大量的hook,如果这个被 hook 了,那我们反射获取其他属性。

 

   private boolean checkCreator2(){

Field[] declaredFields = null;

try {
Object mPM = PackageInfo.CREATOR;
// 取得类名
declaredFields = mPM.getClass().getDeclaredFields();

} catch (Exception e) {
e.printStackTrace();
}
return declaredFields.length == 0;
}

这样,lsp打包的apk就被我们 anti了。

此外,我们继续深耕。

我们是否可以在内存中对我们的代码进行检验呢?

答案是可以,我早在以前就发了一篇关于内存校验的帖子 ,但是那时候的方式被大佬吐槽,确实只能在debug版本才生效。

那篇帖子地址:https://bbs.kanxue.com/thread-282315.htm

现在我继续,探究这种方式。

从安卓源码classloader加载的方式,我们获取app自身加载的dex。

对dex 进行检测

我在这个过程中也遇到很多的问题,当然被解决啦!

这里的环境为:安卓12,其他版本我不保证兼容性哈!

第一个问题就是安卓的反射限制,这里抄的看雪论坛里一篇大佬的代码。

 

bool setApiBlacklistExemptions(JNIEnv* env) {
    /*=================在这里使用了ZygoteInit,而不是VMRuntime, 效果一样==================*/
    jclass zygoteInitClass = env->FindClass("com/android/internal/os/ZygoteInit");
    if (zygoteInitClass == nullptr) {
        ALOGE("not found class");
        env->ExceptionClear();
        return false;
    }

jmethodID setApiBlackListApiMethod =
env->GetStaticMethodID(zygoteInitClass,
"setApiBlacklistExemptions",
"([Ljava/lang/String;)V");
if (setApiBlackListApiMethod == nullptr) {
env->ExceptionClear();
setApiBlackListApiMethod =
env->GetStaticMethodID(zygoteInitClass,
"setApiDenylistExemptions",
"([Ljava/lang/String;)V");
}

if (setApiBlackListApiMethod == nullptr) {
ALOGE("not found method");
return false;
}

jclass stringCLass = env->FindClass("java/lang/String");

jstring fakeStr = env->NewStringUTF("L");

jobjectArray fakeArray = env->NewObjectArray(
1, stringCLass, NULL);

env->SetObjectArrayElement(fakeArray, 0, fakeStr);

env->CallStaticVoidMethod(zygoteInitClass,
setApiBlackListApiMethod, fakeArray);

env->DeleteLocalRef(fakeStr);
env->DeleteLocalRef(fakeArray);
ALOGD("fakeapi success!");
return true;
}

jint JNI_OnLoad(JavaVM* vm, void* reserved) {
UnionJNIEnvToVoid uenv;
uenv.venv = NULL;
jint result = -1;
JNIEnv* env = NULL;

ALOGD("JNI_OnLoad");

if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_6) != JNI_OK) {
ALOGE("ERROR: GetEnv failed");
goto bail;
}
env = uenv.env;

if (!setApiBlacklistExemptions(env)) {
ALOGE("failed");
goto bail;
}
result = JNI_VERSION_1_6;

bail:
return result;
}

通过 so加载 解除反射限制后,反射获取 dex 代码的位置

 

    public static long[] getLoadedDexPaths() {
        long[] dexPaths;
        try {
            // 获取当前应用的 PathClassLoader
            ClassLoader classLoader = MainActivity.class.getClassLoader();
            if (classLoader == null) return null;

// 反射获取 BaseDexClassLoader 的 pathList 字段
Field pathListField = ReflectHelper.getField(classLoader, "pathList");
Object pathList = pathListField.get(classLoader);

// 反射获取 DexPathList 的 dexElements 数组
Field dexElementsField = ReflectHelper.getField(pathList, "dexElements");
Object[] dexElements = (Object[]) dexElementsField.get(pathList);

// 遍历每个 Element 获取 dex 文件路径
for (Object element : dexElements) {
Field dexFileField = ReflectHelper.getField(element, "dexFile");
Object dexFile = dexFileField.get(element);

Log.d(TAG, "getLoadedDexPaths: "+dexFile.toString());

if (dexFile != null) {
Field fileField = dexFile.getClass().getDeclaredField("mInternalCookie");

Field[] declaredFields = dexFile.getClass().getDeclaredFields();
for (Field declaredField : declaredFields) {
// Log.d("TAG", "dexFile: "+declaredField.getName());
}

fileField.setAccessible(true);
long[] file = (long[]) fileField.get(dexFile);
return file;
}
}
} catch (Exception e) {
Log.e("DexUtils", "Error getting dex paths", e);
}
return null;
}

然后在jni中获取地址,在这个过程中我获取的地址是dexfile的地址,它的成员变量指向dex

 

所以对获取指针+1 就是dex在内存中的地址了。

extern "C"
JNIEXPORT jboolean JNICALL
Java_com_calvin_sigcheck_MainActivity_checkMemDex(JNIEnv *env, jobject thiz) {
    // TODO: implement checkMemDex()

jclass jclass1= env->FindClass("com/calvin/sigcheck/MainActivity");
if (jclass1 == nullptr) {
__android_log_print(ANDROID_LOG_INFO, "checkMemDex", "jclass1 is null");
return false;
}
jmethodID jmethodID1 =env->GetStaticMethodID(jclass1, "getLoadedDexPaths", "()[J");
// 3. 调用静态方法
jlongArray result = (jlongArray)env->CallStaticObjectMethod(jclass1, jmethodID1);
if (result == NULL) {
__android_log_print(ANDROID_LOG_INFO,"checkMemDexcheckMemDex","Failed to call getLoadedDexPathsn");
return false;
}

int len = env->GetArrayLength(result);

// 2. 创建本地 long 数组
long *nativeArray = new long[len];

// 3. 将 jlongArray 中的元素复制到本地数组
env->GetLongArrayRegion(result, 0, len, nativeArray);
long crc =0;
for (int i = 0; i < len; i++) {
if(nativeArray[i] != 0){
DexHeader * dexHeader = (DexHeader *) (*((long *)nativeArray[i] +1));
__android_log_print(ANDROID_LOG_INFO, "checkMemDex", "%x",dexHeader->checksum);
crc += dexHeader->checksum;
}
}
__android_log_print(ANDROID_LOG_INFO, "checkMemDex", "%ld",crc);
if(!(crc - 6376092432)){ //这里修改 获取的crc 改为你自己的
__android_log_print(ANDROID_LOG_INFO, "checkMemDex", "crc is ok");
return true;
}
return false;
}

到这里其实我做的并不完善,你可以动态的对内存进行crc校验,而我这只获取了 dex的头文件的值。

当然你有其他其实妙想可以一起交流!

这里测试 np的最强过签名 FancyBpysss

还好!确实是检测出来了(不然研究这麽久连一键去签名都没过,有点没面子)

然后观察有些去签名方式修改的是AppComponentFactory 来进行初始化hook,那么我们来进行检测这个也可以的。

这里只做简单的判断未深入(有代码的兄弟可以在下面发出来,给大伙乐呵乐呵)

  private boolean checkAppComponentFactory() {
        String appComponentFactory = getmyAppComponentFactory();
        if(appComponentFactory!=null){
            Log.d(TAG, "检测到当前AppComponentFactory:"+appComponentFactory);
            return appComponentFactory.equals("androidx.core.app.CoreComponentFactory");
        }
        return false;
    }
    private String getmyAppComponentFactory(){
    try {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            return getmyApplication().getApplicationInfo().appComponentFactory;
        }else {
            return null;
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

剩下svc的部分,珍惜大佬已经讲的太清楚了。

https://bbs.kanxue.com/thread-278982.htm

熟读珍惜大佬的文章,如果你看完还是不太清楚可以买大佬的网课补补基础嘛。

svc部分的代码:

#define STR_HELPER(x) #x
#define STR(x) STR_HELPER(x)

intptr_t openAt(intptr_t fd, const char *path, intptr_t flag) {
#if defined(__arm__)
intptr_t r;
asm volatile(
#ifndef OPTIMIZE_ASM
"mov r0, %1nt"
"mov r1, %2nt"
"mov r2, %3nt"
#endif

"mov ip, r7nt"
".cfi_register r7, ipnt"
"mov r7, #" STR(__NR_openat) "nt"
"svc #0nt"
"mov r7, ipnt"
".cfi_restore r7nt"

#ifndef OPTIMIZE_ASM
"mov %0, r0nt"
#endif
: "=r" (r)
: "r" (fd), "r" (path), "r" (flag));
return r;
#elif defined(__aarch64__)
intptr_t r;
long syscall_number = 56;
asm volatile(
#ifndef OPTIMIZE_ASM
"mov x0, %1nt"
"mov x1, %2nt"
"mov x2, %3nt"
#endif
"mov x8, %4nt"
"svc #0nt"
#ifndef OPTIMIZE_ASM
"mov %0, x0nt"
#endif
: "=r" (r)
: "r" (fd), "r" (path), "r" (flag), "r" (syscall_number));
return r;
#else
return (intptr_t) syscall(__NR_openat, fd, path, flag);
#endif
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_calvin_sigcheck_MainActivity_openAt(JNIEnv *env, jobject thiz,
jstring package_resource_path) {
const char* p = env->GetStringUTFChars(package_resource_path, 0);

__android_log_print(ANDROID_LOG_INFO, "openAt", "path=%s", p);
intptr_t fd = openAt(AT_FDCWD, p, O_RDONLY);
env->ReleaseStringUTFChars(package_resource_path, p);
return fd;
}

extern "C"
JNIEXPORT jstring JNICALL
Java_com_calvin_sigcheck_MainActivity_checkFD(JNIEnv *env, jobject thiz, jint fd) {
// TODO: implement checkFD()
char fdPath[1024] = {0};
char buffer[1024] = {0};

sprintf(fdPath, "/proc/self/fd/%d", (int )fd);
int len = syscall(78, AT_FDCWD, fdPath, buffer, PATH_MAX);

if (len > 0) {
buffer[len] = '�';
__android_log_print(ANDROID_LOG_INFO, "checkFD", "fd=%d, path=%s", (int )fd, buffer);

return env->NewStringUTF(buffer);
}

return env->NewStringUTF("失败");

}

private byte[] signatureFromSVC() {

int aa = openAt(getPackageResourcePath());

fd=aa;
try (ParcelFileDescriptor fd = ParcelFileDescriptor.adoptFd(aa);

ZipInputStream zis = new ZipInputStream(new FileInputStream(fd.getFileDescriptor()))) {
path = checkFD(aa);
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (entry.getName().matches("(META-INF/.*)\.(RSA|DSA|EC)")) {
CertificateFactory certFactory = CertificateFactory.getInstance("X509");
X509Certificate x509Cert = (X509Certificate) certFactory.generateCertificate(zis);
return x509Cert.getEncoded();
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

这里就基本完成了 app的签名校验,欢迎大佬分享新的角度新的思路,一起进步!这里懒得用自己实现的svc了

攻击方法

第一 io重定向 :

https://bbs.kanxue.com/thread-273160.htm

https://bbs.kanxue.com/thread-285339-1.htm

这里的理论基础很好!熟读即可实现 对open相关的svc hook

第二Creator的检测:

由于hook的实现都是由代理的方式替换的,会留下很多的痕迹。

最好进行入侵式hook (类似 xposed )  就不会检测出来了,做好这些 基本就实现比较完美的去签名。

 

通过这些理论知识,我将 mt开源的去签名方式进行修改实现的svc的hook,对22年的360加固进行测试,完成了去签名,也算是 对22年那时候的我一个满意的答复!

 

工具实现了svc 的hook,未对 Creator 进行隐藏,进行去签名,会卡一会儿,完成后 在 download 目录里。

 

工具无法上传,放在github里了:
https://github.com/ywl20020421/-AndroidReverse/tree/main/%E7%AD%BE%E5%90%8D%E6%A0%A1%E9%AA%8C%E7%9B%B8%E5%85%B3

另外附上我编写的签名检验 app未混淆(点击文章底部查看原文,原帖附件中),大家可以反编译,希望大家能使用自己编写的去签名软件过掉它,而不是修改显示的ui。

最后我还有问题想求教大佬们:基于SECCOMP的SVC指令拦截,如何实现仿真? 如何对svc 指令hook的检测?

安卓签名校验-探讨

看雪ID:逆天而行

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

*本文为看雪论坛优秀文章,由 逆天而行 原创,转载请注明来自看雪社区

原文始发于微信公众号(看雪学苑):安卓签名校验-探讨

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年3月3日19:38:39
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   安卓签名校验-探讨https://cn-sec.com/archives/3790788.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息