HOOK技术的分析

很久没有发文章了,因为之前确实很多的事情,所以一直拖着,现在补一下在之前一篇文章中的尾巴,并且对常见的4个层次的hook技术进行一一介绍,文章可能会很长,希望大家能在看完的时候给我点个赞!嘻嘻嘻。
之前的那个尾巴就是在外挂分析的时候留下的,我们如何过掉Windows驱动保护,当然我这里只是浅谈,并不会讲所有方法进行一一的介绍,因为文章篇幅的限制,而且我的能力也并不是所有的驱动保护都能顺利过掉。
如果文章存在什么错误,希望大家多多指正!

文章简单的目录:

  1. Android hook
    1. so动态链接库函数的hook
    2. java层的反射hook
  2. Windows hook
    1. 用户层hook
      1. 导出表hook
      2. inline hook
    2. 驱动层hook
      1. SSDT表hook(我就不写inline hook了我的电脑真经不起我折腾了,蓝屏宝贝已经多次与我见面了,写文章的时候我就听着我的风扇简直就要转出来了)
      2. 过掉驱动层的hook保护

Android Hook

相对于Windows下面的hook操作,Android的hook主要是以java层以及native层为主。

native层hook

通过一个注入器,将我们的so动态链接库注入到我们想要注入的进程中去。
注入的方式有间接注入(zytoge注入)和直接注入(ptrace注入)为主,我这里用的是ADBI框架,比较简单,还不需要root。
从GitHub下载下来之后,我们可以得到两个主要的关键文件夹。

  • instruments
  • hijack

第一个使我们要编写的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文件,里面的内容:

#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,这个文件就是这么简单,我们来到另外一个文件:


#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);}}


// this file is going to be compiled into a thumb mode binary

void __attribute__ ((constructor)) my_init(void);

static struct hook_t eph;

// for demo code only
static int counter;

// arm version of hook
extern jstring my_Java_com_example_testjni_NativeClass_encodestr_arm(JNIEnv * env, jobject jstr, jstring username, jstring password);

/*  
 *  log function to pass to the hooking library to implement central loggin
 *
 *  see: set_logfunction() in base.h
 */
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");
	}
	//char fail[] = "wkertest";
    return (*env)->NewStringUTF(env, "Hello Test NDK !");
	//return res;
}

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函数,是一个入口函数,关键的是:

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文件我们要线程手动生成一个,有没有内容不重要)。
可以看到结果:

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包:
jar包
导入这个包之后我们需要配置一下AndroidManifest.xml,需要在application这个节点下面添加:

<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用户输入的内容:

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:
			//Toast.makeText(MainActivity.this, "ttttttt", Toast.LENGTH_SHORT).show();
			Log.d("wker", "aaa");
			break;

		default:
			break;
		}
	}

}

我们的目的就是要获取到用户输入的内容,其实也就是et_password中的内容

分析完毕之后,接下来我们就需要新建一个类:

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 {
				/*Class clazz = param.thisObject.getClass();
				Log.v("wker", clazz.getName());*/
			}
			
			@Override
			protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
				/*
				TextView tv = (TextView) param.thisObject;
				String text = tv.getText().toString();
				tv.setText(text + " :)");
				tv.setTextColor(Color.RED);
				Log.v("redcolock","redcolock");*/
				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());
				//Toast.makeText((Activity)param.thisObject, password.getText().toString(), Toast.LENGTH_SHORT).show();
				password.setText("ceshi");
			}
		});
	}
}

首先这个类要继承IXposedHookLoadPackage这个接口,并且实现handleLoadPackage方法,这个方法传进来一个LoadPackageParam这个类的一个参数,这个我们是可以获取到包名的,通过packageName这个成员,我们就通过包名来判断是不是我们想要hook的一个程序,不是的话呢返回。
判断是我们想要hook的程序之后,我们使用findAndHookMethod这个样子的一个静态方法设置我们想要hook的类:

  1. 第一个参数是完整的类名
  2. 第二个参数是类的加载器。
  3. 第三个参数是类中想要hook的方法
  4. 第四个参数是hook的方法的参数
  5. 最后一个参数是我们new的一个内部类
    • 内部类可以实现hook的方法。

第五个参数:
beforeHookedMethod实现这个方法是在执行原先函数之前我们需要做的操作,我们这里通过反射获取他的et_password,前两句我们的对象是否正确,发现没问题,第三个是通过我们获取到的这个对象中的成员,然后通过反射获取成员变量,然后最终就可以截获我们输入的密码。
afterHookedMethod这个方法是hook之后做的一些操作,但是我在测试的过程中发现,我在截取密码的时候,after如果有获取对象操作的时候,我们的操作就会失败befor也会出现错误,我觉的很是奇怪,之后具体分析
XC_MethodReplacement这个类是可以完全替换我们的函数方法,这里没有测试,之后具体分析,他也是XC_MethodHook(第五个参数的一个子类)。


这里发现就可以hook成功了。
log信息
其实我们也可以看到,Class clazz = param.thisObject.getClass();这个获取到的类也就是我们MainActivity这个类!

java层hook的实现原理

native层的hook和我们在Windows下面的hook道理其实差不多,但是java层的一个hook是通过反射基址进行hook的。

首先我们要确定我们要hook的类和相对应的方法。
这里就hook我们的点击事件就好。

@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这个方法。
首先我们先要分析这个内部类的一个实现:

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

其实这个setOnClickListerner这个方法就是将getListenerInfo()获取到的对象返回回去:

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

获取到他的对象之后,然后将其中的mOnClickListener变量设置为我们的一个变量,分析到这里就明白了,那么我们就来hook。

Method method = View.class.getDeclaredMethod("getListenerInfo");
method.setAccessible(true);
Log.e("test", "method=" + method.getName());

这一步我们先获取到getListenerInfo这个方法,然后设置这个方法为可更改的。

// 获取Button的ListenerInfo对象mListenerInfo
Object mListenerInfo = method.invoke(button);

