使用C#编写Xposed模块:XamarinPosed

XamarinPosed是我写的模板项目,一个用Xamarin来实现Xposed模块的框架。基于这个框架,你可以用C#来写Xposed或是VirtualXposed模块,来hook、动态修改安卓应用。

后续若有空可能会实际用XamarinPosed写点工具展示一下。

下图是XamarinPosed的Demo,使用C#编写Xposed模块,并且hook另一个Xamarin app,修改它点击ACTION之后的行为,使ACTION被点击后弹出toast。(如果不启用Xposed模块,则不会有任何显示。)当然XamarinPosed和其他Xposed模块一样可以hook正常的安卓app,例子采用Xamarin只是为了方便。(深挖下去或许也有望进行一些C#层的Hook,那就更有趣了。)

XamarinPosed

 

项目地址:https://github.com/UlyssesWu/XamarinPosed

 

项目架构

XamarinPosed:主项目,你的模块逻辑在这里实现。可以通过在项目属性里改包名的方式,创建其他名称的模块。

Xamarin.Posed.Demo:一个用于被hook的示例项目,同样也由C#/Xamarin编写。当然,XaraminPosed也适用于hook非Xamarin的安卓项目。

XPosedAPI:用于从原始jar库生成Xamarin可用的Xposd绑定库的项目,应用了几个技巧(或者说,workaround)最终得以顺利生成。

JavaCodePostProcessor:编译时使用的后处理工具,用于在编译apk之前把java适配代码复制到项目里。

 

用法

参考:https://github.com/UlyssesWu/XamarinPosed/blob/main/XamarinPosed/Loader.cs

在这个Loader类中,你可以看到Xposed熟悉的接口:InitZygote(在Zygote启动时调用)、HandleLoadPackage(在一个app启动时调用,主要入口)、HandleInitPackageResources(当app初始化资源时调用,VXP不可用)。只需在里面实现逻辑即可。

项目属性中可以指定编译宏:VXP(默认指定)。VXP不支持HandleInitPackageResources,在VXP模式下,即使实现了这个接口,也不会实际被调用。如果去掉VXP编译宏,却在VXP上运行,则会出错。

项目中的xposed_init文件内容不可修改。这个文件用于指定Xposed加载此app作为模块时,实例化哪个类型。这里指定的是一个固定的类型,具体原理往下看就知道了。

限制

目前美中不足的一点,就是一个Xamarin编写的模块启用之后,是没有办法把它本身直接作为应用程序再打开的。这是因为它会加载mono的native库,而android系统限制一个native库只能由一个classloader打开,因此模块占用了native库之后,程序再想要打开它时就会失败,导致apk无法启动了。这个问题或许可以通过hook程序,让它使用模块的classloader来解决,但是目前并没有试成功。不过,一般模块并没有作为应用程序打开的需要,除了一些设置选项的需求。这点困难可以通过别的方式(如配置文件等)去克服。

 

实现细节

JAR绑定

首先第一步要克服jar绑定过程中出现的错误。错误是由于java的命名规范与C#不同,导致某个Unhook方法和Unhook类型重名,于是绑定库中的Unhook方法没有被自动生成。此时有两种方案:

一种是在XPosedAPI\Transforms\Metadata.xml中指定重命名规则,使得Xamarin编译系统可以将Unhook方法重命名成UnHook,从而避免和Unhook类型冲突。Xamarin公司的专家Redth的Xamarin.Android.Xposed项目就是这么实现的。可惜的是,这个项目并没有解决后面的其他坑,因此Xamarin.Android.Xposed是没法正常使用的。三年过去了都没有别的方案出现,我只好自己摸索打通了后面的步骤,这才有了XamarinPosed。

另一种就是XamarinPosed的实现方式,在XPosedAPI\Additions\Unhook.cs中自行实现IXUnhook.Unhook()方法。由于使用显式接口实现,不会产生与类型重名的问题。

    public abstract partial class XC_MethodHook
    {
        public partial class Unhook
        {
            void IXUnhook.Unhook()
            {
                InvokeUnhook();
            }
        }
    }

 

JAR引用

生成了绑定库之后,引用它也会出现点小问题:Xposed不允许将xposed-api.jar直接打进apk包里,这个jar只能作为外部引用,真正运行的时候,由系统(xposed)来提供xposed当前版本的jar。然而,在Xamarin中,绑定库默认的行为只能是InputJar,它会把jar的代码打进apk包里。(好在,VXP其实允许这种jar代码在apk包里的情况。如果你只用VXP,这不是个大问题。)

经过摸索,我也找出了正确的实现方式,同样有两种:

一种是在使用标签(Attribute),在XPosedAPI\Properties\AssemblyInfo.cs中加入一个标签:

[assembly: Java.Interop.DoNotPackage("api-82.jar")] 

这个标签标示打包时不应将这个名字的jar包实际打到apk中。可惜的是,这个标签已经标记为弃用了,后续的Xamarin版本可能不再能用。

