BitVisorにVMCALLを追加してドライバを検知してみる

この記事はBitVisor Advent Calendar 8日目の記事です．

BitVisorにVMCALLを追加する事で、Linux上でのデバイスドライバのロードを検知してみます。

# システムコールフック
デバイスドライバはinsmodなどでカーネルに組み込まれます。この時init_moduleまたはfinit_moduleシステムコールが呼ばれドライバがロードされるわけですが、これらのシステムコールをフックしてVMCALLでBitVisorに通知するようなエージェントの役割を持つカーネルモジュールを開発してみます。
システムコールのフックはsys_call_tableのページパーミッションを書き込み可能に変更して、フック関数にすげ替える手法を取ります。
- Linuxカーネルでsystem callのhook http://kernhack.hatenablog.com/entry/2014/12/05/000346

# ドライバのアドレス取得
init_moduleによって読み込まれたモジュールは内部でロード処理を行い、struct module->module_coreメンバにロードされたアドレスが格納されます。
よってinit_moduleのフック関数内で、オリジナルのinit_moduleを実行してロードさせた後、struct moduleを参照してやればロードアドレスが取得できます。実際に該当処理部分のコードを載せます。

```tools/drvhook/drvhook.c
static struct module*
get_module(const char *modname)
{
  struct module *mod = NULL;
  if (mutex_lock_interruptible(&module_mutex) != 0) {
    goto mutex_fail;
  }

  mod = find_module(modname);

  mutex_unlock(&module_mutex);
mutex_fail:
  pr_info("get_module(%s) = %p\n", modname, mod);
  return mod;
}

asmlinkage long
k2e_sys_finit_module (int fd, const char __user *uargs, int flags)
{
  long orig;
  struct module *mod = NULL;
  pr_info("call k2e_sys_finit_module\n");

  orig = orig_sys_finit_module (fd, uargs, flags);

  mod = get_module(target);
  if (mod)
    vmcall_drvhook(mod->module_core);

  return orig;
}
```
k2e_sys_finit_moduleというのがフック関数で、find_moduleでターゲットとするドライバを検索します。orig_sys_finit_moduleでオリジナルを実行した後なので、この段階では既にロードが完了しており、find_moduleで情報が取得できるわけです。

# BitVisorへの通知
では取得したドライバのロードアドレスをVMCALLでBitVisorに通知してみます。
通常BitVisorにVMCALLを追加するにはtools/common/call_vmm.cなどを使うのですが、

- [参考] BitVisorにVMMCALLを追加する
http://qiita.com/deep_tkkn/items/ca794e66c25df9a9bf66

call_vmm.cはそのままではカーネルモジュール用にはコンパイルできないので、必要な部分だけを切り出して使用しました。よってハイパーコール番号取得のためのCALL_VMM_GET_FUNCTIONマクロなどを使わず、一部再実装してカーネルモジュールからも使えるようにしました。