这个就是通过执行getListenerInfo方法来获取到我们原先的一个对象(点击事件的),穿进去的参数是我们的按钮对象。

// 内部类需要使用$分隔
Class<?> classListenerInfo = Class.forName("android.view.View$ListenerInfo");
// 获取内部Field mOnClickListener
Field field = classListenerInfo.getDeclaredField("mOnClickListener");

获取到我们的对象之后我们就需要来获取我们的内部类,我们是需要通过这个内部类来获取到mOnClickListener这个字段的。

// 然后获取Button的ListenerInfo对象mListenerInfo的mOnClickListener变量
// --这就是真正的拿到了Button的监听回调View.OnClickListener的实例对象
final View.OnClickListener onClickListener = (View.OnClickListener) field.get(mListenerInfo);

我们获取到内部类中的mOnClickListener字段之后,我们就通过这个字段获取我们真正的对象(也就是我们那个按钮点击监听器的一个对象,匿名内部类),然后将这个对象保存起来。

// 然后准备替换为我们自己的点击事件
// 1. 创建代理点击对象,然后替换 (这里继承接口实现一个类也可以)
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框架很相似:

  1. 第一个参数是我们hook的对象的一个类加载器
  2. 第二个是这个方法一个参数
  3. 第三个方法就是当执行到这个参数的时候我们想要执行的命令

最后一个参数实现invoke方法,这个方法穿进去的第二三个参数比较重要,第二个参数是我们可以通过他获取原先的执行方法,感觉就是反射得到的,传进去我们之前获取到的对象和第三个参数的参数就可以了,然后我们就弹出一个Toast。
获取完毕之鸥,我们就替换掉之前的button事件:

// 2. 然后替换掉Button的点击事件
field.set(mListenerInfo, proxyOnClickListener);

这样我们就实现了hook点击事件,以此类推,我们也可以hookTextView的一个点击事件:

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操作,一些具体的等待分析!

Windows Hook

Windows的话呢主要分为用户层hook和驱动层hook

API钩子

其实准确的说不是通过SetWindowsHookEx这个函数进行设置钩子,因为这个函数似乎实现不了接管函数的效果,那么该如何才能做到接管其他程序的函数是一个比较关键的一点。

进程保护程序

这是一个比较典型的例子,就是在程序想要调用TerminateProcess异常结束其他进程的时候,我们将它给干掉,看一下实现效果:

但是在我测试的过程中,MFC程序只要被DLL加载之后就会接受消息出错,类似按钮消息之类的一些消息,但易语言是没什么问题的。

实现方法

其实结合之前的例子,基本上可以说我们已经可以完成了,我们要HOOK程序的某个API函数并且更改某个程序的内容,那么我们就需要有一块内存区域是在这个程序中,所以我们第一步就是需要将我们写好的DLL注入进去,注入的方法有很多,这里是通过消息处理的方法进行注入的。将我们的DLL注入后我们就读取注入程序的IAT,将我们想要修改的函数指向地址修改为我们的相同堆栈格式的函数替换进去(传参类型和返回值占用字节数不一样的话呢会出错的,这种错一般在写Call自动打怪的时候会常见),但是还有一种情况我们等下说。

CAPIHook类的实现

首先我们要明确,我们要HOOK的函数可能在用户编写程序的过程中会有不少,特别是那种EX,A,W这些版本的,所以我们需要给用户一个比较好的体验的话呢我们需要构造一个链表的结构,比较简单的就是定义一个静态的头指针,然后我们要钩的时候我们就添加就好了,为什么要这么样,我们之后再说,看下我们头文件的定义:

#ifndef __APIHOOK_H
#define __APIHOOK_H
#include <Windows.h>

class CAPIHook
{
public:
	CAPIHook(LPSTR pszModName,LPSTR pszFuncName,PROC pfnHook,BOOL bExcludeAPIHookMod = TRUE);
	virtual ~CAPIHook();//因为有子类
	operator PROC(){return m_pfnOrig;}

private:
	LPSTR m_pszModName;
	LPSTR m_pszFuncName;
	PROC m_pfnOrig;
	PROC m_pfnHook;
	BOOL m_bExcludeAPIHookMod;
private:
	static void ReplaceIATEntryInAllMods(LPSTR pszExportMod,PROC pfnCurrent,PROC pfnNew,BOOL bExcludeAPIHookMod);
	static void ReplaceIATEntryInOneMod(LPSTR pszExportMod,PROC pfnCurrent,PROC pfnNew,HMODULE hModCaller);

//下面代码用来解决其他模块动态加载DLL的
private:
	//这两个指针用来将所有的CAPIHook连接器
	static CAPIHook *sm_pHeader;
	CAPIHook *m_pNext;
private:
	//当一个新的DLL被加载的时候调用
	static void WINAPI HookNewlyLoadedModule(HMODULE hModule,DWORD dwFlags);
	//用来追踪当前进程加载新的DLL
	static HMODULE WINAPI LoadLibraryA(PCSTR pszModulePath);
	static HMODULE WINAPI LoadLibraryW(PCWSTR pszModulePath);
	static HMODULE WINAPI LoadLibraryExA(PCSTR pszModulePath,HANDLE hFile,DWORD dwFlags);
	static HMODULE WINAPI LoadLibraryExW(PCWSTR pszModulePath,HANDLE hFile,DWORD dwFlags);
	//如果求情已HOOK的API函数,则返回用户自定义的函数的地址
	static FARPROC WINAPI GetProcAddress(HMODULE hModule,LPCSTR lpProcName);
private:
	//自动挂钩的函数
	static CAPIHook sm_LoadLibraryA;
	static CAPIHook sm_LoadLibraryW;
	static CAPIHook sm_LoadLibraryExA;
	static CAPIHook sm_LoadLibraryExW;
	static CAPIHook sm_GetProcAddress;
};
#endif __APIHOOK_H

  1. 我们构造函数中要传入的是模块名称,函数名称,要替换成的函数的地址,是否要Hook本模块的。
  2. 析构函数因为如果有子类的话呢我们就要用到虚函数,否则释放不完全,里面将被替换的函数再替换回去
  3. 重载了PROC这个操作符,便于操作,在写DLL的时候有个地方是要用到的
  4. 几个私有变量:
    1. 模块名称
    2. 函数原地址(用于替换会去)
    3. 函数HOOK的地址
    4. 是否要包含我们自己的模块
  5. 两个静态函数,一个是替换一个模块的一个函数,另一个是替换所有模块的这个函数
  6. 一个静态成员,用来加载链表的,一个下一个节点的地址

