移动安全-Hook技术

Hook简述

1、 hook的定义

hook,钩子。勾住系统的程序逻辑。在某段SDK源码逻辑执行的过程中,通过代码手段拦截执行该逻辑,加入自己的代码逻辑。

Hook 简单类似网络传输中的中间人拦截,我拦截APP中的原始方法,自己定义一个方法,替换原始的东西,实现我不可描述的目的,大白话就是这样,但是实际过程和应用还是比较复杂的。

常见的使用场景,举几个栗子:

  • App登录劫持,一般用户手动点击“登录”按钮才会将用户名和密码信息发送至服务器端去验证账号与密码是否正确。这样就很简单了,居心叵测的人只需要找到开发者在使用EditText控件的getText方法后进行网络验证的方法,Hook该方法,就能劫持到用户的账户与密码了。
  • App注入广告,app 启动的时候加载 HomeActivity 肯定要执行onCreate 方法, 劫持首页的onCreate方法,在里面注入弹窗广告,获得广告收入。
  • App数据篡改,比如玩一款游戏App ,修改里面额金币数,可以反编译App ,找到具体的类和方法,插入自己的方法就可以实现。

听起来很牛b,但是要实现,需要掌握很多的逆向技术和其它需要技术,可是非常不易的。

2、实用价值

  • hook是中级开发通往高级开发的必经之路。如果把谷歌比喻成安卓的造物主,那么安卓SDK源码里面就包含了万事万物的本源。中级开发者,只在利用万事万物,浮于表层,而高级开发者能从本源上去改变万事万物,深入核心。
  • 最有用的实用价值: hook是安卓面向切面(AOP)编程的基础,可以让我们在不变更原有业务的前提下,插入额外的逻辑。这样,既保护了原有业务的完整性,又能让额外的代码逻辑不与原有业务产生耦合。

3、前置技能

  • Java反射
    熟练掌握类Class,方法Method,成员Field的使用方法源码内部,很多类和方法都是@hide的,外部直接无法访问,所以只能通过反射,去创建源码中的类,方法,或者成员.
  • 阅读安卓源码的能力
    hook的切入点都在源码内部,不能阅读源码,不能理清源码逻辑,则不用谈hook。其实使用
    Android Studio来阅读源码有个坑,有时候会看到源码里面 “一片飘红”,看似是有什么东西没有引用进来,其实是因为有部分源码没有对开发者开放,解决起来很麻烦, 所以,推荐从安卓官网下载整套源码,然后使用 SourceInsight 查看源码。 如果不需要跳来跳去的话,直接用 安卓源码网站 一步到位。

关于Java反射的基础知识,请参见博文:Java-反射机制

4、hook通用思路

无论多么复杂的源码,我们想要干涉其中的一些执行流程,最终的杀招只有一个: “偷梁换柱”
而 “偷梁换柱”的思路,通常都是一个套路:

  • 根据需求确定 要hook的对象
  • 寻找要hook的对象的持有者,拿到要hook的对象(持有:B类 的成员变量里有 一个是A类的对象,那么B就是A的持有者)
  • 定义“要hook的对象”的代理类,并且创建该类的对象
  • 使用上一步创建出来的对象,替换掉要hook的对象

Firda框架搭建

frida是一款方便并且易用的跨平台Hook工具,使用它不仅可以Hook Java写的应用程序,而且还可以Hook原生的应用程序。Frida分客户端环境和服务端环境。假如我们要用PC来对Android设备上的某个进程进行操作,那么PC就是客户端,而Android设备就是服务端。

在客户端我们可以编写Python代码,用于连接远程设备,提交要注入的代码到远程,接受服务端的发来的消息等。在服务端,我们需要用Javascript代码注入到目标进程,操作内存数据,给客户端发送消息等操作。我们也可以把客户端理解成控制端,服务端理解成被控端。

客户端搭建

  1. 电脑端安装Python环境。下载安装Python,注意点击进去之后下面有个选项勾选,勾选上它就会自动帮你配置环境变量了。
  2. 安装完python之后在cmd输入python指令看看有没有成功,成功之后如下图。
    在这里插入图片描述
  3. 安装完Python,成功之后打开cmd,输入pip install frida,进行frida的安装。
    在这里插入图片描述
  4. 安装frida-tools。
    在这里插入图片描述
    至此,PC电脑端的配置完成。
    在这里插入图片描述

