NDK应用
卸载反馈 linux 层 -feedback
- fork 一个进程
- 监听自身应用的/data/data/package/目录是否存在
- 通过 linux 中的文件监听,inotify 监听 linux 中的文件状态
- 注意覆盖升级的问题
- 调用 am 命令跳转到网页
Java 层的 FileObserver 中的就是 inotify 机制
分析 AM.java
静默安装
SO 签名校验防被逆向盗用
- valid.cpp
#include <stdio.h>
#include <stdlib.h>
#include <jni.h>
#include <android/log.h>
#define TAG "qsbk.jni"
#define LOGD(...) ((void)__android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__))
#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__))
#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__))
// 内置自己APP的签名信息
const char *app_sha1 = "6DAA48C22BEA90A5928CCA3096D0E21967E74CB6";
const char hexcode[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
// 反射获取应用的签名
char *getSha1FromSo(JNIEnv *env, jobject context_object) {
//上下文对象
jclass context_class = env->GetObjectClass(context_object);
//反射获取PackageManager
jmethodID methodId = env->GetMethodID(context_class, "getPackageManager", "()Landroid/content/pm/PackageManager;");
jobject package_manager = env->CallObjectMethod(context_object, methodId);
if (package_manager == NULL) {
LOGD("[getSha1FromSo] package_manager is NULL!!!");
return NULL;
}
//反射获取包名
methodId = env->GetMethodID(context_class, "getPackageName", "()Ljava/lang/String;");
jstring package_name = (jstring) env->CallObjectMethod(context_object, methodId);
if (package_name == NULL) {
LOGD("[getSha1FromSo] package_name is NULL!!!");
return NULL;
}
env->DeleteLocalRef(context_class);
//获取PackageInfo对象
jclass pack_manager_class = env->GetObjectClass(package_manager);
methodId = env->GetMethodID(pack_manager_class, "getPackageInfo",
"(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
env->DeleteLocalRef(pack_manager_class);
jobject package_info = env->CallObjectMethod(package_manager, methodId, package_name, 0x40);
if (package_info == NULL) {
LOGD("[getSha1FromSo] getPackageInfo() is NULL!!!");
return NULL;
}
env->DeleteLocalRef(package_manager);
//获取签名信息
jclass package_info_class = env->GetObjectClass(package_info);
jfieldID fieldId = env->GetFieldID(package_info_class, "signatures", "[Landroid/content/pm/Signature;");
env->DeleteLocalRef(package_info_class);
jobjectArray signature_object_array = (jobjectArray) env->GetObjectField(package_info, fieldId);
if (signature_object_array == NULL) {
LOGD("[getSha1FromSo] signature is NULL!!!");
return NULL;
}
jobject signature_object = env->GetObjectArrayElement(signature_object_array, 0);
env->DeleteLocalRef(package_info);
//签名信息转换成sha1值
jclass signature_class = env->GetObjectClass(signature_object);
methodId = env->GetMethodID(signature_class, "toByteArray", "()[B");
env->DeleteLocalRef(signature_class);
jbyteArray signature_byte = (jbyteArray) env->CallObjectMethod(signature_object, methodId);
jclass byte_array_input_class = env->FindClass("java/io/ByteArrayInputStream");
methodId = env->GetMethodID(byte_array_input_class, "<init>", "([B)V");
jobject byte_array_input = env->NewObject(byte_array_input_class, methodId, signature_byte);
jclass certificate_factory_class = env->FindClass("java/security/cert/CertificateFactory");
methodId = env->GetStaticMethodID(certificate_factory_class, "getInstance",
"(Ljava/lang/String;)Ljava/security/cert/CertificateFactory;");
jstring x_509_jstring = env->NewStringUTF("X.509");
jobject cert_factory = env->CallStaticObjectMethod(certificate_factory_class, methodId, x_509_jstring);
methodId = env->GetMethodID(certificate_factory_class, "generateCertificate",
("(Ljava/io/InputStream;)Ljava/security/cert/Certificate;"));
jobject x509_cert = env->CallObjectMethod(cert_factory, methodId, byte_array_input);
env->DeleteLocalRef(certificate_factory_class);
jclass x509_cert_class = env->GetObjectClass(x509_cert);
methodId = env->GetMethodID(x509_cert_class, "getEncoded", "()[B");
jbyteArray cert_byte = (jbyteArray) env->CallObjectMethod(x509_cert, methodId);
env->DeleteLocalRef(x509_cert_class);
jclass message_digest_class = env->FindClass("java/security/MessageDigest");
methodId = env->GetStaticMethodID(message_digest_class, "getInstance",
"(Ljava/lang/String;)Ljava/security/MessageDigest;");
jstring sha1_jstring = env->NewStringUTF("SHA1");
jobject sha1_digest = env->CallStaticObjectMethod(message_digest_class, methodId, sha1_jstring);
methodId = env->GetMethodID(message_digest_class, "digest", "([B)[B");
jbyteArray sha1_byte = (jbyteArray) env->CallObjectMethod(sha1_digest, methodId, cert_byte);
env->DeleteLocalRef(message_digest_class);
//转换成char
jsize array_size = env->GetArrayLength(sha1_byte);
jbyte *sha1 = env->GetByteArrayElements(sha1_byte, NULL);
char *hex_sha = new char[array_size * 2 + 1];
for (int i = 0; i < array_size; ++i) {
hex_sha[2 * i] = hexcode[((unsigned char) sha1[i]) / 16];
hex_sha[2 * i + 1] = hexcode[((unsigned char) sha1[i]) % 16];
}
hex_sha[array_size * 2] = '\0';
LOGD("[getSha1FromSo]获取当前APP的签名SHA1:%s ", hex_sha);
return hex_sha;
}
// 比较签名是否一致
jint checkAppSignatureValidity(JNIEnv *env, char *sha1) {
//比较签名
if (strcmp(sha1, app_sha1) == JNI_OK) {
LOGD("[checkAppSignatureValidity]SO签名验证成功");
return JNI_OK;
}
LOGE("[checkAppSignatureValidity]SO签名验证失败");
return JNI_ERR;
}
// 反射获取Application
static jobject getApplication(JNIEnv *env) {
jobject application = NULL;
jclass activity_thread_clz = env->FindClass("android/app/ActivityThread");
if (activity_thread_clz != NULL) {
jmethodID currentApplication = env->GetStaticMethodID(
activity_thread_clz, "currentApplication", "()Landroid/app/Application;");
if (currentApplication != NULL) {
application = env->CallStaticObjectMethod(activity_thread_clz, currentApplication);
} else {
LOGE("Cannot find method: currentApplication() in ActivityThread.");
}
env->DeleteLocalRef(activity_thread_clz);
} else {
LOGE("Cannot find class: android.app.ActivityThread");
}
return application;
}
// 在so加载时,进行签名的校验
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
LOGE("[JNI_OnLoad] JNI_VERSION_1_4 Not OK!!!");
return JNI_ERR;
}
if (checkAppSignatureValidity(env, getSha1FromSo(env, getApplication(env))) == JNI_OK) {
LOGI("[JNI_OnLoad] JNI SO签名校验成功");
return JNI_VERSION_1_4;
}
LOGE("[JNI_OnLoad] 签名不一致 JNI_ERR!");
return JNI_ERR;
}
- qbvoicechatsecurity.cpp
#include <jni.h>
#include <string>
#include "valid.cpp"
extern "C"
JNIEXPORT jstring JNICALL
Java_qsbk_app_voice_common_net_SecurityUtils_getSignaturesSha1(
JNIEnv *env,
jclass type/*,
jobject contextObject*/) {
char *signature = getSha1FromSo(env, getApplication(env));
return env->NewStringUTF(signature);
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_qsbk_app_voice_common_net_SecurityUtils_checkSha1(
JNIEnv *env,
jclass type
/*,jobject contextObject*/) {
char *sha1 = getSha1FromSo(env, getApplication(env));
jboolean result = checkAppSignatureValidity(env, sha1);
return result;
}
- Java 代码
public final class SecurityUtils {
/**
* A native method that is implemented by the 'security' native library,
* which is packaged with this application.
*/
public static native String getSignaturesSha1();
public static native boolean checkSha1();
}
- CMakeLists.txt
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
qbvoicechatsecurity-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/qbvoicechatsecurity.cpp)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log)
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
qbvoicechatsecurity-lib
# Links the target library to the log library
# included in the NDK.
${log-lib})
set_target_properties(qbvoicechatsecurity-lib PROPERTIES OUTPUT_NAME "qbsec")
密钥保存
CMake simple highlighter 插件
Gradle 配置 cmake
android {
// ...
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
Java 层定义 native 方法
// me.hacket.assistant.samples.apps.inews.utils.SecurityUtils
public static native String getSecretKey();
编写 cpp
// src/main/cpp/qbvoicechatsecurity.cpp
#include <string>
#include <jni.h>
using namespace std;
#define LOGINFO(...) ((void)__android_log_print(ANDROID_LOG_INFO, "security", __VA_ARGS__))
#define LOGERROR(...) ((void)__android_log_print(ANDROID_LOG_ERROR, "security", __VA_ARGS__))
#ifdef __cplusplus
extern "C" // C++中以C的方式编译
{
JNIEXPORT jstring
JNICALL Java_me_hacket_assistant_samples_apps_inews_utils_SecurityUtils_getSecretKey
(JNIEnv *env,
jclass type) {
std::string hello = "1234567890";
return env->NewStringUTF(hello.c_str());
}
}
#endif
编写 CMakeLists.txt
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
qbvoicechatsecurity
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/qbvoicechatsecurity.cpp)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log)
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
qbvoicechatsecurity
# Links the target library to the log library
# included in the NDK.
${log-lib})
Java 层加载
public final class SecurityUtils {
// ...
static {
System.loadLibrary("qbvoicechatsecurity");
}
// ...
}
增量更新
增量更新解决方案及其 Kotlin 实现
增量更新和普通更新
- 普通更新:有新版本发布,将完整的包放到服务器里面,供客户端下载完整下载
- 增量更新
服务器在新版本 apk,在服务器和老版本生成一个差分包(apk.patch),供客户端下载;客户端下载后,在客户端本地和旧版本合成新版本的 apk,然后安装
多了个服务器一个差分,客户端合并操作
增量更新 apk 分析
- 手机淘宝
libBSPatch.so 57K - QQ 空间
- 爱奇艺
libbspatch.so 157K
BsPatch/BsDiff
http://www.daemonology.net/bsdiff/
bsdiff:
bspatch:
bzip2:/usr/bin 一般有这个工具
客户端合并过程
- 写一个带有 native 的类,并生产 c 头文件
- 引入 bspatch 的 c 文件
- 配置 CmakeLists.txt
- 在 bspatch.c 文件中实现合成操作
bspatch.c 的 main() 函数参数,第一个固定为 4,第二个参数为长度为 4 的数组(第 1 个为 log 输出,第 2 个为 oldpath,第 3 个为 newPath,第 4 个为 patchPath) - 解决多个 main 函数问题
技术点
- bspatch、bsdiff
- apk 安装到手机的一个过程
- 差分包一般小于 10M?
- 哈夫曼算法
- CmakeLists.txt 文件
编译器用的 llvm,Android.mk 用的是 gcc
tencent 插件化:tws framework
- HFS 简易网络服务器
- 用了混淆加密有啥影响
没影响,文件级的文件;算法基于文件的,差分合并后和你要升级的新 apk 的 md5 值一样
windows 生成文件的 md5 值
certutil -hashfile file MD5
增量更新解决方案
https://github.com/cundong/SmartAppUpdates
bsdiff bspatch
https://github.com/ha-excited/BigNews
Android 增量更新框架差分包升级
差分
BSDiff
http://www.daemonology.net/bsdiff/
DexDiff
dexDiff 是微信结合 Dex 文件格式设计的一个专门针对 Dex 的差分算法。根据 Dex 的文件格式,对两个 Dex 中每一项
数据进行差分记录。