下面就需要注意了,由于某些函数是在运行中动态的加载DLL的,所有有的时候我们就是需要是否要加载DLL了,所以我们不仅要HOOK用户钩住的函数还需要HOOK住动态加载DLL的函数和获取函数地址的函数。各个版本的都需要HOOK住

  1. HookNewlyLoadedModule这个函数用来将我们新加载的模块的进行重新HOOK一遍。
  2. 接下来的几个静态函数是新的替换掉的函数。
  3. 下面几个静态变量是用来HOOK上面这几个静态函数的

类的定义

#include "stdafx.h"
#include "CAPIHook.h"
#include "TlHelp32.h"
#include <ImageHlp.h>//调用那个查询IAT的函数,方便罢了
#pragma comment(lib,"ImageHlp")


//CAPIHook对象链表的头指针
CAPIHook* CAPIHook::sm_pHeader = NULL;

CAPIHook::CAPIHook(LPSTR pszModName,LPSTR pszFuncName,PROC pfnHook,BOOL bExcludeAPIHookMod /* = TRUE */)
{
	//把偶才能这个HOOK信息
	m_bExcludeAPIHookMod = bExcludeAPIHookMod;
	m_pszFuncName = pszFuncName;
	m_pszModName = pszModName;
	m_pfnHook = pfnHook;
	m_pfnOrig = GetProcAddress(GetModuleHandle(m_pszModName),m_pszFuncName);
	//将对象添加到链表
	m_pNext = sm_pHeader;
	sm_pHeader = this;
	ReplaceIATEntryInAllMods(m_pszModName,m_pfnOrig,m_pfnHook,m_bExcludeAPIHookMod);
}
CAPIHook::~CAPIHook()
{
	ReplaceIATEntryInAllMods(m_pszModName,m_pfnHook,m_pfnOrig,m_bExcludeAPIHookMod);
	CAPIHook *p = sm_pHeader;
	if (p == this)
	{
		sm_pHeader = sm_pHeader->m_pNext;
	}else
	{
		while (p != NULL)
		{
			if (p->m_pNext == this)
			{
				p->m_pNext = this->m_pNext;
				break;
			}
			p = p->m_pNext;
		}
	}
}
//挂钩这些后期加载的函数
CAPIHook CAPIHook::sm_LoadLibraryA("kernel32.dll","LoadLibraryA",(PROC)CAPIHook::LoadLibraryA,TRUE);
CAPIHook CAPIHook::sm_LoadLibraryW("kernel32.dll","LoadLibraryW",(PROC)CAPIHook::LoadLibraryW,TRUE);
CAPIHook CAPIHook::sm_LoadLibraryExA("kernel32.dll","LoadLibraryExA",(PROC)CAPIHook::LoadLibraryExA,TRUE);
CAPIHook CAPIHook::sm_LoadLibraryExW("kernel32.dll","LoadLibraryExW",(PROC)CAPIHook::LoadLibraryExW,TRUE);
CAPIHook CAPIHook::sm_GetProcAddress("kernel32.dll","GetProcAddress",(PROC)CAPIHook::GetProcAddress,TRUE);

void WINAPI CAPIHook::HookNewlyLoadedModule(HMODULE hModule,DWORD dwFlags)
{
	//新的API被加载,挂钩各CAPIHook对象要求的API函数
	if ((hModule != NULL) && ((dwFlags && LOAD_LIBRARY_AS_DATAFILE) == 0))//映射的方式到内存
	{
		CAPIHook *p = sm_pHeader;
		ReplaceIATEntryInOneMod(p->m_pszModName,p->m_pfnOrig,p->m_pfnHook,hModule);
		p = p->m_pNext;
	}
}

HMODULE WINAPI CAPIHook::LoadLibraryA(PCSTR pszModulePath)
{
	HMODULE hModule = ::LoadLibraryA(pszModulePath);
	HookNewlyLoadedModule(hModule,0);
	return (hModule);
}
HMODULE WINAPI CAPIHook::LoadLibraryW(PCWSTR pszModulePath)
{
	HMODULE hModule = ::LoadLibraryW(pszModulePath);
	HookNewlyLoadedModule(hModule,0);
	return (hModule);
}
HMODULE WINAPI CAPIHook::LoadLibraryExA(PCSTR pszModulePath,HANDLE hFile,DWORD dwFlags)
{
	HMODULE hModule = ::LoadLibraryExA(pszModulePath,hFile,dwFlags);
	HookNewlyLoadedModule(hModule,dwFlags);
	return (hModule);
}
HMODULE WINAPI CAPIHook::LoadLibraryExW(PCWSTR pszModulePath,HANDLE hFile,DWORD dwFlags)
{
	HMODULE hModule = ::LoadLibraryExW(pszModulePath,hFile,dwFlags);
	HookNewlyLoadedModule(hModule,dwFlags);
	return (hModule);
}

FARPROC WINAPI CAPIHook::GetProcAddress(HMODULE hModule,LPCSTR lpProcName)
{
	//得到真实的函数地址
	FARPROC pfn = ::GetProcAddress(hModule,lpProcName);
	//检查是否是我们需要的函数

	CAPIHook *p = sm_pHeader;

	while(p != NULL)
	{
		if (p->m_pfnOrig == pfn)
		{
			pfn = p->m_pfnHook;
			break;
		}
		p = p->m_pNext;
	}
	return pfn;
}