服务端搭建

我们需要在已root的手机上安装frida-server,并启动frida-server。服务端环境准备步骤如下:

  • 在 https://github.com/frida/frida/releases 选择版本下载。frida-server 是一个守护进程,通过TCP和Frida核心引擎通信,默认的监听端口是27042 。注意:版本和类型对应,框架和设备对应 。下载完后先解压。
    在这里插入图片描述
    选择与自己机器对应的版本:
    在这里插入图片描述
  • 执行以下命令将服务端推到手机的/data/local/tmp目录,并修改文件的权限,然后运行程序:
    在这里插入图片描述
    切换至Root用户并运行:
    在这里插入图片描述
  • 新开一个cmd。转发android tcp端口到PC端,完成之后我们输入frida-ps -U , Frida 检查手机正在运行的进程列表,出现下图则表示所有配置工作已完成。
    在这里插入图片描述

Adb Shell的使用

在这里插入图片描述
Android SDK下的Adb Shell工具如下图所示:
在这里插入图片描述
如何通过数据线在PC端使用adb shell命令操作手机呢?

  • 首先使用USB线连接智能手机,将USB连接方式设置为“MIDI”而不是仅充电,然后打开USB调试功能 http://www.pc6.com/video/727.html 。
  • 然后在PC端CDM命令终端输入“adb shell”即可连接手机:
    在这里插入图片描述

如何不通过数据线在PC端直接使用adb shell命令操作手机呢?

  • 首先保证手机跟电脑处于同一个无线网(处于同一网段,连接同一个Wifi);

  • 查询手机的IP地址:设置–关于手机–状态信息,就可以看到路由器分给手机的IP地址了;
    在这里插入图片描述

  • 需要在你的Android设备上下载一个apk,链接是您也可以到 http://jackpal.github.com/Android-Terminal-Emulator/downloads/Term.apk 下载或在互联网上搜索Android Terminal Emulator来下载安装此APK。

  • 下载后运行该apk,然后在手机的apk上输入以下的命令:
    在这里插入图片描述

  • 然后在PC端重新win+R打开命令行到sdk的plat_tools下,输入命令adb connect
    ****(**代表的是手机的ip地址,即可连接通过adb连接上手机,以后再也不用数据线了)
    在这里插入图片描述

  • 断开连接:adb disconnect 手机IP
    在这里插入图片描述
    Adb shell常用命令:https://www.cnblogs.com/JianXu/p/5161179.html 。

Firda实战案例

这是一个最简单的案例:我们自己的代码里面,给一个view设置了点击事件,现在要求在不改动这个点击事件的情况下,添加额外的点击事件逻辑。

   View v = findViewById(R.id.tv);
   v.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        Toast.makeText(MainActivity.this, "别点啦,再点我咬你了...", Toast.LENGTH_SHORT).show();
      }
   });

这是view的点击事件,toast了一段话,现在要求,不允许改动这个OnClickListener,要在toast之前添加日志打印 Log.d(...).

乍一看,无从下手。来看看看hook如何解决(按照上面的思路来):

  • 第一步:根据需求确定 要hook的对象

我们的目的是在OnClickListener中,插入自己的逻辑。所以,确定要hook的,是v.setOnClickListener()方法的实参。

  • 第二步:寻找要hook的对象的持有者,拿到要hook的对象

进入v.setOnClickListener源码:发现我们创建的OnClickListener对象被赋值给了getListenerInfo().mOnClickListener

public void setOnClickListener(@Nullable OnClickListener l) {
       if (!isClickable()) {
         setClickable(true);
      }
     getListenerInfo().mOnClickListener = l;
 }

继续索引:getListenerInfo() 是个什么玩意?继续追查:

ListenerInfo getListenerInfo() {
       if (mListenerInfo != null) {
           return mListenerInfo;
      }
     mListenerInfo = new ListenerInfo();
      return mListenerInfo;
 }  

结果发现这个其实是一个伪单例,一个View对象中只存在一个ListenerInfo对象。进入ListenerInfo内部:发现OnClickListener对象被ListenerInfo所持有。