另一种方式,是在实际引用XPosedAPI绑定库的项目,也就是XamarinPosed项目中,再建立Jars目录,再放一份同样的api-82.jar,但是要将它的属性-生成操作设置为AndroidExternalJavaLibrary。最终效果与上一种方式相同。

stackoverflow中我对相关问题的回答:https://stackoverflow.com/questions/60086046/xamarin-binding-library-combination-of-inputjar-and-referencejar/64973909#64973909

暴露Java类型,启动mono

阻碍Xamarin应用成为Xposed模块的最大问题,就是Xamarin应用的启动机制。参考 https://docs.microsoft.com/en-us/xamarin/android/internals/architecture ,Xamarin应用通过注册一个特殊的ContentProvider,在应用程序启动的早期拉起mono虚拟机,然后再执行C#代码所在的程序集。然而,Xposed启动模块时是直接实例化一个实现了Xposed接口的java类型,不会走到ContentProvider,也就不会启动mono虚拟机。另外,Xamarin默认不会暴露C#类型,除非它可能需要被Java代码调用,需要生成一个ACW(Android Callable Wrapper, https://docs.microsoft.com/en-us/xamarin/android/platform/java-integration/android-callable-wrappers)。

因此第一步,我们需要保证实现了Xposed接口的Loader类型生成ACW,以供java代码调用。上面的文档提到,基本上所有Android可继承的功能类型(ActivityApplicationServiceBroadcastReceiver, and ContentProvider)都需要生成ACW。因此,简单点就可以让Loader继承自这些类型中的一个。但是这会导致Loader类型变得冗余,Xposed实例化它的开销也会变大。XamarinPosed选择了方便而且轻量的方式,就是让Loader成为MainActivity的嵌套类型(nested type)。这样,当MainActivity被生成ACW时,作为它嵌套类的Loader类型也会被生成ACW了。同时,生成过程并不会把C#的嵌套类生成为java的嵌套类,它会是一个单独的类型,只是类名前面加了MainActivity_。这种生成方式正合我意,保证了Loader的简洁(非嵌套)和轻量(不继承其他不必要的类型)。

(补记:看了另一个未成功的尝试XposedXamarin,或许也可以在Loader类上用 [Register("xamarin/posed/Loader")] 这种标签来要求生成ACW。)

第二步,我们需要在Xposed实例化我们的类型时,在调用任何C#代码之前,先启动mono虚拟机。显然,这是不可能由C#代码完成的任务,否则就成了“先有蛋还是先有鸡”悖论了。因此,我们需要准备一段java实现的xposed接口类型,在initZygote中实现拉起mono虚拟机的任务。拉起mono虚拟机的代码,可以参考正常Xamarin应用生成出来之后,mono.MonoPackageManager.LoadApplication函数的实现方式。里面用到了不少的参数,但是好在必要的参数通过initZygote基本上都可以拿到,当然也不排除某些奇葩魔改系统上的默认路径不讲武德,会出问题。实现具体可看XamarinPosed中的java代码。

第三步,我们需要把这段代码加入到编译流程里去。具体地说,我们需要在Xamarin生成java框架代码之后、准备和C#程序集以及mono native库一起编译成apk之前,把我们准备好的代码塞到Xamarin生成的java框架代码里去。通过查阅Xamarin的文档,确实有这么一个时机可用:https://docs.microsoft.com/en-us/xamarin/android/deploy-test/building-apps/build-properties#aftergenerateandroidmanifest 

Xamarin项目(csproj)中可以指定的一个编译属性:AfterGenerateAndroidManifest,是在生成apk最终的AndroidManifest.xml之后调用自定义的编译步骤。这个时候,显然apk还没有编译,但java代码已经准备就绪,刚刚生成完这个关键的配置文件。此时我们通过调用XamarinPosed中最后一个登场、但是要第一个编译的项目JavaCodePostProcessor,传入编译参数等必要信息(判断是否是VXP模式),就可以把我们的java代码塞到要编译的apk项目里了。好在这APK工程不像CMake或是MSVC项目,不需要再改工程文件来指定我们新加的java文件需要编译。

 

上面的坑都解决之后,就能顺利编译出可用的Xamarin Xposed模块了,真不容易Orz

 

实战示例

使用XamarinPosed hook手机wx,实现wx机器人。

同时,采用dynamic,可以极大减轻Java反射、或者JNI的痛苦,写出的代码几乎看不出是在反射调用。

例如下图中msgInfo.extras.sendername,实际上经历了两次反射调用和一次类型转换。

这个dynamic的实现思路非常类似于之前文章提到的DynamicJavonet(也是我搓的)。

除了dynamic之外,项目中还大量使用async/await,以及各种nuget包。整体开发体验还是不错的。

调试还没有时间研究解决,但只要多写几个try/catch并打log,基本上就算出错也能很快定位问题所在。

评论 (1) -

  • ght
    大神,希望出更多示例,目前用xamarin编写xposed,您这是先行者,顶你!!!

添加评论

Loading