void CAPIHook::ReplaceIATEntryInOneMod(LPSTR pszExportMod,PROC pfnCurrent,PROC pfnNew,HMODULE hModCaller)
{
	//获取导入表地址
	ULONG ulSize;
	PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToData(hModCaller,TRUE,IMAGE_DIRECTORY_ENTRY_IMPORT,&ulSize);
	if (pImportDesc == NULL)
		return ;
	while(pImportDesc->Name !=0 )
	{
		LPSTR pszMod = (LPSTR)((DWORD)hModCaller+pImportDesc->Name);
		if(stricmp(pszExportMod,pszMod) == 0)
			break;
		pImportDesc++;
	}
	if(pImportDesc->Name == 0)
		return;
	//取导入表的地址
	PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)((DWORD)hModCaller+pImportDesc->FirstThunk);
	while(pThunk->u1.Function)
	{
		PDWORD lpAddr = (PDWORD)&(pThunk->u1.Function);
		if (*lpAddr == (DWORD)pfnCurrent)
		{
			DWORD dwWrite;
			/*修改页面保护属性*/
			DWORD dwOldProect;
			MEMORY_BASIC_INFORMATION mbi;
			VirtualQuery(lpAddr,&mbi,sizeof(mbi));
			VirtualProtect(lpAddr,sizeof(DWORD),PAGE_READWRITE,&dwOldProect);
			WriteProcessMemory(GetCurrentProcess(),lpAddr,&pfnNew,sizeof(DWORD),&dwWrite);
			VirtualProtect(lpAddr,sizeof(DWORD),dwOldProect,0);
			return ;
		}
		pThunk++;
	}
}

void CAPIHook::ReplaceIATEntryInAllMods(LPSTR pszExportMod,PROC pfnCurrent,PROC pfnNew,BOOL bExcludeAPIHookMod)
{
	HMODULE hModThis = NULL;
	if (bExcludeAPIHookMod)
	{
		MEMORY_BASIC_INFORMATION mbi;
		if (VirtualQuery(ReplaceIATEntryInAllMods,&mbi,sizeof(mbi)) != 0)
			hModThis = (HMODULE)mbi.AllocationBase;
	}
	HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE,GetCurrentProcessId());

	MODULEENTRY32 me = {sizeof(MODULEENTRY32)};
	BOOL bOk = Module32First(hSnap,&me);
	while(bOk)
	{
		if(me.hModule != hModThis)
			ReplaceIATEntryInOneMod(pszExportMod,pfnCurrent,pfnNew,me.hModule);
		bOk = Module32Next(hSnap,&me);
	}
	CloseHandle(hSnap);
}
  1. 首先源文件中先将我们我们的头指针设置为NULL
  2. 在构造函数的时候我们现将我们的HOOK信息初始化一下,然后用下面将要写的替换所有模块的函数替换,然后将自己设置为链表顶端。
  3. 析构函数的时候我们将所有的模块替换回去,然后从链表中将自身删除。
  4. 然后初始化我们的挂钩函数
  5. HookNewlyLoadedModule这个函数是被下面的加载模块调用的,第二个Flags是在EX版本用到的,当存在LOAD_LIBRARY_AS_DATAFILE这样子的一个标志位的时候说明是内存镜像的方式进行载入的,所以我们需要将其替换一下。
  6. 下面几个LoadLibrary(Ex)类型的函数大同小异。
  7. GetProcAddress这个函数的时候是检查是否有我们要的函数,如果有的话呢就返回我们设置好的函数地址。
  8. 上面这几个默认挂钩的函数是全局的不要忘记加::,否则会进入无限递归。
  9. ReplaceIATEntryInOneMod这个函数我们之前写过,之前用的是PE的一个就够进行获取输入表的,我们这里使用ImageDirectoryEntryToData这个函数,这个函数不是很难,第二个标志位是需要注意的,我们要的是IAT,而且这个函数需要导入:#include <ImageHlp.h>,并且加载#pragma comment(lib,"ImageHlp")这个动态链接库。还需要注意的是我们需要修改内存的保护属性有的时候才能写IAT的内容,变成可读可写的内存块。还需要注意的是DLL大小写是不一样的,所以我们使用stricmp这个不区分大小写。
  10. ReplaceIATEntryInAllMods这个就是通过遍历当前进程所有的模块句柄,然后传进去,(进入上一个函数,判断有相同模块名称的然后就继续做)。

DLL模块的编写

// ProcessHook.cpp : 定义 DLL 应用程序的导出函数。
//

#include "stdafx.h"

#include "CAPIHook.h"

extern CAPIHook g_TerminateProcess;

BOOL WINAPI Hook_TerminateProcess(HANDLE hProcess,UINT uExitCode)
{
	typedef BOOL (WINAPI *PFNTERMINATEPROCESS)(HANDLE ,UINT);

	char szPathName[MAX_PATH];
	GetModuleFileName(NULL,szPathName,MAX_PATH);

	char sz[2048];
	wsprintf(sz,"\r\n 进程:(%d) %s \r\n\r\n 进程句柄: %X\r\n退出代码: %d",GetCurrentProcessId(),szPathName,hProcess,uExitCode);

	COPYDATASTRUCT cds = {GetCurrentProcessId(),strlen(sz)-1,sz};
	if (::SendMessage(FindWindow(NULL,"ProcessProtect"),WM_COPYDATA,0,(LPARAM)&cds) != -1)
		return ((PFNTERMINATEPROCESS)(PROC)g_TerminateProcess)(hProcess,uExitCode);
	return TRUE;
}
CAPIHook g_TerminateProcess("kernel32.dll","TerminateProcess",(PROC)Hook_TerminateProcess);

#pragma data_seg("Wker")
HHOOK g_hHook = NULL;
#pragma data_seg()

