目錄

前言
隨著技術(shù)的發(fā)展,APP的破解技術(shù)也越來越成熟,因此為了防止APP被破解我們可以進(jìn)行一系列安全方面的檢測(cè)和加固,讓破解人員的破解成本增加,這樣的話就可以在一定程度上防止APP被破解
代碼實(shí)現(xiàn)
1.模擬器檢測(cè)
大部分破解人員調(diào)試APP一般都是在模擬器上進(jìn)行,因此首先我們先檢測(cè)模擬器,這里為了偽造檢測(cè)的方法,我把設(shè)置Activity布局的邏輯也加在了檢測(cè)方法中,另外為了提升安全等級(jí),我將檢測(cè)的函數(shù)在NDK層實(shí)現(xiàn),同時(shí)檢測(cè)后的彈窗也在NDK層創(chuàng)建,這樣可以很大程度誤導(dǎo)破解人員,具體如下:
首先我們創(chuàng)建一個(gè)工具類,用于檢測(cè),我們需要傳入布局文件和Activity對(duì)象,目的是為了在NDK層設(shè)置布局加以混淆視線
public class CheckUtil {
static {
System.loadLibrary("native-lib");
}
public native static void init(int layoutId, Activity activity);
}
然后我們創(chuàng)建BaseActivity,其子類需要實(shí)現(xiàn)getLayoutId()方法返回布局文件
public abstract class BaseActivity extends AppCompatActivity {
@LayoutRes
protected abstract int getLayoutId();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
CheckUtil.init(getLayoutId(),this);
}
}
然后我們的邏輯大部分都是用C語言實(shí)現(xiàn),這里大體邏輯為先判斷是否在模擬器上運(yùn)行,如果是的話就彈窗并且在彈窗關(guān)閉的監(jiān)聽事件中退出APP
#include <jni.h>
#include <string>
#include "android/log.h"
#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, "安全", __VA_ARGS__))
enum DialogType{
EMULATOR,//模擬器
JKSERROR,//簽名錯(cuò)誤
WIFIPROXY,//WIFI代理
};
void showDialog(JNIEnv *env,DialogType dialogType,jobject activity){
std::string messageText = "";
switch (dialogType) {
case DialogType::EMULATOR:
messageText = "請(qǐng)不要在模擬器上運(yùn)行";
break;
case DialogType::JKSERROR:
messageText = "請(qǐng)使用正版應(yīng)用";
break;
case DialogType::WIFIPROXY:
messageText = "請(qǐng)不要使用網(wǎng)絡(luò)代理";
break;
}
//創(chuàng)建AlertDialog.Builder
jclass builderClazz = env->FindClass("android/app/AlertDialog$Builder");
jmethodID initMethodID = env->GetMethodID(builderClazz,"<init>","(Landroid/content/Context;)V");
jobject builder = env->NewObject(builderClazz,initMethodID,activity);
jmethodID setPositiveButtonMethodID = env->GetMethodID(builderClazz,"setPositiveButton","(Ljava/lang/CharSequence;Landroid/content/DialogInterface$OnClickListener;)Landroid/app/AlertDialog$Builder;");
//設(shè)置確認(rèn)按鈕和點(diǎn)擊事件
jclass onClickClazz = env->FindClass("com/itfitness/safedemo/dialog/MDialogClickListener");
jmethodID onClickInitMethodID = env->GetMethodID(onClickClazz,"<init>", "()V");
jobject onClickListener = env->NewObject(onClickClazz,onClickInitMethodID);
std::string buttonText = "確定";
env->CallObjectMethod(builder,setPositiveButtonMethodID,env->NewStringUTF(buttonText.c_str()),onClickListener);
//設(shè)置標(biāo)題
jmethodID setTitleMethodID = env->GetMethodID(builderClazz,"setTitle","(Ljava/lang/CharSequence;)Landroid/app/AlertDialog$Builder;");
std::string titleText = "提示";
env->CallObjectMethod(builder,setTitleMethodID,env->NewStringUTF(titleText.c_str()));
//設(shè)置提示內(nèi)容
jmethodID setMessageMethodID = env->GetMethodID(builderClazz,"setMessage","(Ljava/lang/CharSequence;)Landroid/app/AlertDialog$Builder;");
env->CallObjectMethod(builder,setMessageMethodID,env->NewStringUTF(messageText.c_str()));
//顯示彈窗
jmethodID createMethodID = env->GetMethodID(builderClazz,"create",
"()Landroid/app/AlertDialog;");
jobject alertDialog = env->CallObjectMethod(builder,createMethodID);
jclass alertDialogClazz = env->GetObjectClass(alertDialog);
//設(shè)置Dialog關(guān)閉的監(jiān)聽
jmethodID setOnDismissListenerMethodID = env->GetMethodID(alertDialogClazz,"setOnDismissListener",
"(Landroid/content/DialogInterface$OnDismissListener;)V");
jclass onDismissListenerClazz = env->FindClass("com/itfitness/safedemo/dialog/DialogDismissListener");
jmethodID onDismissListenerInitMethodID = env->GetMethodID(onDismissListenerClazz,"<init>", "()V");
jobject onDismissListener = env->NewObject(onDismissListenerClazz,onDismissListenerInitMethodID);
env->CallVoidMethod(alertDialog,setOnDismissListenerMethodID,onDismissListener);
//顯示
jmethodID showMethodID = env->GetMethodID(alertDialogClazz,"show", "()V");
env->CallVoidMethod(alertDialog,showMethodID);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_itfitness_safedemo_utils_CheckUtil_init(JNIEnv *env, jclass clazz, jint layout_id,
jobject activity) {
//設(shè)置布局文件
jclass activityClazz = env->GetObjectClass(activity);
//獲取Java層方法的id
char* methodName = "setContentView";
char* methodSig = "(I)V";
jmethodID setContentViewMethodId = env->GetMethodID(activityClazz,methodName,methodSig);
//調(diào)用方法
env->CallVoidMethod(activity, setContentViewMethodId,layout_id);
//檢測(cè)是否是模擬器
jclass deviceUtilsClazz = env->FindClass("com/blankj/utilcode/util/DeviceUtils");
jmethodID isEmulatorMethodId = env->GetStaticMethodID(deviceUtilsClazz,"isEmulator", "()Z");
jboolean isEmulator = env->CallStaticBooleanMethod(deviceUtilsClazz,isEmulatorMethodId);
if(isEmulator){
showDialog(env,DialogType::EMULATOR,activity);
return;
}
}
按鈕點(diǎn)擊和彈窗關(guān)閉的監(jiān)聽事件如下
public class DialogDismissListener implements DialogInterface.OnDismissListener{
@Override
public void onDismiss(DialogInterface dialog) {
AppUtils.exitApp();
}
}
public class MDialogClickListener implements DialogInterface.OnClickListener {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
}
然后我們創(chuàng)建MainActivity
public class MainActivity extends BaseActivity {
@Override
protected int getLayoutId() {
return R.layout.activity_main;
}
}
運(yùn)行APP效果如下(分別為模擬器和真機(jī))


2.簽名校驗(yàn)
假如某些破解人員發(fā)現(xiàn)模擬器不能運(yùn)行,由于APP破解價(jià)值較高,他可能會(huì)繼續(xù)使用真機(jī)進(jìn)行調(diào)試,那么他在反編譯代碼并重新打包的過程中必定會(huì)對(duì)APK進(jìn)行重新簽名,因此接下來我們就對(duì)APK的簽名進(jìn)行校驗(yàn),我們的校驗(yàn)邏輯同樣在NDK層,我們?cè)谀M器檢測(cè)的邏輯下面加入簽名校驗(yàn),在這之前我們先通過命令獲取我們APK簽名的SHA-1值,如下
keytool -list -v -keystore safedemo.jks