```tools/drvhook/drvhook.c
MODULE_DESCRIPTION("guest driver hook for k2e");
MODULE_AUTHOR("rkx1209");
MODULE_LICENSE("GPL");

/* following string SYSCALL_TABLE_ADDRESS will be replaced by set_syscall_table_address.sh */
static void **syscall_table = (void *) 0xffffffff81a001c0;
static char *target = "blahblah";
module_param(target, charp, S_IRUGO);

asmlinkage long (*orig_sys_init_module)(void __user *umod, unsigned long len, const char __user *uargs);
asmlinkage long (*orig_sys_finit_module)(int fd, const char __user *uargs, int flags);


void
call_vmm_call_function (call_vmm_function_t *function,
                        call_vmm_arg_t *arg, call_vmm_ret_t *ret)
{
  struct call_vmm_call_function_sub_data data;
  data.function = function;
  data.arg = arg;
  data.ret = ret;
  pr_info("vmcall %d(arg=0x%lx)\n", data.function->vmmcall_number, data.arg->rbx);
  switch (data.function->vmmcall_type) {
  	case VMMCALL_TYPE_VMCALL:
  		asm volatile ("vmcall"
  			           : "=a" (data.ret->rax), "=b" (data.ret->rbx),
				             "=c" (data.ret->rcx), "=d" (data.ret->rdx),
  				           "=S" (data.ret->rsi), "=D" (data.ret->rdi)
  			           : "a" (data.function->vmmcall_number),
  				           "b" (data.arg->rbx),
  				           "c" (data.arg->rcx), "d" (data.arg->rdx),
  				           "S" (data.arg->rsi), "D" (data.arg->rdi)
  			           : "memory");
  		break;
  	case VMMCALL_TYPE_VMMCALL:
  		asm volatile ("vmmcall"
  			           : "=a" (data.ret->rax), "=b" (data.ret->rbx),
  				           "=c" (data.ret->rcx), "=d" (data.ret->rdx),
  				           "=S" (data.ret->rsi), "=D" (data.ret->rdi)
  			           : "a" (data.function->vmmcall_number),
  				           "b" (data.arg->rbx),
              			 "c" (data.arg->rcx), "d" (data.arg->rdx),
              			 "S" (data.arg->rsi), "D" (data.arg->rdi)
  			           : "memory");
  		break;
  	}
}

/* Get a entry number of specified vmmcall */
void
vmmcall_get_function(const char *vmmcall, call_vmm_function_t *res)
{
  call_vmm_function_t gf;
  call_vmm_arg_t gf_a;
  call_vmm_ret_t gf_r;

  gf.vmmcall_number = GET_VMMCALL_NUMBER;
  gf.vmmcall_type = VMMCALL_TYPE_VMCALL;
  gf_a.rbx = (intptr_t)vmmcall;
  pr_info("vmmcall_string:%p, 0x%lx, %s\n", vmmcall, gf_a.rbx, vmmcall);
  call_vmm_call_function(&gf, &gf_a, &gf_r);

  /* RAX = vmmcall number */
  res->vmmcall_number = (int)gf_r.rax;
  res->vmmcall_type = VMMCALL_TYPE_VMCALL; // XXX: should get this value too.
}

```
vmmcall_get_functionでハイパーコールの番号を取得できます。後はdrvhookという新しいVMCALL(手元では4番になりました)を発行して、BitVisorにモジュールのアドレスを通知します。

```tools/drvhook/drvhook.c
static void
vmcall_drvhook (void *mod)
{
  call_vmm_function_t drvf;
  call_vmm_arg_t drv_a;
  call_vmm_ret_t drv_r;
  char drvhook[] = "drvhook";

  vmmcall_get_function(drvhook, &drvf);
  pr_info("drvhook number=%d\n",drvf.vmmcall_number);


  drv_a.rbx = (intptr_t)mod;
  call_vmm_call_function(&drvf, &drv_a, &drv_r);
}

```

# BitVisorにVMCALLを追加する
ゲストのエージェントモジュールは実装できたので、あとはBitVisor側にVMCALLを追加します。とりあえず引数で渡ってきたモジュールのアドレスを表示するだけです。

```core/vmmcall_drvhook.c
#include "config.h"
#include "constants.h"
#include "current.h"
#include "debug.h"
#include "initfunc.h"
#include "panic.h"
#include "printf.h"
#include "process.h"
#include "thread.h"
#include "spinlock.h"
#include "vmmcall.h"

static void
drvhook(void)
{
  ulong drvaddr;
  ulong ret = 0;
  current->vmctl.read_general_reg(GENERAL_REG_RBX, &drvaddr);
  printf("k2e: driver has loaded at 0x%lx", drvaddr);
  current->vmctl.write_general_reg(GENERAL_REG_RAX, (ulong)ret);
}

static void
vmmcall_drvhook_init(void)
{
  vmmcall_register("drvhook", drvhook);
}
INITFUNC ("vmmcal0", vmmcall_drvhook_init);

```

# 実行
では実際に実行してみましょう。
testmodというテスト用のモジュールを書いてフックしてみます。
`sudo insmod drvhook.ko target="testmod"`
`sudo insmod testmod.ko`

dmesgで確認するとちゃんとエージェントが動いている事が分かります。

```text

[   47.663567] system call replaced
[   57.012967] call k2e_sys_finit_module
[   57.014397] testmod: module license 'unspecified' taints kernel.
[   57.014399] Disabling lock debugging due to kernel taint
[   57.014696] try 'sudo mknod testdev c 200 0'
[   57.014729] get_module(testmod) = ffffffffc095b100
[   57.014732] vmmcall_string:ffff880117e7fef8, 0xffff880117e7fef8, drvhook
[   57.014733] vmcall 0(arg=0xffff880117e7fef8)
[   57.014751] drvhook number=4
[   57.014753] vmcall 4(arg=0xffffffffc0959000)
```
dbgshでBitVisor側のログを見てみると

```text
k2e: driver has loaded at 0xffffffffc0959000`
```

ちゃんとドライバの正しいロード場所が検知できました。

簡単な処理ですが、一日ちょっとで書けてしまった。BitVisorすごく扱いやすい。