Hello大家好,好久没有出文章了,不知道大家想我了没有,本来这一篇是要写《Flutter原理篇:硬核-从Platform到Dart通信原理分析之iOS篇》的,但是中间在工作中遇到一些积累的东西,特别想把他记录下来,而且关于这个主题我其实很早以前就想写了,一方面当时积累不够多不足以全面的来书写这个话题,二来没有机缘巧合的时间所以每每想起时只有作罢,所以直到今天才有了今天这篇文章,喜欢Flutter的小伙伴要有点耐心再稍等一下😁,好了回到这篇文章的这个话题来,我们今天来聊一聊App开发涉及或者接触到的用到的Hook技术,我会以我视角来带大家去看看这些常用的Hook技术。
首先声明,本人并非是做底层架构出身,也并非是从事App安全相关的工作,之所以对于Hook有这么多的了解只不过是兴趣使然,再加上本人有扎实的计算机底层的基础,对于一些Hook技术可以去弄明白他的缘由而已,另外这篇文章也为了我也是为了我对于Hook这个主题做一个自我的总结,以后可能都不会多花时间去涉及这一块的内容了,好的让我们愉快的开始吧。
本着该文是杂谈属性的又一篇“杰作”,熟悉我博客的小伙伴就知道这一篇肯定也是轻松愉快的文章,代码量肯定不大,大家只需放松心态即可,我们还是从《Android App Hook》开始来谈谈,《iOS App Hook》我们放到下一篇来讲解,那么大家知道Android主要是基于Java语言的开发的,熟悉Java语言的小伙伴都知道,Java本身就是一门带动态特性的静态语言,所以我们单从Java应用层就有很多的Hook技术可以用来玩的。
首先使用Java语言很容易的写出一种简单的一般称作静态代理模式的Hook,如下:
/**
* 售票服务接口
*/
public interface TicketService {
//售票
public void sellTicket();
}
/**
* 售票服务接口实现类,车站
*/
public class Station implements TicketService {
@Override
public void sellTicket() {
System.out.println("\n\t售票.....\n");
}
}
/**
* 车票代售点
*
*/
public class StationProxy implements TicketService {
private Station station;
public StationProxy(Station station){
this.station = station;
}
@Override
public void sellTicket() {
// 1.做真正业务前,提示信息
this.showAlertInfo("××××您正在使用车票代售点进行购票,每张票将会收取5元手续费!××××");
// 2.调用真实业务逻辑
station.sellTicket();
// 3.后处理
this.showAlertInfo("××××欢迎您的光临,再见!××××\n");
}
}
很简单吧,这种在原本业务逻辑前面加上一些判断处理的方式,我们称之为AOP的编程也就是面向切面的编程。
说完了静态代理,那么就不得不提到Java还有一种动态代理的模式,他不需要手动创建代理对象,但是可以完全实现静态代理功能,最常见的就是JDK自带的InvocationHandler类,使用方法很简单
static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。
Object invoke(Object proxy,Method method,Object[] args)
在代理实例上处理方法调用并返回结果。
这样一来你就不需要自己实现很多的代理对象,通过这个可以自动的创建一个代理对象来实现AOP的Hook,动态代理的实现主要依赖于java.lang.reflect.InvocationHandler接口与java.lang.reflect.Proxy类,其实现原理是基于java的反射技术来实现的。
我们知道Java语言是编译成字节码解释执行的,那么有没有一种可能使得我们在编译的时候自动生成一些代码来进行Hook呢,答案是有的,比如我们做Android开发很常见的APT技术,我们常见使用这个的框架为ButterKnife,ARouter等等,简单的来讲这些框架通过APT技术实现的。
我们先来看看APT是什么,APT(Annotation Processing Tool)是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,根据注解自动生成代码。 Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件(文件具体内容由Annotation处理器的编写者决定),APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件
简单的来讲通过APT技术,即注解处理器在编译时扫描并处理注解,注解处理器可以在编译时生成额外的.java文件,在程序运行的时候调用相关方法,可以达到减少重复代码的效果。它的好处:提高开发效率,使得项目更容易维护和扩展,同时几乎不影响性能。
例如ARouter动态生成的代码如下:
public class ARouter$$Root$$app implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("test", ARouter$$Group$$test.class);
}
}
大家具体想了解ARouter与APT的原理,可以去看看我这篇博客 Android App模块化篇
既然Java是编译成字节码执行的,那么有没有一种可能使得他在执行的时候去修改字节码达到Hook的目的呢,答案是有的最常见的就是javassist,他可以直接编辑和生成Java的字节码,达到另一种动态的特性,例如:
package com.example.javassist;
public class Hello {
public static void say() {
System.out.println("hello world!");
}
}
package com.example.javassist;
import javassist.*;
public class Javassist {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("com.example.javassist.Hello");
CtMethod personFly = cc.getDeclaredMethod("say");
personFly.insertBefore("System.out.println(\"执行方法之前\");");
personFly.insertAfter("System.out.println(\"执行方法之后\");");
cc.toClass();
Hello.say();
}
}
执行方法之前
hello world!
执行方法之后
上面就是通过javassist动态操作字节码去生成的方法,以此来达到了动态AOP的效果,具有类似功能的还有ASM框架,作用类似,这里就不细讲了。
上面更多的是从Java的运行原理去分析的,我们再从一些常见的框架去看看,还有哪些Hook的特性呢,首先介绍的就是DroidPlugin这个框架了,这个是一个比较老的Android插件化框架了,通过Hook了Android的四大组件,Binder机制等等来实现插件化的功能的,那么他的基本原理是怎么实现的呢
这个框架的Hook有两个要素:
- 第一要找到一个最合适Hook的点,既不影响额外的功能,又能很要的解决问题,
- 第二去动态的修改他里面的变量或者属性等等,达到Hook的目的,至于修改的方法其实就是我们上面介绍过的通过Java的动态代理以及反射去去实现,
给大家举一个例子吧:
我们想Hook住系统的剪贴板内容替换为 "you are hooked”,一共要分几步呢:第一首先Hook住sCache这个变量使得我们查找CLIPBOARD_SERVICE这个剪贴板服务的时候返回我们自己生成的一个BinderProxyHookHandler动态代理
// Hook 掉这个Binder代理对象的 queryLocalInterface 方法
// 然后在 queryLocalInterface 返回一个IInterface对象, hook掉我们感兴趣的方法即可.
IBinder hookedBinder = (IBinder) Proxy.newProxyInstance(serviceManager.getClassLoader(),
new Class<?>[] { IBinder.class },
new BinderProxyHookHandler(rawBinder));
// 把这个hook过的Binder代理对象放进ServiceManager的cache里面
// 以后查询的时候 会优先查询缓存里面的Binder, 这样就会使用被我们修改过的Binder了
Field cacheField = serviceManager.getDeclaredField("sCache");
cacheField.setAccessible(true);
Map<String, IBinder> cache = (Map) cacheField.get(null);
cache.put(CLIPBOARD_SERVICE, hookedBinder);
然后在这个IBinder的对象也就是BinderProxyHookHandler的queryLocalInterface方法里面返回是我们生成的一个实现了IInterface,IClipboard接口的BinderHookHandler对象,这里使用的是动态代理机制
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("queryLocalInterface".equals(method.getName())) {
Log.d(TAG, "hook queryLocalInterface");
// 这里直接返回真正被Hook掉的Service接口
// 这里的 queryLocalInterface 就不是原本的意思了
// 我们肯定不会真的返回一个本地接口, 因为我们接管了 asInterface方法的作用
// 因此必须是一个完整的 asInterface 过的 IInterface对象, 既要处理本地对象,也要处理代理对象
// 这只是一个Hook点而已, 它原始的含义已经被我们重定义了; 因为我们会永远确保这个方法不返回null
// 让 IClipboard.Stub.asInterface 永远走到if语句的else分支里面
return Proxy.newProxyInstance(proxy.getClass().getClassLoader(),
// asInterface 的时候会检测是否是特定类型的接口然后进行强制转换
// 因此这里的动态代理生成的类型信息的类型必须是正确的
new Class[] { IBinder.class, IInterface.class, this.iinterface },
new BinderHookHandler(base, stub));
}
Log.d(TAG, "method:" + method.getName());
return method.invoke(base, args);
}
最后在返回实现了IClipboard的对象BinderHookHandler去查找具体方法的时候Hook住getPrimaryClip方法使其返回固定的内容即可,这里也是使用的动态代理机制
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 把剪切版的内容替换为 "you are hooked"
if ("getPrimaryClip".equals(method.getName())) {
Log.d(TAG, "hook getPrimaryClip");
return ClipData.newPlainText(null, "you are hooked");
}
// 欺骗系统,使之认为剪切版上一直有内容
if ("hasPrimaryClip".equals(method.getName())) {
return true;
}
return method.invoke(base, args);
}
关于DroidPlugin的内容还有很多,网络上有一个高手叫 weishu 专门写了几篇分析这个框架的文章可以给大家分享一下 http://weishu.me/,大家感兴趣的话可以去看看他的博客
接下来我们再来谈谈QQ空间的热修复方案 MultiDex,他的Hook方式相对来说比较好理解,主要是利用了Android的动态的类加载机制(可以运行时加载)把需要Hook的内容提前放到前面使用Class Loader加载使得运行的方法被替换了效果,QQ空间热修复方案正是基于ClassLoader的这个原理,把修复后的类打包到一个dex(path.dex)中去,然后把这个dex插入到Elements的最前面去
private static void injectAboveEqualApiLevel14(Context context, String dexPath, String dexClassName)
throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
//得到当前的PathClassLoader
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
//将老的dexElements和pathDexElements进行组合生出新的dexElements
Object a = combineArray(getDexElements(getPathList(pathClassLoader)),
getDexElements(getPathList(
new DexClassLoader(dexPath, context.getDir("dex", 0).getAbsolutePath(), dexPath, context.getClassLoader()))));
//拿到DexPathList对象
Object a2 = getPathList(pathClassLoader);
//将DexPathList实例中的dexElements成员替换为合并后的dexElements
setField(a2, a2.getClass(), "dexElements", a);
//加载指定的类
pathClassLoader.loadClass(dexClassName);
}
好了,上面介绍的都是基于应用层的Hook了已经是五花八门的,但是仅仅只是一半而已,另一半基于底层的Hook也很精彩,比如接下来要介绍的Andfix框架,AndFix不同于QQ空间超级补丁技术和微信Tinker通过增加或替换整个DEX的方案,提供了一种运行时在Native修改Filed指针的方式,实现方法的替换,达到即时生效无需重启,对应用无性能消耗的目的;
说白了就是利用Android虚拟机执行的机制,是把要被替换的方法变为Native方法,因为由于虚拟机的执行顺序如果是JAVA方法会被解释执行(解释执行里面的字节码指令),如果是Native方法那么就会直接调用(Method 结构体里面的nativeFunc就是函数的地址),这里再覆盖他的nativeFunc为自定义的dalvik_dispatcher方法,这里面最核心的就是dvmCallMethod_fnPtr这个指针函数的调用,
dvmCallMethod_fnPtr(self, (Method*) jInvokeMethod,
dvmCreateReflectMethodObject_fnPtr(meth), &result, thisObj,argArray);
这里通过这个JVM函数动态调用了JAVA的invoke的反射方法,方法体就是替换的函数内容dvmCreateReflectMethodObject_fnPtr(meth), 总结就是让被替换方法走Native的方式然后在JNI层去Hook住nativeFun,然后操作JVM的调用函数去调用反射的方式去执行一个新的JAVA方法,由于是“杂谈”的缘故不会讲解得很细,大家想看具体的Andfix的话可以去看看我的这篇博客 AndFix各个版本的改动以及原理
既然到了底层我们再来看看Android 底层还有哪些Hook的好东西呢,下一个要介绍的就是基于Got的hook,比较可惜的是在Android里面没有找到一个成熟的框架,大家都是手写去实现的,他的是怎样进行Hook的呢
首先通过解析elf格式,分析Section header table找出静态的.got表的位置,并在内存中找到相应的.got表位置,这个时候内存中.got表保存着导入函数的地址,读取目标函数地址,与.got表每一项函数入口地址进行匹配,找到的话就直接替换新的函数地址
GotHook涉及内容比较多,给大家推荐一篇文章 https://www.jianshu.com/p/43ef7ddd0081
GotHook在Android中应用较少,而且并不是全能的,网络上很多的博客以及up主的视频都提到了主要他是用来Hook外部方法的,但是其实这种说法是不正确的,这里没有针对那些博主的以及up主(因为网络上很多博客博主自己都没弄明白Got Hook的原理),因为大家知道C语言里面方法默认就是外部方法,难道都是可以被Got Hook的吗,显然是不可能的,熟悉Got Hook的都知道这种应该是在动态库里面Hook才会有效果的,所以应该是在动态库里面的强符号方法才可以被Hook
现在Got Hook已经很底层了,那还有没有更底层的Hook呢,还真有那就是称之为终极Hook的inline Hook,目前据了解支持inline Hook的框架有Cydia Substrate,Dobby这两个框架,老实讲这种Hook的作用范围非常广,不限于Android底层,只要是C/C++语言相关的他都可以Hook,而且几乎没有限制条件,那么他是怎么Hook的呢,先来看一张图
它属于是运行时指令替换,假设目标方法内有ins0, ins1, ins2三条指令,首先将起始指令(实际上是前2条指令)替换为等长的跳转指令jump_ins,jump_ins负责跳转到hook方法执行,而hook操作后,往往还需要保留调用原方法的能力以保证功能可用性,所以hook方法内还有一个跳转指令来调回原方法继续执行(jump ins1),调回前需要先补充执行目标方法已被替换的原始指令(图中ins0),保证原方法完整性。综上,inline hook需要完成的工作就是图中绿色的部分,即跳转指令的替换、补充执行原指令、跳回原方法继续执行这三步。
这个原理也是比较复杂,具体给大家分享一个链接 https://www.jianshu.com/p/2684e251124d
好了已经介绍了很多种,下面再来给大家最后介绍一种Hook最为结束的开胃菜吧,那就是通过ptrace的Hook,这种方式比较少见一来是实现比较复杂,二来稳定性也不是最高的,而且需要root的权限去实现,所以用得很少,但是我们还是来看看他是怎么实现的吧
他的原理就是利用ptrace附加进程进行调试,利用ARM汇编底层修改替换寄存器来实现Hook,可以简单的分为以下步骤:
- 1.先找到被注入进程的pid
- 2.附加当前进程到被注入进程
- 3.保存原寄存器的值
- 4.找到需要Hook的系统调用函数
- 5.修改目标进程寄存器
- 6.执行目标函数调用
- 7.恢复寄存器的值
- 8.分离附加进程
原理也是比较复杂的,细节大家可以看看我这两篇博客 :
已经讲得很到位了,绝对能帮助到大家了
介绍得差不多了,这篇博客也是把我这几年接触到的Hook基本上都写出来了,因为这些对于我来说在工作中都不是直接主要的技能,所以以后可能都不会再去写关于Hook的内容了,这个也是为什么副标题叫“绝口不再提”的原因,一开始是凭着兴趣入门摸索,一腔热血,没有任何人指导也坚持到了现在,今天也算是为了自己这么多年的做的总结吧
好了对于Android的Hook我们就介绍到这里了,上面的这些Hook技术都是我在工作中积累的内容了,如果能帮助到也是喜欢Hook技术的你的话那么我很是欣慰了,如果你还喜欢的话请帮忙点赞加关注,你的一键三连是我持续写作的动力。
在2022年最后一天这篇博客还是发表出来了,最后祝大家2023年元旦节心想事成,天天开心😁···