首页
论坛
课程
招聘
[原创]如何使用FastHook免root Hook微信
2019-3-16 17:55 13349

[原创]如何使用FastHook免root Hook微信

2019-3-16 17:55
13349

一、概述

本文介绍如何通过FastHook + VirtualApp实现免root hook。由于VirtualApp已经不更新了,所以本文只作为一个教程,并不主要解决一些兼容和稳定性问题。

二、实现原理

要实现应用hook,可以简单的分为下面三个步骤:
  1. 识别Hook插件
  2. 保存Hook插件信息
  3. 获取Hook插件信息进行Hook

  • 识别Hook插件

规定Hook插件须在AndroidManifest.xml里定义三个<meta-data>
<meta-data
            android:name="fasthook.hook.plugin"
            android:value="true"/>

        <meta-data
            android:name="fasthook.hook.process"
            android:value="XXX"/>

        <meta-data
            android:name="fasthook.hook.info"
            android:value="XXX"/>
  1. fasthook.hook.plugin:表示这是一个Hook插件
  2. fasthook.hook.process:表示要Hook的进程
  3. fasthook.hook.info:Hook信息类名
可以在VirtualApp解析Apk时加上判断是否为Hook插件的代码,例如在AppRepository.java
private List<AppInfo> convertPackageInfoToAppData(Context context, List<PackageInfo> pkgList, boolean fastOpen) {
        PackageManager pm = context.getPackageManager();
        List<AppInfo> list = new ArrayList<>(pkgList.size());
        String hostPkg = VirtualCore.get().getHostPkg();
        for (PackageInfo pkg : pkgList) {
            // ignore the host package
            if (hostPkg.equals(pkg.packageName)) {
                continue;
            }
            // ignore the System package
            if (isSystemApplication(pkg)) {
                continue;
            }
            boolean isHookPlugin = false;
            //判断是否是Hook插件
            ApplicationInfo ai = null;
            try {
                ai = context.getPackageManager().getApplicationInfo(pkg.packageName,PackageManager.GET_META_DATA);
                if(ai.metaData != null) {
                    boolean enable = ai.metaData.getBoolean("fasthook.hook.plugin", false);
                    if(enable) {
                        String hookProcess = ai.metaData.getString("fasthook.hook.process","");
                        String hookInfo = ai.metaData.getString("fasthook.hook.info","");
                        if(!hookProcess.isEmpty() || !hookInfo.isEmpty()) {
                            isHookPlugin = true;
                        }
                    }
                }
            }catch (Exception e) {
                e.printStackTrace();
            }
            String path = ai.publicSourceDir != null ? ai.publicSourceDir : ai.sourceDir;
            if (path == null) {
                continue;
            }
            AppInfo info = new AppInfo();
            info.packageName = pkg.packageName;
            info.fastOpen = fastOpen;
            info.path = path;
            info.icon = ai.loadIcon(pm);
            info.name = ai.loadLabel(pm);
            info.isHook = isHookPlugin;
            android.util.Log.d("FastHookManager","isHookPlugin:"+isHookPlugin+" package:"+pkg.packageName);
            InstalledAppInfo installedAppInfo = VirtualCore.get().getInstalledAppInfo(pkg.packageName, 0);
            if (installedAppInfo != null) {
                info.cloneCount = installedAppInfo.getInstalledUsers().length;
            }
            list.add(info);
        }
        return list;
    }

  • 如何获取Hook插件

