首页
社区
课程
招聘
[原创] 一个轻量级的ArtHook插件
发表于: 2022-5-11 02:24 23519

[原创] 一个轻量级的ArtHook插件

2022-5-11 02:24
23519

ApoloPlugin 提供了一个Android轻量级的java hook 库,它支持 arm32 和 arm64两种架构。Apolo意为阿波罗,其为艺术之神,Art翻译过来也有艺术之意,故以此命名。

ArtHook的灵感,都要从接触jvmti技术开始。但是framework只支持debug包加载jvmti插件,虽然某些技术分享者过掉了debug检测,但是运行时依然功能受限。后续又通过研究android.os.Trace,发现了art另一机制-instrumentation。该机制功能非常强大,比如下述源码(均在art/runtime/instrumentation.cc中)

特别是GetCurrentInstrumentationLevel函数中kInstrumentWithInterpreter以及kInstrumentWithInstrumentationStubs这两个变量大大引发了我的思考,并由此开始了Apolo插件之路。

经过一个月左右的研究,此路已通。虽然还有一些问题待解决,但是经过版本迭代,我相信Apolo插件会更加完善。我个人其实也没有什么终极目标,毕竟技术一直都在变化,只愿能真正帮助到有需求的人。

注意: 暂时不支持声明为abstract、interface、native等特殊函数hook

ApoloPlugin目前发布在maven central, 方便接入

接口函数可以在仓库ApoloPlugin module找到,目前只提供了Java版本

示例代码可以在app module中找到

hook引擎中会绕过hiddenApi检测,该处参考了RePublic。所以调用preLoad之后,您无须担心hiddenApi问题。

比如hook Java.lang.String.equals函数(样例)

上述代码样例有四个重点

1) 参数类型不是public class,如何声明代理函数?

2) 代理函数中能否调用其他被代理的函数,这样是否会导致死循环?

下图为StringBuilder.toString代理函数的smali code,我们可以看到字符串拼接被编译成了StringBuilder.append,最后会调用StringBuilder.toString。

为了防止死循环,我在Slog.d代码前后添加了hookTransition解决

再次注意: 1)原函数如果是static,那么代理函数的参数类型与原函数需保持一致。2)原函数非static,那么代理函数的第一个参数类型必须使用ThisObject注解,其余参数与原函数保持一致

代码示例github查看

比如您要hook Activity.onCreate(Bundle.class)函数

假定您已经通过2中的注解方式或者3中的xposed方式添加了hooker, hook其实还未生效,您需要主动调用ArtHook.startHook才可以

当前提供了非常方便的接口(ArtEngine.callOrigin(Object instance, Object... args)),上述2中也提及到了。您需要再深刻理解一下该接口,比如静态函数时,第一个参数instance为null即可,但是参数args要求就比较严苛了,不能为null,并且要与原函数签名一致

该处代码,是demo中ActivityThread某静态函数的hook示例,callOrigin的时候第一个参数必须显式传null

某些场景下您可能会使用该特性

示例函数分为三个步骤,第二步为调用原函数。第一步以及第三步均调用了ArtHook.hookTransition。

所以,您可以简单的将hookTransition理解为hook状态切换, 该函数非常实用:
1) true:状态切换为origin,该线程取消hook.
2) false: 状态切换为hook,该线程将进行相关函数代理.

注意: 如果您在第二步中,有大量逻辑代码,如果该处逻辑中有直接或者间接调用某一个被hook的函数,那么它将不会被代理,直到您调用hookTransition(false)为止。所以此处使用了finally语句块,确保hook继续生效,否则当前线程hook功能会失效(仅当前线程)。

群消息会及时同步最新feature。

void Instrumentation::InstallStubsForClass(ObjPtr<mirror::Class> klass) {
    ...
}
void Instrumentation::InstallStubsForMethod(ArtMethod* method) {
    ...
}
 
Instrumentation::InstrumentationLevel Instrumentation::GetCurrentInstrumentationLevel() const {
  if (interpreter_stubs_installed_) {
    return InstrumentationLevel::kInstrumentWithInterpreter;
  } else if (entry_exit_stubs_installed_) {
    return InstrumentationLevel::kInstrumentWithInstrumentationStubs;
  } else {
    return InstrumentationLevel::kInstrumentNothing;
  }
}
void Instrumentation::InstallStubsForClass(ObjPtr<mirror::Class> klass) {
    ...
}
void Instrumentation::InstallStubsForMethod(ArtMethod* method) {
    ...
}
 
Instrumentation::InstrumentationLevel Instrumentation::GetCurrentInstrumentationLevel() const {
  if (interpreter_stubs_installed_) {
    return InstrumentationLevel::kInstrumentWithInterpreter;
  } else if (entry_exit_stubs_installed_) {
    return InstrumentationLevel::kInstrumentWithInstrumentationStubs;
  } else {
    return InstrumentationLevel::kInstrumentNothing;
  }
}
 
dependencies {
    implementation "io.github.waxmoon:ApoloPlugin:0.0.1"
}
dependencies {
    implementation "io.github.waxmoon:ApoloPlugin:0.0.1"
}
dependencies {
    implementation "io.github.waxmoon:xposed:0.0.1"
}
dependencies {
    implementation "io.github.waxmoon:xposed:0.0.1"
}
 
public class DemoApplication extends Application {
    private static Application sApp;
    static {
        //init ArtHook
        ArtEngine.preLoad();
    }
 
    @Override
    public void onCreate() {
        super.onCreate();
    }
}
public class DemoApplication extends Application {
    private static Application sApp;
    static {
        //init ArtHook
        ArtEngine.preLoad();
    }
 
