Android Hook 相对于Windows下面的hook操作,Android的hook主要是以java层以及native层为主。
native层hook 通过一个注入器,将我们的so动态链接库注入到我们想要注入的进程中去。 注入的方式有间接注入(zytoge注入)和直接注入(ptrace注入)为主,我这里用的是ADBI框架,比较简单,还不需要root。 从GitHub下载下来之后,我们可以得到两个主要的关键文件夹。
第一个使我们要编写的so文件,也就是想要被注入的动态链接库,第二个是我们的注入器,作者已经写好了,我们只需要用NDK进行编译就好,来到jni的目录,使用ndk-build就可以编译(环境变量需要设置好),然后他就会在hijack\obj\local\armeabi
目录下生成一个hijack文件,这个就是我们的注入器。 注入器得到之后,我们就需要编写我们的动态链接库,在这之前我们需要先获取到我们需要hook的函数,示例是之前那个TestJNI,分析java层之后,我们要hook的是encodestr这个函数。Java_com_example_testjni_NativeClass_encodestr
我们就是要hook这个函数,新建一个文件夹,拷贝一下example文件家中的内容: 首先打开后缀是_arm的那个c文件,里面的内容:
1 2 3 4 5 6 7 8 9 10 #include <jni.h> #include <sys/types.h> #include <sys/epoll.h> extern jstring my_Java_com_example_testjni_NativeClass_encodestr (JNIEnv * env, jobject jstr, jstring username, jstring password) ;jstring my_Java_com_example_testjni_NativeClass_encodestr_arm (JNIEnv * env, jobject jstr, jstring username, jstring password) { return my_Java_com_example_testjni_NativeClass_encodestr(env, jstr, username, password); }
声明的外部函数是我们的hook函数,然后又实现了一个后面是arm的函数,这两个函数的函数调用约定是要和我们的定义的一样的。 由于使用了JNI的函数,所以我们要导入标准的jni.h,这个文件就是这么简单,我们来到另外一个文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 #define _GNU_SOURCE #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <dlfcn.h> #include <sys/socket.h> #include <netinet/in.h> #include <sys/select.h> #include <string.h> #include <termios.h> #include <pthread.h> #include <sys/epoll.h> #include <jni.h> #include <stdlib.h> #include "../base/hook.h" #include "../base/base.h" #undef log #define log(...) \ {FILE *fp = fopen("/data/local/tmp/adbi_example.log" , "a+" ); if (fp) {\ fprintf (fp, __VA_ARGS__);\ fclose(fp);}} void __attribute__ ((constructor)) my_init(void );static struct hook_t eph ;static int counter;extern jstring my_Java_com_example_testjni_NativeClass_encodestr_arm (JNIEnv * env, jobject jstr, jstring username, jstring password) ;static void my_log (char *msg) { log ("%s" , msg) } jstring my_Java_com_example_testjni_NativeClass_encodestr (JNIEnv * env, jobject jstr, jstring username, jstring password) { jstring (*orig_testjni_NativeClass_encodestr)(JNIEnv *, jobject , jstring , jstring ); orig_testjni_NativeClass_encodestr = (void *)eph.orig; hook_precall(&eph); jstring res = orig_testjni_NativeClass_encodestr(env, jstr, (*env)->NewStringUTF(env, "a" ), (*env)->NewStringUTF(env, "b" )); if (counter) { hook_postcall(&eph); log ("encodestr() called\n" ); counter--; if (!counter) log ("removing hook for encodestr()\n" ); } return (*env)->NewStringUTF(env, "Hello Test NDK !" ); } void my_init (void ) { counter = 3 ; log ("%s started\n" , __FILE__) set_logfunction(my_log); hook(&eph, getpid(), "libTestJNI." , "Java_com_example_testjni_NativeClass_encodestr" , my_Java_com_example_testjni_NativeClass_encodestr_arm, my_Java_com_example_testjni_NativeClass_encodestr); }
首先定义了一个log函数,在/data/local/tmp/adbi_example.log
这个目录下面我们保存我们的日志。 然后下面的一些内容照葫芦画瓢就好。 比较关键的是my_Java_com_example_testjni_NativeClass_encodestr
这个函数和my_init
函数,这两个函数是关键,前一个就是我们的hook函数,里面的内容编写我们要hook写入的方法,这里我们直接返回随意一个字符串就好。 他接受了原先的函数,使用了hook_precall
这个样子的一个函数,具体这个函数是做什么的,没分析,反正他写上去了,咱就跟着写上去,然后下面的操作就根据需求来。my_init
函数,是一个入口函数,关键的是:
1 hook(&eph, getpid(), "libTestJNI." , "Java_com_example_testjni_NativeClass_encodestr" , my_Java_com_example_testjni_NativeClass_encodestr_arm, my_Java_com_example_testjni_NativeClass_encodestr);
这个函数,第一个参数是哪个结构体,第二个是当前的pid,第三个是你要hook的so,第四个是导出函数,第五个使我们在之前那个arm文件中实现的函数,最后一个是我们的hook函数。 最后我们还是用ndk-build编译那个Android.mk文件,最后会在lib文件加下面生成我们的so文件,当然我们要在之前生成我们的base文件,这个base文件夹是在instruments文件加下面的,同样使用NDK编译。 最后我们将我们的这个注入器和so文件导入我们的Android模拟器中。 我们在adb中启动我们的注入器,注入器使用规则:./hijack -d -p 21846 -l /data/local/tmp/libexample.so
21846使我们进程的PID,这里我们通过ddms获取,最后一个使我们的so文件,必须绝对路径(最好都给权限,还有那个log文件我们要线程手动生成一个,有没有内容不重要)。 可以看到结果:
1 2 3 4 5 6 7 8 9 mprotect: 0x400421ec dlopen: 0x40000ef9 pc=400433dc lr=400b1fb5 sp=bec14488 fp=bec1461c r0=fffffffc r1=bec144a8 r2=10 r3=1f0 stack: 0xbec00000-0xbec15000 leng = 86016 executing injection code at 0xbec14438 calling mprotect library injection completed!
看到这个之后我们就算是成功了。 使用IDA附加进程,来到我们的encodestr函数: 可以看到确实实现了hook。
java层hook 这里使用到的框架式Xposed这个框架,这个框架使用起来很简单,首先我们需要先安装我们的客户端: 安装完毕之后就可以了,当然要给root了,但是这个是可以在X86架构上的,因为是java层的。 安装完毕之后我们就需要导入我们的jar包: 导入这个包之后我们需要配置一下AndroidManifest.xml,需要在application
这个节点下面添加:
1 2 3 4 5 6 7 8 9 <meta-data android:name ="xposedmodule" android:value ="true" /> <meta-data android:name ="xposeddescription" android:value ="测试服务端" /> <meta-data android:name ="xposedminversion" android:value ="54" />
第一个是表明是xposed的模块,第二个是插件的名称,第三个是你使用的jar包的版本号。 声明好这三个节点之后,我们就需要对APK程序进行分析,这里为了方便我就直接用了源代码级别的,我们是要hook用户输入的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package com.example.passtest;import android.app.Activity;import android.os.Bundle;import android.util.Log;import android.view.View;import android.view.View.OnClickListener;import android.widget.Button;import android.widget.EditText;public class MainActivity extends Activity implements OnClickListener { public EditText et_username; public EditText et_password; public Button btn; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); et_username = (EditText) findViewById(R.id.username); et_password = (EditText) findViewById(R.id.password); btn = (Button) findViewById(R.id.soLogin); btn.setOnClickListener(this ); } public void onClick (View v) { switch (v.getId()) { case R.id.soLogin: Log.d("wker" , "aaa" ); break ; default : break ; } } }
我们的目的就是要获取到用户输入的内容,其实也就是et_password中的内容
分析完毕之后,接下来我们就需要新建一个类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package com.example.androidtestxposed;import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;import java.lang.reflect.Field;import android.util.Log;import android.view.View;import android.widget.EditText;import de.robv.android.xposed.IXposedHookLoadPackage;import de.robv.android.xposed.XC_MethodHook;import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam; public class TestMain implements IXposedHookLoadPackage { public void handleLoadPackage (LoadPackageParam lpparam) throws Throwable { Log.v("packageName" , lpparam.packageName); Log.d("wker1" ,lpparam.packageName); if (!lpparam.packageName.equals("com.example.passtest" )) return ; Log.d("wker" ,"into" ); findAndHookMethod("com.example.passtest.MainActivity" , lpparam.classLoader, "onClick" , View.class ,new XC_MethodHook () { @Override protected void afterHookedMethod (MethodHookParam param) throws Throwable { } @Override protected void beforeHookedMethod (MethodHookParam param) throws Throwable { Class clazz = param.thisObject.getClass(); Log.v("wker" , clazz.getName()); Field field = clazz.getField("et_password" ); EditText password = (EditText) field.get(param.thisObject); Log.v("wker" , password.getText().toString()); password.setText("ceshi" ); } }); } }
首先这个类要继承IXposedHookLoadPackage
这个接口,并且实现handleLoadPackage
方法,这个方法传进来一个LoadPackageParam
这个类的一个参数,这个我们是可以获取到包名的,通过packageName
这个成员,我们就通过包名来判断是不是我们想要hook的一个程序,不是的话呢返回。 判断是我们想要hook的程序之后,我们使用findAndHookMethod
这个样子的一个静态方法设置我们想要hook的类:
第一个参数是完整的类名
第二个参数是类的加载器。
第三个参数是类中想要hook的方法
第四个参数是hook的方法的参数
最后一个参数是我们new的一个内部类
第五个参数:beforeHookedMethod
实现这个方法是在执行原先函数之前我们需要做的操作,我们这里通过反射获取他的et_password,前两句我们的对象是否正确,发现没问题,第三个是通过我们获取到的这个对象中的成员,然后通过反射获取成员变量,然后最终就可以截获我们输入的密码。afterHookedMethod
这个方法是hook之后做的一些操作,但是我在测试的过程中发现,我在截取密码的时候,after如果有获取对象操作的时候,我们的操作就会失败befor也会出现错误,我觉的很是奇怪,之后具体分析XC_MethodReplacement
这个类是可以完全替换我们的函数方法,这里没有测试,之后具体分析,他也是XC_MethodHook
(第五个参数的一个子类)。 这里发现就可以hook成功了。 其实我们也可以看到,Class clazz = param.thisObject.getClass();
这个获取到的类也就是我们MainActivity这个类!
java层hook的实现原理 native层的hook和我们在Windows下面的hook道理其实差不多,但是java层的一个hook是通过反射基址进行hook的。
首先我们要确定我们要hook的类和相对应的方法。 这里就hook我们的点击事件就好。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button button = (Button) findViewById(R.id.hookClick); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick (View v) { Log.e("test" , "点击事件继续" ); } }); TextView tv = (TextView) findViewById(R.id.textView1); tv.setOnClickListener(new OnClickListener() { @Override public void onClick (View v) { Toast.makeText(MainActivity.this , "test" , Toast.LENGTH_SHORT).show();; } });
简单的一个点击。 想要hook OnclickListener这个方法。 首先我们先要分析这个内部类的一个实现:
1 2 3 4 5 6 public void setOnClickListener (OnClickListener l) { if (!isClickable()) { setClickable(true ); } getListenerInfo().mOnClickListener = l; }
其实这个setOnClickListerner这个方法就是将getListenerInfo()
获取到的对象返回回去:
1 2 3 4 5 6 7 ListenerInfo getListenerInfo () { if (mListenerInfo != null ) { return mListenerInfo; } mListenerInfo = new ListenerInfo(); return mListenerInfo; }
获取到他的对象之后,然后将其中的mOnClickListener
变量设置为我们的一个变量,分析到这里就明白了,那么我们就来hook。
1 2 3 Method method = View.class.getDeclaredMethod("getListenerInfo"); method.setAccessible(true ); Log.e("test" , "method=" + method.getName());
这一步我们先获取到getListenerInfo
这个方法,然后设置这个方法为可更改的。
1 2 Object mListenerInfo = method.invoke(button);
这个就是通过执行getListenerInfo
方法来获取到我们原先的一个对象(点击事件的),穿进去的参数是我们的按钮对象。
1 2 3 4 Class<?> classListenerInfo = Class.forName("android.view.View$ListenerInfo" ); Field field = classListenerInfo.getDeclaredField("mOnClickListener" );
获取到我们的对象之后我们就需要来获取我们的内部类,我们是需要通过这个内部类来获取到mOnClickListener
这个字段的。
1 2 3 final View.OnClickListener onClickListener = (View.OnClickListener) field.get(mListenerInfo);
我们获取到内部类中的mOnClickListener
字段之后,我们就通过这个字段获取我们真正的对象(也就是我们那个按钮点击监听器的一个对象,匿名内部类),然后将这个对象保存起来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Object proxyOnClickListener = Proxy.newProxyInstance(button.getClass().getClassLoader(), new Class[]{View.OnClickListener.class }, new InvocationHandler () { @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { Toast.makeText(MainActivity.this , "你点击我嘛,我很烦的!" , Toast.LENGTH_SHORT).show(); return method.invoke(onClickListener, args); } });
我们通过动态代理获取一下相对应的一个点击事件,可以看到和我们的xposed框架很相似:
第一个参数是我们hook的对象的一个类加载器
第二个是这个方法一个参数
第三个方法就是当执行到这个参数的时候我们想要执行的命令
最后一个参数实现invoke
方法,这个方法穿进去的第二三个参数比较重要,第二个参数是我们可以通过他获取原先的执行方法,感觉就是反射得到的,传进去我们之前获取到的对象和第三个参数的参数就可以了,然后我们就弹出一个Toast。 获取完毕之鸥,我们就替换掉之前的button事件:
1 2 field.set(mListenerInfo, proxyOnClickListener);
这样我们就实现了hook点击事件,以此类推,我们也可以hookTextView的一个点击事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 try { Method method = View.class.getDeclaredMethod("getListenerInfo"); method.setAccessible(true ); Object mLObject = method.invoke(tv); Class<?> cli = Class.forName("android.view.View$ListenerInfo" ); Field fi = cli.getDeclaredField("mOnClickListener" ); final View.OnClickListener oc = (OnClickListener) fi.get(mLObject); Object newOnClickListern = Proxy.newProxyInstance(tv.getClass().getClassLoader(),new Class[]{View.OnClickListener.class }, new InvocationHandler () { @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { Toast.makeText(MainActivity.this , "HOOK" , Toast.LENGTH_SHORT).show(); return method.invoke(oc,args); } }); fi.set(mLObject, newOnClickListern); } catch (Exception e) { e.printStackTrace(); }
这个的话呢和上面的大同小异。
这就是比较简单的Android HOOk操作,一些具体的等待分析!