static HMODULE ModuleFromAddress(PVOID pv)
{
	MEMORY_BASIC_INFORMATION mbi;
	if (VirtualQuery(pv,&mbi,sizeof(mbi)) != 0)
	{
		return (HMODULE)mbi.AllocationBase;
	}else
	{
		return NULL;
	}
}
LRESULT CALLBACK GetMsgProc(  int code,       // hook code
	WPARAM wParam,  // removal flag
	LPARAM lParam   // address of structure with message
)
{
	return CallNextHookEx(g_hHook,code,wParam,lParam);
}

BOOL WINAPI SetSysHook(BOOL bInstall,DWORD dwThread)
{
	BOOL bOk;
	if (bInstall)
	{
		g_hHook = SetWindowsHookEx(WH_GETMESSAGE,GetMsgProc,ModuleFromAddress(GetMsgProc),dwThread);
		bOk = (g_hHook != NULL);
	}else
	{
		bOk = UnhookWindowsHookEx(g_hHook);
		g_hHook = NULL;
	}
	return bOk;
}

首先这个extern我不太明白,但是根据我的感觉就是说使用这个变量g_TerminateProcess我们要遵守这个变量的一些命名规范,只有这样我们下面的强制转换才能返回我们的origin。

我们先定义我们自己的TerminateProcess,先将这个函数的原型定义出来,然后获取模块名称,然后配置字符串,下面这个之前没用到过,COPYDATASTRUCT这个就是一个数据传输的结构体:“其中dwData为32位的自定义数据, lpData为指向数据的指针,cbData为lpData指针指向数据的大小(字节数)。”从网上看到的,其实就是用来传送数据的,主要最后一个参数,我们将我们的字符串放进去就好。然后再给我们的窗口发消息,这个窗口是我们必须定义好的,如果SendMessage返回给我们-1,就是放行,我们就正常执行,这里可以看到使用了强制转换,(PROC)我们将会得到原先函数的地址,然后将这个PROC强制转换成我们的这个函数的原型,然后再调用就好了。

我们声明我们的全局变量,因为全局,所以一加载我们就会创建,这种感觉就好像MFC和安卓的那种感觉,在构造函数里写过程。
然后定义一个数据段,名字为"Wker",将我们的钩子句柄放进去,下面就基本一样了,只不过我们在消息那里我们什么也不做,这里放钩子只是为了注入DLL,因为Message在可视化应用程序中基本都有。

EXPORTS
	SetSysHook

SECTIONS
	Wker Read Write Shared

导出配置就和之前一样。

主程序编写

这个主程序就很简单了,有了DLL就好说了。
首先我们先把钩子函数给完善起来。

BOOL WINAPI pSetSysHook(BOOL bInstall,DWORD dwThread = 0)
{
	typedef BOOL (WINAPI *PFNSETSYSHOOK)(BOOL,DWORD);
	char szDll[] = "ProcessHook.dll";
	BOOL bNeedFree = FALSE;
	HMODULE hModule = GetModuleHandle(szDll);
	if (hModule == NULL)
	{
		hModule = LoadLibrary(szDll);
		bNeedFree = TRUE;
	}
	PFNSETSYSHOOK mSetSysHook = (PFNSETSYSHOOK)GetProcAddress(hModule,"SetSysHook");
	if (mSetSysHook == NULL)
	{
		if(bNeedFree)
			FreeLibrary(hModule);
		return FALSE;
	}
	BOOL bRet = mSetSysHook(bInstall,dwThread);

	if(bNeedFree)
		FreeLibrary(hModule);
	return bRet;
}

就是和之前一样,但是我们这个是动态加载的,我们是先看看加没加载,加载了的话呢我们就不用了再LoadLibrary了,也就是我们要释放了。

消息处理:

BOOL CProcessProtectDlg::OnCopyData(CWnd* pWnd, COPYDATASTRUCT* pCopyDataStruct)
{
	m_HookList.AddString((char *)pCopyDataStruct->lpData);
	BOOL bForBid = ((CButton*)GetDlgItem(IDC_FORBIDEXE))->GetCheck();
	if(bForBid)
		return -1;
	return TRUE;
	//return CDialogEx::OnCopyData(pWnd, pCopyDataStruct);
}

这个消息处理也比较简单就一个强制转换,但是返回值需要注意,这个返回值是确定我们是不是要关闭程序的,连接在DLL那一边的。

初始化窗口的时候:

if(!pSetSysHook(TRUE,0))
	MessageBox("钩子加载失败");

销毁窗口的时候:

void CProcessProtectDlg::OnDestroy()
{
	CDialogEx::OnDestroy();

	pSetSysHook(FALSE);
}

也是比较简单的。

程序跟踪

首先我们先用OD加载程序(加载的是关闭程序的)
首先先看我们的程序原先的函数定义位置为:


当我们程序加载进来的时候:

我们可以看到DLL成功加载,当然这里我是为了方便,用OD加载的,程序也是一样的。
再看我们的函数地址:

我们跳过去看一下。

可以看到就是我们的函数,而且确实是在我们的DLL领空。

CULHook类

这个类写起来时很暴力的,和书上说的一样,我喜欢。
思路也比较简单粗暴,就是将我们的API指向地址的前八个字节写成:

mov eax,0xXXXX
jmp eax

也就是:
汇编代码
这里比较难搞的就是地址我们怎么赋值,可能有人就要考虑到一个大端小段,但是我们强转成DWOD类型就可以解决这个困惑了。

头文件:

#ifndef __ULHOOK_H__
#define __ULHOOK_H__

#include <Windows.h>

class CULHook
{
public:
	CULHook(LPSTR pszModName,LPSTR pszFuncName,PROC pfnHook);//这个新的函数是需要naked的,不要处理堆栈平衡
	~CULHook();
	void Unhook();
	void Rehook();

protected:
	PROC m_pfnOrig;
	BYTE m_btNewBytes[8];
	BYTE m_btOldBytes[8];
	HMODULE m_hModule;
};

