我的 Android 重構(gòu)之旅:Hook與模擬器檢測(cè)

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();
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末吗冤,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子九府,更是在濱河造成了極大的恐慌椎瘟,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件侄旬,死亡現(xiàn)場(chǎng)離奇詭異肺蔚,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)儡羔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門宣羊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人笔链,你說(shuō)我怎么就攤上這事段只。” “怎么了鉴扫?”我有些...
    開(kāi)封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)澈缺。 經(jīng)常有香客問(wèn)我坪创,道長(zhǎng),這世上最難降的妖魔是什么姐赡? 我笑而不...
    開(kāi)封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任莱预,我火速辦了婚禮,結(jié)果婚禮上项滑,老公的妹妹穿的比我還像新娘依沮。我一直安慰自己,他們只是感情好枪狂,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布危喉。 她就那樣靜靜地躺著,像睡著了一般州疾。 火紅的嫁衣襯著肌膚如雪辜限。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天严蓖,我揣著相機(jī)與錄音薄嫡,去河邊找鬼氧急。 笑死,一個(gè)胖子當(dāng)著我的面吹牛毫深,可吹牛的內(nèi)容都是我干的吩坝。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼哑蔫,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼钉寝!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起鸳址,我...
    開(kāi)封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤瘩蚪,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后稿黍,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體疹瘦,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年巡球,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了言沐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡酣栈,死狀恐怖险胰,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情矿筝,我是刑警寧澤起便,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站窖维,受9級(jí)特大地震影響榆综,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜铸史,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一鼻疮、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧琳轿,春花似錦判沟、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至媚送,卻和暖如春中燥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背塘偎。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工疗涉, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留拿霉,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓咱扣,卻偏偏與公主長(zhǎng)得像绽淘,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子闹伪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容