在apk安装时,可以把Hook插件保存起来,例如在VAppManagerService.java
public synchronized InstallResult installPackage(String path, int flags, boolean notify) {
        long installTime = System.currentTimeMillis();
        if (path == null) {
            return InstallResult.makeFailure("path = NULL");
        }
        boolean isHook = (flags & InstallStrategy.IS_HOOK) != 0;
        File packageFile = new File(path);
        if (!packageFile.exists() || !packageFile.isFile()) {
            return InstallResult.makeFailure("Package File is not exist.");
        }
        VPackage pkg = null;
        try {
            pkg = PackageParserEx.parsePackage(packageFile);
        } catch (Throwable e) {
            e.printStackTrace();
        }
        if (pkg == null || pkg.packageName == null) {
            return InstallResult.makeFailure("Unable to parse the package.");
        }
        InstallResult res = new InstallResult();
        res.packageName = pkg.packageName;
        // PackageCache holds all packages, try to check if we need to update.
        VPackage existOne = PackageCacheManager.get(pkg.packageName);
        PackageSetting existSetting = existOne != null ? (PackageSetting) existOne.mExtras : null;
        if (existOne != null) {
            if ((flags & InstallStrategy.IGNORE_NEW_VERSION) != 0) {
                res.isUpdate = true;
                return res;
            }
            if (!canUpdate(existOne, pkg, flags)) {
                return InstallResult.makeFailure("Not allowed to update the package.");
            }
            res.isUpdate = true;
        }
        File appDir = VEnvironment.getDataAppPackageDirectory(pkg.packageName);
        File libDir = new File(appDir, "lib");
        if (res.isUpdate) {
            FileUtils.deleteDir(libDir);
            VEnvironment.getOdexFile(pkg.packageName).delete();
            if(isHook) {
                VActivityManagerService.get().killAllApps();
            }
            else {
                VActivityManagerService.get().killAppByPkg(pkg.packageName, VUserHandle.USER_ALL);
            }
        }
        if (!libDir.exists() && !libDir.mkdirs()) {
            return InstallResult.makeFailure("Unable to create lib dir.");
        }
        boolean dependSystem = (flags & InstallStrategy.DEPEND_SYSTEM_IF_EXIST) != 0
                && VirtualCore.get().isOutsideInstalled(pkg.packageName);

        if (existSetting != null && existSetting.dependSystem) {
            dependSystem = false;
        }

        NativeLibraryHelperCompat.copyNativeBinaries(new File(path), libDir);
        try {
            // copy libva-native.so so that the symbol MSHookFunction() can be accessed in patch plugin after Android N
            if(Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
                File vaNativeSo = new File(libDir, "libva++.so");
                if (!vaNativeSo.exists()) {
                    FileUtils.createSymlink(
                            new File(VEnvironment.getRoot().getParent(), "lib/libva++.so").getAbsolutePath(),
                            vaNativeSo.getAbsolutePath());
                }
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }

        if (!dependSystem) {
            File privatePackageFile = new File(appDir, "base.apk");
            File parentFolder = privatePackageFile.getParentFile();
            if (!parentFolder.exists() && !parentFolder.mkdirs()) {
                VLog.w(TAG, "Warning: unable to create folder : " + privatePackageFile.getPath());
            } else if (privatePackageFile.exists() && !privatePackageFile.delete()) {
                VLog.w(TAG, "Warning: unable to delete file : " + privatePackageFile.getPath());
            }
            try {
                FileUtils.copyFile(packageFile, privatePackageFile);
            } catch (IOException e) {
                privatePackageFile.delete();
                return InstallResult.makeFailure("Unable to copy the package file.");
            }
            packageFile = privatePackageFile;
            chmodPackageDictionary(packageFile);
        }
        if (existOne != null) {
            PackageCacheManager.remove(pkg.packageName);
        }
        PackageSetting ps;
        if (existSetting != null) {
            ps = existSetting;
        } else {
            ps = new PackageSetting();
        }
        ps.isHook = isHook;
        ps.dependSystem = dependSystem;
        ps.apkPath = packageFile.getPath();
        ps.libPath = libDir.getPath();
        ps.packageName = pkg.packageName;
        ps.appId = VUserHandle.getAppId(mUidSystem.getOrCreateUid(pkg));
        if (res.isUpdate) {
            ps.lastUpdateTime = installTime;
        } else {
            ps.firstInstallTime = installTime;
            ps.lastUpdateTime = installTime;
            for (int userId : VUserManagerService.get().getUserIds()) {
                boolean installed = userId == 0;
                ps.setUserState(userId, false/*launched*/, false/*hidden*/, installed);
            }
        }

        PackageParserEx.savePackageCache(pkg);
        PackageCacheManager.put(pkg, ps);
        mPersistenceLayer.save();
        if (!dependSystem) {
            boolean runDexOpt = false;
            if (VirtualRuntime.isArt()) {
                try {
                    ArtDexOptimizer.interpretDex2Oat(ps.apkPath, VEnvironment.getOdexFile(ps.packageName).getPath());
                } catch (IOException e) {
                    e.printStackTrace();
                    runDexOpt = true;
                }
            } else {
                runDexOpt = true;
            }
            if (runDexOpt) {
                try {
                    DexFile.loadDex(ps.apkPath, VEnvironment.getOdexFile(ps.packageName).getPath(), 0).close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        BroadcastSystem.get().startApp(pkg);
        //保存Hook插件信息
        if(isHook) {
            HookCacheManager.HookCacheInfo info = new HookCacheManager.HookCacheInfo(ps.packageName,(String)(pkg.mAppMetaData.get(HookCacheManager.HOOK_PROCESS)),(String)(pkg.mAppMetaData.get(HookCacheManager.HOOK_INFO)));
            HookCacheManager.put((String)(pkg.mAppMetaData.get(HookCacheManager.HOOK_PROCESS)),info);
        }
        else if (notify) {
            notifyAppInstalled(ps, -1);
        }
        res.isSuccess = true;
        return res;
    }

  • 如何Hook

实际可以在任意地方Hook,但为了更好的Hook,这里在应用apk加载之后,attachBaseContext方法调用之前进行Hook,这样便可以Hook所有应用的方法了。例如在VClientImpl.java
private void bindApplicationNoCheck(String packageName, String processName, ConditionVariable lock) {
        VDeviceInfo deviceInfo = getDeviceInfo();
        if (processName == null) {
            processName = packageName;
        }
        mTempLock = lock;
        try {
            setupUncaughtHandler();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        try {
            fixInstalledProviders();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        mirror.android.os.Build.SERIAL.set(deviceInfo.serial);
        mirror.android.os.Build.DEVICE.set(Build.DEVICE.replace(" ", "_"));
        ActivityThread.mInitialApplication.set(
                VirtualCore.mainThread(),
                null
        );
        AppBindData data = new AppBindData();
        InstalledAppInfo info = VirtualCore.get().getInstalledAppInfo(packageName, 0);
        if (info == null) {
            new Exception("App not exist!").printStackTrace();
            Process.killProcess(0);
            System.exit(0);
        }
        data.appInfo = VPackageManager.get().getApplicationInfo(packageName, 0, getUserId(vuid));
        data.processName = processName;
        data.providers = VPackageManager.get().queryContentProviders(processName, getVUid(), PackageManager.GET_META_DATA);
        Log.i(TAG, "Binding application " + data.appInfo.packageName + " (" + data.processName + ")");
        mBoundApplication = data;
        VirtualRuntime.setupRuntime(data.processName, data.appInfo);
        int targetSdkVersion = data.appInfo.targetSdkVersion;
        if (targetSdkVersion < Build.VERSION_CODES.GINGERBREAD) {
            StrictMode.ThreadPolicy newPolicy = new StrictMode.ThreadPolicy.Builder(StrictMode.getThreadPolicy()).permitNetwork().build();
            StrictMode.setThreadPolicy(newPolicy);
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && targetSdkVersion < Build.VERSION_CODES.LOLLIPOP) {
            mirror.android.os.Message.updateCheckRecycle.call(targetSdkVersion);
        }
        if (VASettings.ENABLE_IO_REDIRECT) {
            startIOUniformer();
        }
        NativeEngine.launchEngine();
        Object mainThread = VirtualCore.mainThread();
        NativeEngine.startDexOverride();
        Context context = createPackageContext(data.appInfo.packageName);
        System.setProperty("java.io.tmpdir", context.getCacheDir().getAbsolutePath());
        File codeCacheDir;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            codeCacheDir = context.getCodeCacheDir();
        } else {
            codeCacheDir = context.getCacheDir();
        }

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
            if (HardwareRenderer.setupDiskCache != null) {
                HardwareRenderer.setupDiskCache.call(codeCacheDir);
            }
        } else {
            if (ThreadedRenderer.setupDiskCache != null) {
                ThreadedRenderer.setupDiskCache.call(codeCacheDir);
            }
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (RenderScriptCacheDir.setupDiskCache != null) {
                RenderScriptCacheDir.setupDiskCache.call(codeCacheDir);
            }
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            if (RenderScript.setupDiskCache != null) {
                RenderScript.setupDiskCache.call(codeCacheDir);
            }
        }
        Object boundApp = fixBoundApp(mBoundApplication);
        mBoundApplication.info = ContextImpl.mPackageInfo.get(context);
        mirror.android.app.ActivityThread.AppBindData.info.set(boundApp, data.info);
        VMRuntime.setTargetSdkVersion.call(VMRuntime.getRuntime.call(), data.appInfo.targetSdkVersion);
        //进行Hook
        try {
            tryHook(processName,context.getClassLoader());
        }catch (Exception e) {
            e.printStackTrace();
        }

        Configuration configuration = context.getResources().getConfiguration();
        Object compatInfo = CompatibilityInfo.ctor.newInstance(data.appInfo, configuration.screenLayout, configuration.smallestScreenWidthDp, false);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
                DisplayAdjustments.setCompatibilityInfo.call(ContextImplKitkat.mDisplayAdjustments.get(context), compatInfo);
            }
            DisplayAdjustments.setCompatibilityInfo.call(LoadedApkKitkat.mDisplayAdjustments.get(mBoundApplication.info), compatInfo);
        } else {
            CompatibilityInfoHolder.set.call(LoadedApkICS.mCompatibilityInfo.get(mBoundApplication.info), compatInfo);
        }

        boolean conflict = SpecialComponentList.isConflictingInstrumentation(packageName);
        if (!conflict) {
            InvocationStubManager.getInstance().checkEnv(AppInstrumentation.class);
        }
        mInitialApplication = LoadedApk.makeApplication.call(data.info, false, null);
        mirror.android.app.ActivityThread.mInitialApplication.set(mainThread, mInitialApplication);
        ContextFixer.fixContext(mInitialApplication);
        if (Build.VERSION.SDK_INT >= 24 && "com.tencent.mm:recovery".equals(processName)) {
            fixWeChatRecovery(mInitialApplication);
        }
        if (data.providers != null) {
            installContentProviders(mInitialApplication, data.providers);
        }
        if (lock != null) {
            lock.open();
            mTempLock = null;
        }
        VirtualCore.get().getComponentDelegate().beforeApplicationCreate(mInitialApplication);
        try {
            mInstrumentation.callApplicationOnCreate(mInitialApplication);
            InvocationStubManager.getInstance().checkEnv(HCallbackStub.class);
            if (conflict) {
                InvocationStubManager.getInstance().checkEnv(AppInstrumentation.class);
            }
            Application createdApp = ActivityThread.mInitialApplication.get(mainThread);
            if (createdApp != null) {
                mInitialApplication = createdApp;
            }
        } catch (Exception e) {
            if (!mInstrumentation.onException(mInitialApplication, e)) {
                throw new RuntimeException(
                        "Unable to create application " + mInitialApplication.getClass().getName()
                                + ": " + e.toString(), e);
            }
        }
        VActivityManager.get().appDoneExecuting();
        VirtualCore.get().getComponentDelegate().afterApplicationCreate(mInitialApplication);
    }

    //根据进程名获取Hook插件并Hook
    private void tryHook(String process, ClassLoader apkClassLoader) {
        String[] infos = VPackageManager.get().getInstalledHookPlugins(process);
        if(infos != null) {
            for(String info : infos) {
                int size = info.charAt(0);
                String pluginName = info.substring(1,1 + size);
                String hookInfoName = info.substring(1 + size);

                DexClassLoader hookClassLoader = new DexClassLoader(VEnvironment.getPackageResourcePath(pluginName).getAbsolutePath(),
                        VEnvironment.getDalvikCacheDirectory().getAbsolutePath(),
                        VEnvironment.getPackageLibPath(pluginName).getAbsolutePath(),
                        apkClassLoader);

                FastHookManager.doHook(hookInfoName,hookClassLoader,apkClassLoader,hookClassLoader,hookClassLoader,false);
            }
        }
    }

三、Hook微信

  • 首先准备一个Hook插件

HookMethodInfo.java
public class HookMethodInfo {
    public static void hook(Object thiz, Context context) {
        Log.d("FastHookManager","hook attachBaseContext2");
        forward(thiz,context);
        Toast toast = Toast.makeText(context,"hook attachBaseContext2",Toast.LENGTH_LONG);
        toast.show();
    }

    public native static void forward(Object thiz, Context context);
}
当启动微信时会弹出一个Toast
HookInfo.java
public class HookInfo {
    public static String[][] HOOK_ITEMS = {
            {"1",
                    "com.tencent.tinker.loader.app.TinkerApplication","attachBaseContext","Landroid/content/Context;",
                    "com.example.fasthookplugin.HookMethodInfo","hook","Ljava/lang/Object;Landroid/content/Context;",
                    "com.example.fasthookplugin.HookMethodInfo","forward","Ljava/lang/Object;Landroid/content/Context;"}
    };
}
AndroidManifest.xml
<application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <meta-data
            android:name="fasthook.hook.plugin"
            android:value="true"/>

        <meta-data
            android:name="fasthook.hook.process"
            android:value="com.tencent.mm"/>

        <meta-data
            android:name="fasthook.hook.info"
            android:value="com.example.fasthookplugin.HookInfo"/>
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

  • 实际效果



注:左上角有红色图标即为Hook插件

四、结语

上述是一个简单的例子,也可以做一些有用的功能,下面是我随手做的一个7.0.3版本的微信消息防撤回



参考

FastHook:https://github.com/turing-technician/FastHook

VirtualApp:https://github.com/asLody/VirtualApp


[2023春季班]《安卓高级研修班(网课)》月薪两万班招生中~

收藏
点赞3
打赏
分享
最新回复 (14)
雪    币: 160
活跃值: 活跃值 (169)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
cnlife 活跃值 2019-3-17 08:59
2
0
雪    币: 6639
活跃值: 活跃值 (46409)
能力值: (RANK:115 )
在线值:
发帖
回帖
粉丝
Editor 活跃值 2019-3-17 10:58
3
0
感谢分享!
雪    币: 463
活跃值: 活跃值 (1140)
能力值: ( LV12,RANK:1010 )
在线值:
发帖
回帖
粉丝
sudami 活跃值 25 2019-3-17 11:20
4
1
两年前已做 hello
1. 安卓沙箱自己写不要用VA,可参考以下成熟产品:闪电盒子、平行空间、爱游穿梭机.. 
2. hook引擎可用epic、and*k..
3. xposed功能可参考微X模块(闭源需逆向 ) ,开源的参考微信巫师等github项目..

业余时间玩玩儿可以,手动狗头= =.





最后于 2019-3-17 11:23 被sudami编辑 ,原因:
雪    币: 229
活跃值: 活跃值 (650)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
只是来打酱油 活跃值 2019-3-18 09:35
5
0
直接贴出项目代码老哥
雪    币: 76
活跃值: 活跃值 (13)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mingxuan三千 活跃值 2019-3-19 10:53
6
0
不错 感谢分享
雪    币: 314
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
皮皮豪 活跃值 2019-3-22 10:30
7
0
感谢分享
雪    币: 11050
活跃值: 活跃值 (2454)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
tDasm 活跃值 2019-3-25 10:21
8
0
感觉楼主写得有点复杂,通常免root hook应该这样:
1、安装VirtualApp
2、安装fasthook或xposed(或者学xposed把 VirtualApp集成到 Virtualxposed中一步到位)
3、写要hook的apk然后安装并完成hook
说明fasthook不易于使用?还要修改 VirtualApp ?(或许我没看懂)
最后于 2019-3-25 10:27 被tDasm编辑 ,原因:
雪    币: 2319
活跃值: 活跃值 (1375)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
WindStormy 活跃值 2 2019-3-25 19:40
9
0
不能做到兼容Xposed插件吗?
雪    币: 1
活跃值: 活跃值 (107)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
SunnySmart 活跃值 2019-3-31 03:02
10
0
fasthook类似于xposed的插件么?
雪    币: 2256
活跃值: 活跃值 (159)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
图灵技师 活跃值 1 2019-3-31 14:13
11
0
WindStormy 不能做到兼容Xposed插件吗?
应该是可以的,后续有空尝试做一下
雪    币: 2256
活跃值: 活跃值 (159)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
图灵技师 活跃值 1 2019-3-31 14:16
12
0
SunnySmart fasthook类似于xposed的插件么?
xposed集成到系统里去了,所以功能上是不同的,fasthook仅是一个hook框架
雪    币: 23
活跃值: 活跃值 (22)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
陌上君 活跃值 2019-4-1 20:14
13
0
这个能注入的方式使用吗?必须要其他工具辅助吗
雪    币: 20
活跃值: 活跃值 (105)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
xuyahui 活跃值 2019-4-8 21:34
14
0
学习。看雪 为什么没有收藏的功能,
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
ReeZhou 活跃值 2020-3-14 14:16
15
0
你好,请问有联系方式吗?
有合作想聊一下
最后于 2020-3-14 14:16 被ReeZhou编辑 ,原因:
游客
登录 | 注册 方可回帖
返回