static class ListenerInfo {
 ...
 public OnClickListener mOnClickListener;
 ...
}

到这里为止,完成第二步,找到了点击事件的实际持有者:ListenerInfo

  • 第三步:定义“要hook的对象”的代理类,并且创建该类的对象

我们要hook的是View.OnClickListener对象,所以,创建一个类 实现View.OnClickListener接口。

static class ProxyOnClickListener implements View.OnClickListener {
       View.OnClickListener oriLis;
       public ProxyOnClickListener(View.OnClickListener oriLis) {
           this.oriLis = oriLis;
      }
       @Override
      public void onClick(View v) {
          Log.d("HookSetOnClickListener", "点击事件被hook到了");
         if (oriLis != null) {
             oriLis.onClick(v);
         }
     }
 }

到这里为止,第三步:定义“要hook的对象”的代理类,并且创建该类的对象完成。

  • 第四步:使用上一步创建出来的对象,替换掉要hook的对象,达成 偷梁换柱的最终目的.

利用反射,将我们创建的代理点击事件对象,传给这个view:

    field.set(mListenerInfo, proxyOnClickListener);

这里,贴出最终代码:

/**
* hook的辅助类
* hook的动作放在这里
*/
public class HookSetOnClickListenerHelper {
   /**
    * hook的核心代码
    * 这个方法的唯一目的:用自己的点击事件,替换掉 View原来的点击事件
    *
    * @param v hook的范围仅限于这个view
    */
   public static void hook(Context context, final View v) {
       try {
           // 反射执行View类的getListenerInfo()方法,拿到v的mListenerInfo对象,这个对象就是点击事件的持有者
           Method method = View.class.getDeclaredMethod("getListenerInfo");
           //由于getListenerInfo()方法并不是public的,所以要加这个代码来保证访问权限
           method.setAccessible(true);
           //这里拿到的就是mListenerInfo对象,也就是点击事件的持有者
           Object mListenerInfo = method.invoke(v);
           //要从这里面拿到当前的点击事件对象,得到原始的OnClickListener对象
           Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");
           Field mOnClickListener = listenerInfoClz.getDeclaredField("mOnClickListener");      
		   mOnClickListener.setAccessible(true);
           View.OnClickListener originOnClickListener = (View.OnClickListener) 
                                                    mOnClickListener.get(mListenerInfo);
           // 用自定义的 OnClickListener 替换原始的 OnClickListener
           View.OnClickListener hookedOnClickListener = new HookedOnClickListener
                                                        (originOnClickListener);
           mOnClickListener.set(listenerInfo, hookedOnClickListener);
           //完成
       } catch (Exception e) {
           e.printStackTrace();
       }
   }

   //自定义代理类
   class HookedOnClickListener implements View.OnClickListener {
       private View.OnClickListener origin;
	   
       HookedOnClickListener(View.OnClickListener origin) {
            this.origin = origin;
       }
 
       @Override
       public void onClick(View v) {
           Log.d("HookSetOnClickListener", "点击事件被hook到了");
           if (oriLis != null) {
               oriLis.onClick(v);
           }
       }
   } 
}

效果展示
在这里插入图片描述
同时我并没有改动setOnClickListener的代码,我只是在它的后面,加了一行HookSetOnClickListenerHelper.hook(this, v)

 v.setOnClickListener(new View.OnClickListener() {
     @Override
     public void onClick(View v) {
       Toast.makeText(MainActivity.this, "别点啦,再点我咬你了...", Toast.LENGTH_SHORT).show();
     }
 });
 //这个hook的作用,是用我们自己创建的点击事件代理对象,替换掉之前的点击事件。
 HookSetOnClickListenerHelper.hook(this, v);

Ok,目的达成。v.setOnClickListener已经被成功hook了。

前方有坑,高能提示

我曾经尝试,是不是可以将上面两段代码换个顺序. 结果证明,换了之后,hook就不管用了,原因是,hook方法的作用,是将v已有的点击事件,替换成 我们代理的点击事件。所以,在v还没有点击事件的时候进行hook,是没用的。

发布了117 篇原创文章 · 获赞 84 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/weixin_39190897/article/details/91979456