Risk 設(shè)計(jì)初衷
隨著我們項(xiàng)目的用戶群體不斷壯大茁裙,漸漸的我們會(huì)從 Bugly 日志等地方發(fā)現(xiàn)一些灰產(chǎn)使用 Hook 析二、自動(dòng)化腳本等對(duì)我們應(yīng)用進(jìn)行數(shù)據(jù)的抓取、對(duì)正常用戶進(jìn)行騷擾與欺詐役电,我們希望能夠有一款框架能夠?qū)@些“非法用戶”進(jìn)行識(shí)別凶异,這就是我們 Risk 框架設(shè)計(jì)的初衷。
Risk 原理
Hook 檢測(cè)
典型框架:Xpatch看铆、Xposed徽鼎、太極
對(duì)于 Hook 來(lái)說(shuō)自然不得不提大名鼎鼎的 Xposed 由于他是免費(fèi)、開(kāi)源的,Xposed 已經(jīng)有了種種可以繞過(guò)常規(guī)檢測(cè)的方法否淤,由于市面上的檢測(cè)代碼并不準(zhǔn)確悄但,我們通過(guò)分析 Xposed 源碼,找出了以下這幾個(gè)方案進(jìn)行檢測(cè)石抡。
方案1:Class.forName()
我們通過(guò) bugly 上報(bào)上來(lái)的異常 Class 路徑檐嚣,人工篩選過(guò)后通過(guò)“配置中心”下發(fā)到 Risk 框架中通過(guò) Class.forName() 來(lái)進(jìn)行檢測(cè),這個(gè)方法較為準(zhǔn)確啰扛,但是非常依靠人力來(lái)做異常 Class 路徑的識(shí)別與下發(fā)嚎京,并且沒(méi)法第一時(shí)間發(fā)現(xiàn),只能作為后手方案隐解。方案2:com.android.internal.os.ZygoteInit
我們?nèi)绻ラ喿x Xposed 源碼鞍帝,可以發(fā)現(xiàn)他是搶在我們 App 的 ZygoteInit 初始化之前初始化的,那這樣我們就可以通過(guò)檢測(cè) exception 堆棧來(lái)進(jìn)行識(shí)別煞茫,但是需要注意帕涌,Hook 可以隱藏自身的信息,詳情參見(jiàn):利用Xposed躲過(guò)Xposed檢測(cè) 所以我們這邊利用了一個(gè)雙重檢測(cè)续徽,經(jīng)過(guò)線上的測(cè)試發(fā)現(xiàn)雙重檢測(cè)(指的是最底層的倆層堆是否都為 ZygoteInit 蚓曼,一般的 Hook 框架只隱藏最后一層堆)還是較為有效的,大部份 Hook 框架使用者并未發(fā)現(xiàn)這個(gè)檢測(cè)方案钦扭,代碼如下:
// 應(yīng)用都是從 zygoteInit 初始化出來(lái)的纫版,所以我們判斷最底層是否是 zygote 就可以判斷是否被hook了
if (!sZygoteInit.equals(exception.getStackTrace()[(exception.getStackTrace().length - 1)].getClassName())) {
if(sZygoteInit.equals(exception.getStackTrace()[(exception.getStackTrace().length - 2)].getClassName())){
checkCredit(isTrusted);
isTrusted = false;
next();
return;
}
}
-
方案3:Application.class.getSuperclass()
由于 Hook 存在二次打包后入侵 Application 進(jìn)行應(yīng)用內(nèi) Hook 的情況,這種框架十分難檢測(cè)土全,它的原理大致是這樣:二次打包目標(biāo)應(yīng)用捎琐,替換目標(biāo)應(yīng)用的 Application 并在替換后的 Application 的 static 方法塊寫上初始化 Hook 的相關(guān)代碼,這樣就能在第一次時(shí)間初始化 Hook 框架裹匙,所以我們需要校驗(yàn) Application 的完整性瑞凑,這里已線上項(xiàng)目為例,被二次打包前的代碼:
public class XjbApplication extends BaseApplication {
private static final String TAG = "XjbApplication";
private static XjbApplication instance;
private XjbApplicationHelper xjbAppHelper = XjbApplicationHelper.getInstance();
private Context mApplicationContext;
public XjbApplication() {
super();
instance = this;
Loger.init(BuildConfig.DEBUG);
Log.i(TAG, "APP instanced");
}
.........
二次打包后的代碼:
public class XjbApplication extends HookApplication {
private static final String TAG = "XjbApplication";
private static XjbApplication instance;
private XjbApplicationHelper xjbAppHelper = XjbApplicationHelper.getInstance();
private Context mApplicationContext;
public XjbApplication() {
super();
instance = this;
Loger.init(BuildConfig.DEBUG);
Log.i(TAG, "APP instanced");
}
.........
public class HookApplication extends Application {
static {
Hook.init();
}
.........
所以概页,根據(jù)以上的情況我們先校驗(yàn)代碼的完整性:
XjbApplication.class.getSuperclass();
BaseApplication.class.getSuperclass();
需要特別注意籽御,有些入侵式 Hook 框架會(huì)更改 AndroidManifest.xml 中聲明的 Application ,暫時(shí)還沒(méi)找到什么比較好的檢測(cè)方案惰匙。
多開(kāi)檢測(cè)
典型框架:virtualApp
關(guān)于多開(kāi)檢測(cè)網(wǎng)上的一些方案都十分有效技掏,難點(diǎn)是由于多開(kāi)框架眾多,我們需要集成進(jìn)大量的檢測(cè)代碼项鬼,下面分享倆個(gè)較為有效的方案
方案1:Context.getCacheDir()
VirtualApp哑梳、dkplugin 等框架在生成文件目錄的時(shí)候,往往生成的目錄很奇怪绘盟,例如
nativeLibraryDirectories=[/data/user/0/dkplugin.aix.ttr/virtual/data/user/0/com.xingjiabi.shengsheng/lib]
特別注意鸠真,檢測(cè) nativeLibraryDirectories 目錄十分有效方案2:/proc/self/maps
/proc/self/maps 中出現(xiàn)包含 /vbox/data/ 悯仙、 /shadow/data/ 、 /virtual/data/ 的動(dòng)態(tài)庫(kù)吠卷,則運(yùn)行在多開(kāi)環(huán)境下锡垄。由于許多多開(kāi)軟件都是開(kāi)源的,不排除某些大手子自己改名重新編譯祭隔。
模擬器檢測(cè)
模擬器檢測(cè)并無(wú)太多技巧货岭,主要檢測(cè) CPU 架構(gòu)、ROM 名稱疾渴、手機(jī)是否一直在充電中千贯、電池電量等。
但是模擬器的系統(tǒng)應(yīng)用都有一個(gè)特點(diǎn)程奠,就是它們的 nativeLibraryDir 最終目錄都是 x86丈牢,MuMu模擬器、逍遙模擬器瞄沙、藍(lán)疊模擬器、夜神模擬器慌核、雷電模擬器 都經(jīng)過(guò)驗(yàn)證距境,無(wú)一例外針對(duì)這個(gè)漏洞進(jìn)行檢測(cè),準(zhǔn)確率會(huì)比較高垮卓。
/**
* @author:楊浩
* 創(chuàng)建日期:2019-12-19
* 功能簡(jiǎn)介:用于檢測(cè)虛擬機(jī)的工具類
* aosp:Android Open-Source Project 一般虛擬機(jī)都是基于這個(gè)開(kāi)發(fā)的
* 目前能檢測(cè)到的模擬器有:MuMu模擬器垫桂、逍遙模擬器、藍(lán)疊模擬器粟按、夜神模擬器诬滩、雷電模擬器、480 * 800 分辨率的腳本
*/
public class AntiAospUtils {
private static final String SCAN_DEVICE_TIME = "s_aosp_device_time";
/**
* 開(kāi)始掃描設(shè)備信息
*
* @param accountId 賬號(hào)灭将,用于保存上一次掃描的時(shí)間疼鸟,每隔 3 天才會(huì)掃描一次,如果掃描到模擬器就上報(bào)
* @param contex
*/
public static void startScanDeviceInfo(final String accountId, final Context contex) {
startScanDeviceInfo(accountId, contex, null);
}
/**
* 開(kāi)始掃描設(shè)備信息
*
* @param accountId 賬號(hào)庙曙,用于保存上一次掃描的時(shí)間空镜,每隔 3 天才會(huì)掃描一次,如果掃描到模擬器就上報(bào)
* @param context
* @param scanDeviceListener 掃描完成回調(diào)
*/
public static void startScanDeviceInfo(final String accountId, final Context context, final ScanDeviceListener scanDeviceListener) {
ScanDevicePlanWrapper scanScreenInfo = new ScanDevicePlanWrapper(new ScanScreenInfo());
ScanDevicePlanWrapper scanAppInfo = new ScanDevicePlanWrapper(new ScanAppInfo());
ScanDevicePlanWrapper scanCpuInfo = new ScanDevicePlanWrapper(new ScanCpuInfo());
scanCpuInfo.setNextScanDevicePlanWrapper(scanAppInfo);
scanAppInfo.setNextScanDevicePlanWrapper(scanScreenInfo);
DeviceScanInfo deviceScanInfo = scanCpuInfo.scanDevice(context);
// 判斷是否可疑設(shè)備 或者是否模擬器設(shè)備捌朴,都需要上報(bào)
if (deviceScanInfo.isFaker() || deviceScanInfo.isBadDevice()) {
LogUploadUtil.postAospDeviceLog(deviceScanInfo);
}
if (scanDeviceListener != null) {
scanDeviceListener.onComplete(deviceScanInfo);
}
}
private static class ScanDevicePlanWrapper implements ScanDevicePlanAble {
/**
* 下一個(gè)掃描器
*/
@Nullable
public ScanDevicePlanWrapper mNextScanDevicePlanWrapper;
@NotNull
public ScanDevicePlanAble mScanDevicePlan;
public ScanDevicePlanWrapper(ScanDevicePlanAble scanDevicePlan) {
mScanDevicePlan = scanDevicePlan;
}
public void setNextScanDevicePlanWrapper(ScanDevicePlanWrapper nextScanDevicePlanWrapper) {
mNextScanDevicePlanWrapper = nextScanDevicePlanWrapper;
}
@Override
public DeviceScanInfo scanDevice(Context context) throws Exception {
@Nullable
DeviceScanInfo nextDeviceScanInfo = null;
if (mNextScanDevicePlanWrapper != null) {
nextDeviceScanInfo = mNextScanDevicePlanWrapper.scanDevice(context);
// 判斷是否需要掃描
if (!isAllScanInfo() && nextDeviceScanInfo.isBadDevice()) {
return nextDeviceScanInfo;
}
}
DeviceScanInfo currentDeviceScanInfo = mScanDevicePlan.scanDevice(context);
if (nextDeviceScanInfo != null) {
// 如果其他掃描器掃描出來(lái)有用的信息就保存下來(lái)
String scanInfoTemp = nextDeviceScanInfo.getScanInfo();
currentDeviceScanInfo.setScanInfo(scanInfoTemp + " || " + currentDeviceScanInfo.getScanInfo());
if (nextDeviceScanInfo.isBadDevice()) {
// 發(fā)現(xiàn)模擬器
currentDeviceScanInfo.setBadDevice(true);
} else if (nextDeviceScanInfo.isFaker()) {
// 發(fā)現(xiàn)疑似模擬器
currentDeviceScanInfo.setFaker(true);
}
}
return currentDeviceScanInfo;
}
@Override
public boolean isAllScanInfo() {
return mScanDevicePlan.isAllScanInfo();
}
}
private interface ScanDevicePlanAble {
/**
* 掃描設(shè)備
*
* @param context
* @return
* @throws Exception
*/
@NotNull
public DeviceScanInfo scanDevice(Context context) throws Exception;
/**
* 是否需要完整的掃描信息吴攒,因?yàn)檫@邊的掃描器是鏈?zhǔn)降? * return true 的情況下,會(huì)將所有的鏈?zhǔn)綊呙杵髋芤槐樯氨危瑸榈氖峭暾哪M器信息
* return false 的情況下洼怔,只要有其中一個(gè)掃描器掃描到信息,本掃描器將不掃描信息
*
* @return
*/
public boolean isAllScanInfo();
}
/**
* 掃描 cpu 的架構(gòu)信息
*/
private static class ScanCpuInfo implements ScanDevicePlanAble {
@Override
public DeviceScanInfo scanDevice(Context context) throws Exception {
if (checkDeviceForumX86()) {
return new DeviceScanInfo("scanCpuInfo:x86 == true", true);
} else {
return new DeviceScanInfo("scanCpuInfo:x86 == false", false);
}
}
/**
* cpu 架構(gòu)信息不重要左驾,如果之前其他掃描器已經(jīng)掃描到了镣隶,這里就不需要工作
*
* @return
*/
@Override
public boolean isAllScanInfo() {
return false;
}
}
/**
* 針對(duì) app 做掃描
* 模擬器的系統(tǒng)應(yīng)用都有一個(gè)特點(diǎn)泽台,就是它們的 nativeLibraryDir 最終目錄都是 x86
* MuMu模擬器、逍遙模擬器矾缓、藍(lán)疊模擬器怀酷、夜神模擬器、雷電模擬器 都經(jīng)過(guò)驗(yàn)證嗜闻,無(wú)一例外
* 針對(duì)這個(gè)漏洞進(jìn)行檢測(cè)蜕依,準(zhǔn)確率會(huì)比較高
*/
private static class ScanAppInfo implements ScanDevicePlanAble {
/**
* 模擬器身上的標(biāo)記
*/
private static final String BAD_TAG = "x86";
// --------------------- 需要掃描的包名 ---------------------
/**
* 撥打電話
*/
private final String CALL = "com.android.server.telecom";
/**
* 通訊錄
*/
private final String CONTACTS = "com.android.contacts";
/**
* 網(wǎng)頁(yè)渲染器
*/
private final String WEB_VIEW = "com.android.webview";
/**
* 系統(tǒng)設(shè)置
*/
private final String SYSTEM_SETTING = "com.android.settings";
/**
* Android 默認(rèn)的瀏覽器
*/
private final String SYSTEM_BROWSER = "com.android.browser";
/**
* 需要掃描的應(yīng)用包名
*/
private final String[] ALL_SCAN_PACKAGE_INFO = new String[]{CALL, CONTACTS, WEB_VIEW, SYSTEM_SETTING, SYSTEM_BROWSER};
@Override
public DeviceScanInfo scanDevice(Context context) throws Exception {
// 判斷是否掃描成功過(guò)
// 正常的手機(jī)不太可能一個(gè)應(yīng)用都沒(méi)有找到
// 如果出現(xiàn)這種情況的話,一般只有倆種可能琉雳,1样眠、系統(tǒng)沒(méi)給權(quán)限(默認(rèn)都是給的)2、被 Hook 了
// 這種情況下需要考慮一下這個(gè)設(shè)備是否是有問(wèn)題的了
boolean isScanDeviceComplete = false;
PackageManager packageManager = context.getPackageManager();
if (packageManager == null) {
return new DeviceScanInfo("scanPackageInfo:packageManager == null", false, isScanDeviceComplete);
}
for (String scanPackageInfo : ALL_SCAN_PACKAGE_INFO) {
try {
PackageInfo packageInfo = packageManager.getPackageInfo(scanPackageInfo, PackageManager.GET_ACTIVITIES);
if (packageInfo != null) {
String nativeLibraryDir = packageInfo.applicationInfo.nativeLibraryDir;
// 如果 nativeLibraryDir 沒(méi)有獲取到的話翠肘,非抽苁可疑
if (nativeLibraryDir != null) {
isScanDeviceComplete = true;
if (nativeLibraryDir.contains(BAD_TAG)) {
return new DeviceScanInfo("scanPackageInfo:" + scanPackageInfo + "." + BAD_TAG, true);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
return new DeviceScanInfo("scanPackageInfo:scanPackageInfo == null", false, !isScanDeviceComplete);
}
@Override
public boolean isAllScanInfo() {
return true;
}
}
/**
* 掃描屏幕的寬高與物理尺寸來(lái)區(qū)分模擬器
* 目前發(fā)現(xiàn)針對(duì)的腳本,都需要限定屏幕的尺寸束倍,就算他進(jìn)行了 hook 也不太可能針對(duì)獲取屏幕分辨率進(jìn)行處理
* 所以這里檢測(cè)屏幕分辨率
*/
private static class ScanScreenInfo implements ScanDevicePlanAble {
// --------------------- 可疑的屏幕分辨率 ---------------------
/**
* 貌似腳本會(huì)固定這個(gè)寬高被丧,先檢測(cè)看看
*/
private static final Integer SCREEN_WIDTH[] = new Integer[]{480, 540};
private static final Integer SCREEN_HEIGHT[] = new Integer[]{800, 960};
@Override
public DeviceScanInfo scanDevice(Context context) throws Exception {
DisplayMetrics dm = context.getResources().getDisplayMetrics();
int screenWidth = dm.widthPixels;
int screenHeight = dm.heightPixels;
// 判斷是否是可疑寬高
if (Arrays.asList(SCREEN_HEIGHT).contains(screenHeight) && Arrays.asList(SCREEN_WIDTH).contains(screenWidth)) {
// 計(jì)算屏幕物理尺寸
double diagonalPixels = Math.sqrt(Math.pow(screenWidth, 2) + Math.pow(screenHeight, 2));
double size = new BigDecimal(diagonalPixels / (160 * dm.density)).setScale(1, BigDecimal.ROUND_HALF_UP).doubleValue();
return new DeviceScanInfo("ScanScreenInfo:badPixel width-" + screenWidth + "、height-" + screenHeight + "-size:" + size, true);
}
return new DeviceScanInfo("ScanScreenInfo:normal《 " + "width-" + screenWidth + "绪妹、height-" + screenHeight + " 》", false);
}
@Override
public boolean isAllScanInfo() {
return true;
}
}
/**
* 掃描設(shè)備回調(diào)
*/
public interface ScanDeviceListener {
public void onComplete(DeviceScanInfo info);
}
/**
* 掃描的結(jié)果信息
*/
public static class DeviceScanInfo {
/**
* 掃描的結(jié)果
*/
private String mScanInfo = "";
/**
* 是否模擬器
*/
private boolean isBadDevice = false;
/**
* 是否是可疑的設(shè)備
*/
private boolean isFaker = false;
public DeviceScanInfo(String scanInfo, boolean isBadDevice, boolean isFaker) {
mScanInfo = scanInfo;
this.isBadDevice = isBadDevice;
this.isFaker = isFaker;
}
public DeviceScanInfo(String scanInfo, boolean isBadDevice) {
mScanInfo = scanInfo;
this.isBadDevice = isBadDevice;
}
public String getScanInfo() {
return mScanInfo;
}
public void setScanInfo(String scanInfo) {
mScanInfo = scanInfo;
}
public boolean isBadDevice() {
return isBadDevice;
}
public void setBadDevice(boolean badDevice) {
isBadDevice = badDevice;
}
public boolean isFaker() {
return isFaker;
}
public void setFaker(boolean faker) {
isFaker = faker;
}
@NonNull
@Override
public String toString() {
if (isBadDevice) {
return "BadDevice: " + isBadDevice + "甥桂。" + mScanInfo;
}
return "Faker: " + isFaker + "," + mScanInfo;
}
}
}
/**
* @author:楊浩 項(xiàng)目:haibaobase
* 創(chuàng)建日期:2019-09-03
* 功能簡(jiǎn)介:
*/
class DeviceUtils {
public static final String ABI_X86 = "x86";
public static final String ABI_MIPS = "mips";
public static enum ARCH {
Unknown, ARM, X86, MIPS, ARM64,
}
private static ARCH sArch = ARCH.Unknown;
// see include/uapi/linux/elf-em.h
private static final int EM_ARM = 40;
private static final int EM_386 = 3;
private static final int EM_MIPS = 8;
private static final int EM_AARCH64 = 183;
// /system/lib/libc.so
// XXX: need a runtime check
public static synchronized ARCH getMyCpuArch() {
byte[] data = new byte[20];
File libc = new File(Environment.getRootDirectory(), "lib/libc.so");
if (libc.canRead()) {
RandomAccessFile fp = null;
try {
fp = new RandomAccessFile(libc, "r");
fp.readFully(data);
int machine = (data[19] << 8) | data[18];
switch (machine) {
case EM_ARM:
sArch = ARCH.ARM;
break;
case EM_386:
sArch = ARCH.X86;
break;
case EM_MIPS:
sArch = ARCH.MIPS;
break;
case EM_AARCH64:
sArch = ARCH.ARM64;
break;
default:
Log.e("NativeBitmapFactory", "libc.so is unknown arch: " + Integer.toHexString(machine));
break;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fp != null) {
try {
fp.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
return sArch;
}
public static String get_CPU_ABI() {
return Build.CPU_ABI;
}
public static String get_CPU_ABI2() {
try {
Field field = Build.class.getDeclaredField("CPU_ABI2");
if (field == null)
return null;
Object fieldValue = field.get(null);
if (!(fieldValue instanceof String)) {
return null;
}
return (String) fieldValue;
} catch (Exception e) {
}
return null;
}
public static boolean supportABI(String requestAbi) {
String abi = get_CPU_ABI();
if (!TextUtils.isEmpty(abi) && abi.equalsIgnoreCase(requestAbi))
return true;
String abi2 = get_CPU_ABI2();
return !TextUtils.isEmpty(abi2) && abi.equalsIgnoreCase(requestAbi);
}
public static boolean supportX86() {
return supportABI(ABI_X86);
}
public static boolean supportMips() {
return supportABI(ABI_MIPS);
}
public static boolean isARMSimulatedByX86() {
ARCH arch = getMyCpuArch();
return !supportX86() && ARCH.X86.equals(arch);
}
public static boolean isMiBox2Device() {
String manufacturer = Build.MANUFACTURER;
String productName = Build.PRODUCT;
return manufacturer.equalsIgnoreCase("Xiaomi")
&& productName.equalsIgnoreCase("dredd");
}
public static boolean isMagicBoxDevice() {
String manufacturer = Build.MANUFACTURER;
String productName = Build.PRODUCT;
return manufacturer.equalsIgnoreCase("MagicBox")
&& productName.equalsIgnoreCase("MagicBox");
}
public static boolean isProblemBoxDevice() {
return isMiBox2Device() || isMagicBoxDevice();
}
public static boolean isRealARMArch() {
ARCH arch = getMyCpuArch();
return (supportABI("armeabi-v7a") || supportABI("armeabi")) && ARCH.ARM.equals(arch);
}
public static boolean isRealX86Arch() {
ARCH arch = getMyCpuArch();
return supportABI(ABI_X86) || ARCH.X86.equals(arch);
}
/**
* 檢測(cè)設(shè)備是否是 x86
*
* @return
*/
public static boolean checkDeviceForumX86() {
return isRealX86Arch() || isARMSimulatedByX86() || supportX86();
}
}
二次打包檢測(cè)
由于所有二次打包的檢測(cè)都會(huì)被 Hook 繞過(guò)邮旷,所以請(qǐng)先檢測(cè) Hook 黄选,特別是二次打包后入侵 Application 進(jìn)行應(yīng)用內(nèi) Hook 的情況,所以不可信任任何 Java 層的代碼婶肩,下列方案都是在 JNI 層執(zhí)行办陷。
- 方案1:通過(guò)讀取 Apk 的 Zip包信息進(jìn)行校驗(yàn)
如何開(kāi)發(fā) Risk 框架
因?yàn)?Risk 設(shè)計(jì)之初就是以代碼被反編譯的情況下,也能保證邏輯不被發(fā)現(xiàn)并且正確工作律歼,所以需要有很多額外的設(shè)計(jì)民镜,請(qǐng)后期維護(hù) Risk 框架的同事請(qǐng)按照以下設(shè)計(jì)思路:
設(shè)計(jì)代碼蜜罐
用最簡(jiǎn)單的名稱例如 RiskManager 或者明顯的字符串,讓破解者在第一時(shí)能找到苗膝,這塊代碼不可信任殃恒,出現(xiàn)問(wèn)題或者被移除都不影響真正的流程,一定要避免被混淆辱揭。保證邏輯分散
在保證邏輯連貫性的前提下离唐,將代碼分散開(kāi),用 extends 來(lái)將代碼分布在各個(gè)子類问窃、父類中亥鬓。盡量混淆
用 JNI 代替原生代碼
不要使用能夠被閱讀的包名
設(shè)計(jì)指南
對(duì)于 api 相關(guān)的設(shè)計(jì)推薦大量加入鹽方法并且分散,以防止被發(fā)現(xiàn)核心邏輯域庇。
· Hook 檢測(cè)
// 初始化 hook 監(jiān)聽(tīng)嵌戈,必須在主線程中執(zhí)行
new HookManager()
// 假方法覆积,迷惑反編譯者
.fakerMethods()
// 假方法,迷惑反編譯者
.fakerInitHook()
// 真初始化方法
.initHookManager()
// 真方法 獲取需要觀測(cè)的對(duì)象熟呛,這里的數(shù)據(jù)推薦使用配置中心下載
.saveInfo("Xposed==com.bly.chaos0-0de.robv.android.xposed.XposedBridge0-0de.robv.android.xposed.installer0-0xposed0-0de.robv.android.xposed.XposedHelper")
// 假方法宽档,迷惑反編譯者
.fakerMethods2()
// 假方法,迷惑反編譯者
.startHookObserver()
.fakerJniScanHook()
// 真正 jni 檢測(cè)的方法
.jniScanHook()
// 假方法庵朝,迷惑反編譯者
.fakerJniScanHook2();