然后我們加入檢測(cè)邏輯
const char * SHA1 = "B5:AA:30:95:6A:AA:67:F3:74:FB:CB:91:A6:1C:A2:E2:A8:61:87:8B";
extern "C"
JNIEXPORT void JNICALL
Java_com_itfitness_safedemo_utils_CheckUtil_init(JNIEnv *env, jclass clazz, jint layout_id,
jobject activity) {
...省略部分代碼
//檢測(cè)簽名是否正確
jclass appUtilsClazz = env->FindClass("com/blankj/utilcode/util/AppUtils");
jmethodID getAppSignaturesSHA1MethodId = env->GetStaticMethodID(appUtilsClazz,"getAppSignaturesSHA1", "()Ljava/util/List;");
jobject sha1ArrayList = env->CallStaticObjectMethod(appUtilsClazz,getAppSignaturesSHA1MethodId);
jclass listClazz = env->FindClass("java/util/List");
jmethodID getMethodId = env->GetMethodID(listClazz,"get", "(I)Ljava/lang/Object;");
jstring javaSha1 = (jstring)env->CallObjectMethod(sha1ArrayList,getMethodId,0);
char * cSha1 = (char*)env->GetStringUTFChars(javaSha1,0);
LOGE("%s",cSha1);
//將獲取到的SHA-1值與正確簽名的SHA-1值比較
if(strcmp(cSha1,SHA1) != 0){
showDialog(env,DialogType::JKSERROR,activity);
return;
}
}
運(yùn)行APP效果如下(分別為正版簽名和盜版簽名)