#endif //__ULHOOK_H__

这里就一个构造函数就可以进行HOOK了,但是非常注意的就是由于我们是直接暴力的写汇编,所以我们是不能要堆栈操作的。

源文件

构造函数:

CULHook::CULHook(LPSTR pszModName,LPSTR pszFuncName,PROC pfnHook)
{
	BYTE btNewBytes[8] = {0xB8,0x00,0x00,0x40,0x00,0xFF,0xE0,0x00};
	memcpy(m_btNewBytes,btNewBytes,8);
	*(DWORD *)(m_btNewBytes+1) = (DWORD)pfnHook;
	m_hModule = LoadLibrary(pszModName);
	if (m_hModule == NULL)
	{
		m_pfnOrig = NULL;
		return ;
	}
	m_pfnOrig = GetProcAddress(m_hModule,pszFuncName);

	if (m_pfnOrig != NULL)
	{
		DWORD dwOldProtect;
		MEMORY_BASIC_INFORMATION mbi;
		VirtualQuery(m_pfnOrig,&mbi,sizeof(mbi));
		VirtualProtect(m_pfnOrig,8,PAGE_READWRITE,&dwOldProtect);
		memcpy(m_btOldBytes,m_pfnOrig,8);
		WriteProcessMemory(GetCurrentProcess(),(LPVOID)m_pfnOrig,m_btNewBytes,sizeof(DWORD)*2,NULL);
		VirtualProtect(m_pfnOrig,8,dwOldProtect,0);
	}
}

首先将我们的汇编代码改一下,这里可以看到非常的巧妙,先转成DWORD然后再赋值,然后我们再将2,3,4,5这四个字节替换成我们的地址,这个是真的巧妙,之后我们就获取要HOOK函数的地址,再将这个地址的前八个字节的保护属性改掉,改完之后,写我们的这八个字节。

取消HOOK和重新HOOK:

void CULHook::Unhook()
{
	if (m_pfnOrig != NULL)
	{
		DWORD dwOldProtect;
		MEMORY_BASIC_INFORMATION mbi;
		VirtualQuery(m_pfnOrig,&mbi,sizeof(mbi));
		VirtualProtect(m_pfnOrig,8,PAGE_READWRITE,&dwOldProtect);
		memcpy(m_btOldBytes,m_pfnOrig,8);
		WriteProcessMemory(GetCurrentProcess(),(LPVOID)m_pfnOrig,m_btOldBytes,sizeof(DWORD)*2,NULL);
		VirtualProtect(m_pfnOrig,8,dwOldProtect,0);
	}
}

void CULHook::Rehook()
{
	if (m_pfnOrig != NULL)
	{
		DWORD dwOldProtect;
		MEMORY_BASIC_INFORMATION mbi;
		VirtualQuery(m_pfnOrig,&mbi,sizeof(mbi));
		VirtualProtect(m_pfnOrig,8,PAGE_READWRITE,&dwOldProtect);
		memcpy(m_btOldBytes,m_pfnOrig,8);
		WriteProcessMemory(GetCurrentProcess(),(LPVOID)m_pfnOrig,m_btNewBytes,sizeof(DWORD)*2,NULL);
		VirtualProtect(m_pfnOrig,8,dwOldProtect,0);
	}
}

取消HOOK和重新HOOK就是一样,也就是修改前八个字节。

析构函数就是先取消HOOK,然后再释放DLL句柄

CULHook::~CULHook()
{
	Unhook();
	if(m_hModule != NULL)
		FreeLibrary(m_hModule);
}

驱动的hook

相比Win32API的hook,这个貌似更加的流氓一些,其实原理是这样子的(下面的例子用OpenProcess举例),在我们调用OpenProcess函数的时候会调用ZwOpenProcess函数,还是通过汇编代码分析一下吧(随便找个程序进去就行),这样更清楚:

73975A00 >  8BFF            mov edi,edi                              ; OpenProc.<ModuleEntryPoint>
73975A02    55              push ebp
73975A03    8BEC            mov ebp,esp
73975A05    5D              pop ebp                                  ; kernel32.73978494
73975A06  - FF25 F8149E73   jmp dword ptr ds:[<&api-ms-win-core-proc>; KernelBa.OpenProcess

可以看到,我们在jmp之前做了点迷惑操作,具体为什么这么做,不需要了解,但是可以注意到,我们跳转到了KernelBa.OpenProcess里面,我们跟进去:

754B3C70 >/$  8BFF          mov edi,edi                              ;  OpenProc.<ModuleEntryPoint>
754B3C72  |.  55            push ebp
754B3C73  |.  8BEC          mov ebp,esp
754B3C75  |.  83EC 24       sub esp,0x24
754B3C78  |.  8B45 10       mov eax,[arg.3]
754B3C7B  |.  33C9          xor ecx,ecx                              ;  OpenProc.<ModuleEntryPoint>
754B3C7D  |.  8945 F4       mov [local.3],eax
754B3C80  |.  8B45 0C       mov eax,[arg.2]
754B3C83  |.  F7D8          neg eax
754B3C85  |.  894D F8       mov [local.2],ecx                        ;  OpenProc.<ModuleEntryPoint>
754B3C88  |.  C745 DC 18000>mov [local.9],0x18
754B3C8F  |.  1BC0          sbb eax,eax
754B3C91  |.  894D E0       mov [local.8],ecx                        ;  OpenProc.<ModuleEntryPoint>
754B3C94  |.  83E0 02       and eax,0x2
754B3C97  |.  894D E4       mov [local.7],ecx                        ;  OpenProc.<ModuleEntryPoint>
754B3C9A  |.  8945 E8       mov [local.6],eax
754B3C9D  |.  8D45 F4       lea eax,[local.3]
754B3CA0  |.  50            push eax
754B3CA1  |.  8D45 DC       lea eax,[local.9]
754B3CA4  |.  894D EC       mov [local.5],ecx                        ;  OpenProc.<ModuleEntryPoint>
754B3CA7  |.  50            push eax
754B3CA8  |.  FF75 08       push [arg.1]
754B3CAB  |.  8D45 FC       lea eax,[local.1]
754B3CAE  |.  894D F0       mov [local.4],ecx                        ;  OpenProc.<ModuleEntryPoint>
754B3CB1  |.  50            push eax
754B3CB2  |.  FF15 70385775 call dword ptr ds:[<&ntdll.NtOpenProcess>;  ntdll.ZwOpenProcess

前面的还是不需要关注,还是最后一句话,call了ntdll.NtOpenProcess,那么我们继续跟进这个ntdll.NtOpenProcess这个函数:

7707AB30 >  B8 26000000     mov eax,0x26
7707AB35    BA 60F10877     mov edx,ntdll.7708F160
7707AB3A    FFD2            call edx                                 ; OpenProc.<ModuleEntryPoint>

可以看到,我们又call了一下edx,然后就返回了,这个edx是多少呢,是0x7708F160,我们继续看这个地址:

7708F160  - FF25 28B21277   jmp dword ptr ds:[Wow64Transition]       ; wow64cpu.76FA7000

这是最深的一层了,没必要跟进了,其实这个函数通过汇编的分析,可以简单的得到一个结论,就是他需要一个eax的值,在我们调用OpenProcess的时候,这个eax的值是0x26,我截个图大家看看上一层:


发现在上一层有好多类似的代码,都是赋值eax,然后跳到这个Wow64Transition函数中去,这里我讲解的是SSDT,也就是kernel的(不是GDI和user32的)
SSDT就是系统服务描述符表,主要作用就是将我们的用户层API与内核层的Nt函数进行一一的对应,里面会存放一个指向这个NT开头函数的地址:
说白了就是OpenProcess其实调用了ZwOpenProcess,然后通过eax的值在SSDT表中指向了NtOpenprocess函数,最终调用内核层的代码,Wow64Transition这个函数应该是从用户层跳转的一个函数,这里不做过多的分析。

简单的分析到这里,我们可以画一张图来理顺一下思路(本人画图技术不精,PS一直都是很混)
paint
在之前我们知道用户层的hook分为两种方式,一种是修改PE导入表,另一种是inlinehook,那么这里也主要分为这两种:

  1. 修改SSDT表的OpenProcess函数的地址,指向我们hook的地址
  2. 修改函数的跳转

为了简单起见,我主要讲解的是第一种修改SSDT表的方式。

这里我们开发一个hook的驱动程序,我准备的环境是win7,vs2013,wdk,ddk标准的开发环境,虽然有一点点的老了,但是还是可以用的,如果不熟悉搭建环境流程的和我以前学驱动的时候一样的话呢,那么可以联系我,我将这个环境配置到了VM虚拟机中,你只需要下载虚拟机导入就可以直接使用我配置好的环境。

首先我们导入ntddk.h

#include <ntddk.h>

对了还有一点要说的是,我用的是C语言写的,我建议使用C语言,不要用CPP了,CPP写起来我总是遇到问题!!!虽然高级属性少了,但是会避免一些不必要的问题。
定义NTOpenProcess的原型:

typedef NTSTATUS(*pfnNtOpenProcess)(
	PHANDLE,
	ACCESS_MASK,
	POBJECT_ATTRIBUTES,
	PCLIENT_ID
	);

定义一个全局变量,用来保存我们的被hook前真实的NtOpenprocess地址,这里为什么我没用到设备扩展,因为我们不是一个真实的wdm驱动,而是一个测试的nt驱动,为了简单,我就不创建驱动对象了。

pfnNtOpenProcess OldNtOpenProcess;

下面是重点:

typedef struct _SERVICE_DESCRIPTOR_TABLE {
	PVOID 	ServiceTableBase;//System Service Dispatch Table 的基地址
	PVOID 	ServiceCounterTableBase;//包含着 SSDT 中每个服务被调用次数的计数器。这个计数器一般由sysenter 更新
	ULONG	NumberOfServices;//由 ServiceTableBase 描述的服务的数目
	PUCHAR	ParamTableBase;//包含每个系统服务参数字节数表的基地址-系统服务参数表
}SERVICE_DESCRIPTOR_TABLE, *PSERVICE_DESCRIPTOR_TABLE;

__declspec(dllimport) SERVICE_DESCRIPTOR_TABLE KeServiceDescriptorTable;

主要讲解这一块,我们定义SSDT的结构,这个微软给了,我直接copy别人的,下面的是ntoskrnl.lib导出的一个全局变量,这个变量里面就是我们要修改的宏大的SSDT表!

定义我们的NtOpenprocess函数:

NTSTATUS MyNtOpenProcess(PHANDLE ProcessHandle, ACCESS_MASK DesireAccess, POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID ClientID)
{
	return STATUS_ACCESS_DENIED;
}

可以看到,我什么都不做,只是返回一个连接拒绝,当然我只是为了简单,其实真正的做法应该是我们遍历我们保护的进程,得到PID,然后如果PID等于要保护的话呢我们就返回拒绝,不是我们就让他执行原先的Nt函数。

下面就是hook代码,我参考别人的,我下面简单解释一下他的做法是什么:

PVOID HookSSDTFunction(PVOID OldFunction, PVOID HookFuction)
{
	KdPrint(("Enter HookSSDTFunction \n"));
	PMDL pMdl = MmCreateMdl(NULL, KeServiceDescriptorTable.ServiceTableBase,KeServiceDescriptorTable.NumberOfServices * 4);
	if (!pMdl)
	{
		KdPrint(("Hook SSDT Failed!\n"));
		return 0;
	}
	MmBuildMdlForNonPagedPool(pMdl);
	pMdl->MdlFlags |= MDL_MAPPED_TO_SYSTEM_VA;
	PLONG pMdlLocked = (PLONG)MmMapLockedPages(pMdl, KernelMode);
	for (ULONG i = 0; i < KeServiceDescriptorTable.NumberOfServices;i++)
	{
		if ((LONG)pMdlLocked[i] == (LONG)OldFunction)
		{
			InterlockedExchange(&pMdlLocked[i], (LONG)HookFuction);
			break;
		}
	}
	MmUnmapLockedPages(pMdlLocked, pMdl);
	IoFreeMdl(pMdl);
	return OldFunction;
}

首先我们创建一块mdl空间,SSDT表的基地址到SSDT表的结尾,大小的话呢就是SSDT个数的4倍,主要是一个指针四个字节,我们用的32位的操作系统。
然后更新我们的MDL空间在非分页内存上,然后再锁住,防止蓝屏这个小可爱。
然后遍历SSDT表判断是不是我们要hook的地址,如果是的话呢我们就用这种原子级别的数据交换,修改要hook的函数,最终解锁MDL,并且释放,返回我们之前的函数地址,为了在驱动卸载的时候取消hook。

VOID hook(PCWSTR sz)
{
	UNICODE_STRING strToFind;
	RtlInitUnicodeString(&strToFind, sz);
	PVOID AddToFind = MmGetSystemRoutineAddress(&strToFind);
	OldNtOpenProcess = (pfnNtOpenProcess)HookSSDTFunction(NtOpenProcess, MyNtOpenProcess);
}

这段代码为了验证,但是实际上关键的就是OldNtOpenProcess = (pfnNtOpenProcess)HookSSDTFunction(NtOpenProcess, MyNtOpenProcess);,MmGetSystemRoutineAddress这个函数是通过字符串获得地址,这个也就是XT这种工具可以帮助我们取消hookSSDT表的方法函数。
可以看到我们将之前的NtOpenprocess hook 成为了我们的函数。

驱动加载函数:

#pragma INITCODE
//extern "C" 
NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject,
	IN PUNICODE_STRING pRegistryPath)
{
	NTSTATUS status = STATUS_SUCCESS;
	pDriverObject->DriverUnload = DriverUnLoad;
	hook(L"NtOpenProcess");
	return status;
}

很简单,驱动一加载我们就hook,也不创建别的东西了。

驱动卸载函数:

#pragma PAGECODE
VOID DriverUnLoad(IN PDRIVER_OBJECT pDriverObject)
{
	HookSSDTFunction(MyNtOpenProcess, OldNtOpenProcess);
}

也是,卸载的话呢设备对象也不管了,直接取消hook就可以了。

代码其实比较的简单,关键的也就没几行。
看一下在32位上实际的操作是什么样子的:


可以看到XT已经检测出来hook了,并且我们现在如果要用管理员方式运行任何东西,都会提示这个样子的对话框:
hook
这个对话框是不是很熟悉,杀毒软件经常这么搞。

卸载之后XT也就给我们提示了,没有hook了。

过掉驱动

我们知道原理之后,我们只需要写一个简单的驱动,通过MmGetSystemRoutineAddress函数修改回我们之前的OpenProcess地址就可以了,在这里Wker给大家一些问题以及思路:

  1. 我们修改回来的话呢,驱动其实可以设置一个DPC的时钟,一直监视我们的NtOpenprocess的地址,如果发现被改回来的话呢还是可以进行设置回来的。
    1. 解决办法:用汇编修改新的NtOpenprocess代码,让他直接跳转到的旧的NtOpenProcess
  2. 如果我们用了汇编修改方法,他可以检测这个内存单元的字节,如果存在跳转,并且不是他之前的字节的话呢,他还是可以进行保护

留一个问题,希望读者可以通过思考得出如何过掉他的第二种保护手法。

那种内联的hook方法我就不讲了,这个的话呢还要牵扯到一些比较底层的东西,牵扯到CR0的一些问题,给大家一段代码,从网上摘录的,其实也比较简单,就是对CR0的位进行了修改:

//禁用写保护,wp=0
VOID PROTECT()
{
	__asm
	{
		cli ;
		mov eax, cr0
		and  eax, ~0x10000
		mov cr0, eax
	}
}
//启用写保护,wp=1
VOID UN_PROTECT()
{
	__asm
	{
		mov  eax, cr0
		or     eax, 0x10000
		mov  cr0, eax
		sti ;
	}
}

之后有机会的话呢会给大家介绍一下R3的用户层进入R0层执行函数的方法,并且介绍一些驱动过滤器的技术。

结语

感谢能看到这里,肯定很无聊吧,我也准备写一个驱动绕过工具加入到我的Wker_EXEDebug里面方便我用来过保护,并且如果我真的很闲,我有可能修改框架让我的Wker_EXEDebug直接调用R0层的Nt函数,绕过大部分的驱动保护,但是这个前提是我需要考完研了可能。

终于写完了,逆向方面的知识今后会少发了,我看到论坛里主要是对PHP的代码审计感兴趣,我可能之后会主要以审计PHP和jsp代码的文章为主了。(前段时间主要是自己的感情琐事以及JSP的项目影响了一些经历,现在基本解决完了)

感谢admin05对我的信任,让我当上了版主,我也会不断提升自己的技术,发表文章带动论坛的活跃度的。 :wink:

5 个赞

主要是看你兴趣还有以后的方向,审计这个路不好走,逆向挺好的

1 个赞

其实还好,之前也一直在做PHP的审计,只是不怎么发文 :face_with_monocle:

这个其实没必要,个人兴趣为主,论坛代码审计这块多,是因为很多都是搞渗透的,对于漏洞这块自然感兴趣,逆向也不是不感兴趣,而是很多人不知道怎么把逆向的知识点应用到实际情况中

1 个赞

看来还是要多学学多分享自己学习过程的东西就好 :thinking:

1 个赞

那之后我也会多分享分享这些资料的,资料还是不少的

2 个赞