    @Override
    public void onCreate() {
        super.onCreate();
    }
}
@HookClass(String.class)
public class StringProxy {
 
    private static final String TAG = StringProxy.class.getSimpleName();
    @HookName("equals")
    public static boolean equals(@ThisObject String str1, Object str2) {
        Slog.d(TAG, "proxy_equals called %s vs %s", str1, str2);
        return ArtEngine.callOrigin(str1, str2);
    }
}
@HookClass(String.class)
public class StringProxy {
 
    private static final String TAG = StringProxy.class.getSimpleName();
    @HookName("equals")
    public static boolean equals(@ThisObject String str1, Object str2) {
        Slog.d(TAG, "proxy_equals called %s vs %s", str1, str2);
        return ArtEngine.callOrigin(str1, str2);
    }
}
代理函数的参数也可使用HookName注解,比如android.app.ContextImpl.createAppContext(ActivityThread mainThread, LoadedApk packageInfo), 您的代理函数参数可以这样使用:
     (HookName("android.app.ActivityThread" Object mainThread, @HookName("android.app.LoadedApk") Object packageInfo))
代理函数的参数也可使用HookName注解,比如android.app.ContextImpl.createAppContext(ActivityThread mainThread, LoadedApk packageInfo), 您的代理函数参数可以这样使用:
     (HookName("android.app.ActivityThread" Object mainThread, @HookName("android.app.LoadedApk") Object packageInfo))
代理函数中支持调用其他被代理的函数,但是一旦调用链形成A->ProxyA->B->ProxyB->A这样一个环形结构,就会出现死循环。您可以在调用其他函数时,尝试使用ArtEngine.hookTransition规避此类问题。app样例中就有该类问题,比如hook了StringBuilder.toString函数,并且在代理函数中使用Log.d,有可能就会因为再次间接调用StringBuilder.toString, 导致死循环.
代理函数中支持调用其他被代理的函数,但是一旦调用链形成A->ProxyA->B->ProxyB->A这样一个环形结构,就会出现死循环。您可以在调用其他函数时,尝试使用ArtEngine.hookTransition规避此类问题。app样例中就有该类问题,比如hook了StringBuilder.toString函数,并且在代理函数中使用Log.d,有可能就会因为再次间接调用StringBuilder.toString, 导致死循环.
 
@HookName("toString")
public static String toString(@ThisObject Object sb) {
    String ret = ArtEngine.callOrigin(sb);
    ArtEngine.hookTransition(true);
    Slog.d(TAG, "proxy_toString :" + ret);
    ArtEngine.hookTransition(false);
    return ret;
}
@HookName("toString")
public static String toString(@ThisObject Object sb) {
    String ret = ArtEngine.callOrigin(sb);
    ArtEngine.hookTransition(true);
    Slog.d(TAG, "proxy_toString :" + ret);
    ArtEngine.hookTransition(false);
    return ret;

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2022-5-20 14:30 被WaxMoon编辑 ,原因:
收藏
免费 4
支持
分享
最新回复 (12)
雪    币: 1623
活跃值: (1773)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
支持一下
2022-5-11 12:54
0
雪    币: 208
活跃值: (387)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
对比 https://github.com/LSPosed/LSPlant 有什么优势?
2022-5-11 14:25
0
雪    币: 175
活跃值: (226)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
对比 https://github.com/LSPosed/LSPlant 有什么优势?
2022-5-11 14:28
0
雪    币: 509
活跃值: (778)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
yujincheng08 对比 https://github.com/LSPosed/LSPlant 有什么优势?
嗯,优势的对比,我这几天会出一个文档。ApoloPlugin目前是采取art自身的instrumentation机制实现的,相比其他框架(使用inlineHook或者强制解释执行,原方法通过clone ArtMethod保存)的侵入性,本身就是优势(因为使用的是art原本的插件机制)。同时大家可以了解一下android8的jvmti,但是jvmti只支持debug包,而ApoloPlugin支持线上使用,稳定性理论上非常棒,但是待观察。目前该方案还在初级阶段,后续会持续优化并开放核心源码
2022-5-11 15:15
0
雪    币: 5330
活跃值: (5479)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
6
希望楼主发一份原理分析文档
2022-5-11 17:22
0
雪    币: 509
活跃值: (778)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
现在项目才开发出来,需要设计以及优化整个插件的架构。工作+项目有点忙不过来。等后面发布版本的时候,会把原理附上哈。细心的童鞋可能会看到github目前还没有发版本
2022-5-11 18:27
0
雪    币: 4423
活跃值: (2757)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
支持楼主
2022-5-13 09:36
0
雪    币: 509
活跃值: (778)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
仓库更新:
1.修复crash->在Application.onCreate生命周期之前调用ArtHook.preLoad崩溃
2.支持java构造函数的hook
3.提供callOrigin接口,方便调用原函数
4.支持代理函数中调用其他被hook的函数
2022-5-14 00:32
0
雪    币: 550
活跃值: (2387)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
10
java.lang.String.equals都hook到了,牛逼
2022-5-19 22:03
0
雪    币: 3836
活跃值: (4142)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
关注
2022-5-20 08:48
0
雪    币:
活跃值: (27)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
WaxMoon 现在项目才开发出来,需要设计以及优化整个插件的架构。工作+项目有点忙不过来[em_20]。等后面发布版本的时候,会把原理附上哈。细心的童鞋可能会看到github目前还没有发版本[em_13]
楼主,native层逻辑目前是不开源是吧
2022-8-2 21:28
0
雪    币: 562
活跃值: (4190)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
拉个微信好不好
2022-11-21 16:28
0
游客
登录 | 注册 方可回帖
返回
//