3.WiFi代理校驗(yàn)
有時(shí)破解人員也會(huì)通過設(shè)置WiFi代理來截取手機(jī)的數(shù)據(jù),因此這里我們也檢測(cè)下是否有WIFI代理,如下所示,我們創(chuàng)建了一個(gè)檢測(cè)代理的工具類,當(dāng)然最好也是通過C語言實(shí)現(xiàn),不過這里我暫時(shí)就用Java代碼實(shí)現(xiàn)
public class WifiUtil {
/*
* 判斷設(shè)備 是否使用代理上網(wǎng)
* */
public static boolean isWifiProxy(Context context) {
// 是否大于等于4.0
final boolean IS_ICS_OR_LATER = Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;
String proxyAddress;
int proxyPort;
if (IS_ICS_OR_LATER) {
proxyAddress = System.getProperty("http.proxyHost");
String portStr = System.getProperty("http.proxyPort");
proxyPort = Integer.parseInt((portStr != null ? portStr : "-1"));
} else {
proxyAddress = android.net.Proxy.getHost(context);
proxyPort = android.net.Proxy.getPort(context);
}
return (!TextUtils.isEmpty(proxyAddress)) && (proxyPort != -1);
}
}
然后我們?cè)贘NI函數(shù)中加入邏輯
extern "C"
JNIEXPORT void JNICALL
Java_com_itfitness_safedemo_utils_CheckUtil_init(JNIEnv *env, jclass clazz, jint layout_id,
jobject activity) {
...省略部分代碼
//檢測(cè)是否使用了WIFI代理
jclass wifiUtilsClazz = env->FindClass("com/itfitness/safedemo/utils/WifiUtil");
jmethodID isWifiProxyMethodId = env->GetStaticMethodID(wifiUtilsClazz,"isWifiProxy",
"(Landroid/content/Context;)Z");
jboolean isWifiProxy = env->CallStaticBooleanMethod(wifiUtilsClazz,isWifiProxyMethodId,activity);
if(isWifiProxy){
showDialog(env,DialogType::WIFIPROXY,activity);
return;
}
}
運(yùn)行APP效果如下(分別為有代理和無代理)



具體的代理設(shè)置方法可以參考這篇文章:charles連接手機(jī)抓包
4.代碼混淆
接下來我們?yōu)榱俗尨a更不易閱讀可以進(jìn)行代碼混淆
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
signingConfig signingConfigs.release
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
不過在混淆代碼的時(shí)候要注意將JNI函數(shù)用到的類取消混淆
-keep class com.itfitness.safedemo.utils.** { *; }
-keep class com.itfitness.safedemo.dialog.** { *; }
-keep class com.blankj.utilcode.util.** { *; }
5.安全加固
最后我們?yōu)榱烁颖kU(xiǎn),同時(shí)也是為了給破解人員再增加破解難度,這時(shí)我們可以進(jìn)行安全加固,這里我知道的免費(fèi)加固有:愛加密和360加固,如果認(rèn)為免費(fèi)的不保險(xiǎn)可以使用付費(fèi)的,完成以上步驟后,一般情況下如果沒有特別深的仇恨或是APP價(jià)值與破解成本相比還是很高的話一般是沒人再去破解了
結(jié)語
我現(xiàn)在還在學(xué)習(xí)中,假如有哪些地方寫的不好還請(qǐng)大家?guī)臀抑刚?,謝謝大家