基础签名校验-建立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) {
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
2025-02-19 11:46:25.628 22548-22548 风控 com.calvin.sigcheck I android.content.pm.PackageInfo$1
2025-02-19 11:46:25.628 22548-22548 风控 com.calvin.sigcheck I android.content.pm.PackageInfo$1
2025-02-19 11:46:25.628 22548-22548 风控 com.calvin.sigcheck I android.content.pm.IPackageManager$Stub$Proxy
2025-02-19 11:46:25.628 22548-22548 风控 com.calvin.sigcheck I android.content.pm.PackageManager
2025-02-19 11:46:25.628 22548-22548 风控 com.calvin.sigcheck I android.content.pm.PackageManager
2025-02-19 11:46:25.628 22548-22548 风控 com.calvin.sigcheck I android.content.pm.PackageManager$2
2025-02-19 11:46:25.628 22548-22548 风控 com.calvin.sigcheck I android.content.pm.PackageManager$2
2025-02-19 11:46:25.628 22548-22548 风控 com.calvin.sigcheck I android.app.PropertyInvalidatedCache
2025-02-19 11:46:25.628 22548-22548 风控 com.calvin.sigcheck I android.content.pm.PackageManager
2025-02-19 11:46:25.628 22548-22548 风控 com.calvin.sigcheck I android.app.ApplicationPackageManager
2025-02-19 11:46:25.628 22548-22548 风控 com.calvin.sigcheck I android.app.ApplicationPackageManager
2025-02-19 11:46:25.629 22548-22548 风控 com.calvin.sigcheck I com.calvin.sigcheck.MainActivity
从堆栈来讲 ,我 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);
cache.getClass().getMethod("clear").invoke(cache);
} catch (Throwable ignored) {
}
try {
Map, ?> mCreators = (Map, ?>) findField(Parcel.class, "mCreators").get(null);
mCreators.clear();
} catch (Throwable ignored) {
}
try {
Map, ?> sPairedCreators = (Map, ?>) findField(Parcel.class, "sPairedCreators").get(null);
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("__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 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;
}
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) {
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 {
ClassLoader classLoader = MainActivity.class.getClassLoader();
if (classLoader == null) return null;
Field pathListField = ReflectHelper.getField(classLoader, "pathList");
Object pathList = pathListField.get(classLoader);
Field dexElementsField = ReflectHelper.getField(pathList, "dexElements");
Object[] dexElements = (Object[]) dexElementsField.get(pathList);
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) {
}
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) {
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");
jlongArray result = (jlongArray)env->CallStaticObjectMethod(jclass1, jmethodID1);
if (result == NULL) {
__android_log_print(ANDROID_LOG_INFO,"checkMemDexcheckMemDex","Failed to call getLoadedDexPaths\n");
return false;
}
int len = env->GetArrayLength(result);
long *nativeArray = new long[len];
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)){
__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, %1\n\t"
"mov r1, %2\n\t"
"mov r2, %3\n\t"
#endif
"mov ip, r7\n\t"
".cfi_register r7, ip\n\t"
"mov r7, #" STR(__NR_openat) "\n\t"
"svc #0\n\t"
"mov r7, ip\n\t"
".cfi_restore r7\n\t"
#ifndef OPTIMIZE_ASM
"mov %0, r0\n\t"
#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, %1\n\t"
"mov x1, %2\n\t"
"mov x2, %3\n\t"
#endif
"mov x8, %4\n\t"
"svc #0\n\t"
#ifndef OPTIMIZE_ASM
"mov %0, x0\n\t"
#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) {
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] = '\0';
__android_log_print(ANDROID_LOG_INFO, "checkFD", "fd=%d, path=%s", (int )fd, buffer);
return env->NewStringUTF(buffer);