Skip to main content

Command Palette

Search for a command to run...

记一次实现minivmi

Published
6 min read

我已经验证了运行结果:

  • sudo ./list_domains 可以列出 dom0 + guest,并拿到 guest 的 uuid

  • sudo ./cr3trace_uuid --uuid <uuid> 可以持续打印 CR3 写入事件(含 old/new CR3、RIP)

  • 项目位置:minivmi

luckybird@luckybird:~/vmi_test/minivmi/_build/bin$ sudo ./list_domains
count=2
domid=0 hvm=no dying=no shutdown=no name='Domain-0' uuid=''
domid=5 hvm=yes dying=no shutdown=no name='ubuntu2204' uuid='d5448ca1-abfc-45cd-9566-718742f51fb3'
luckybird@luckybird:~/vmi_test/minivmi/_build/bin$ sudo ./cr3trace_uuid --uuid d5448ca1-abfc-45cd-9566-718742f51fb3
attach uuid=d5448ca1-abfc-45cd-9566-718742f51fb3 domid=5
monitor started (Ctrl+C to stop)
domid=5 uuid=d5448ca1-abfc-45cd-9566-718742f51fb3 vcpu=1 old=0x1812002 new=0x3250c006 rip=0xffffffff946a7c4c
domid=5 uuid=d5448ca1-abfc-45cd-9566-718742f51fb3 vcpu=1 old=0x3250c006 new=0x3250d806 rip=0xffffffff95400213
domid=5 uuid=d5448ca1-abfc-45cd-9566-718742f51fb3 vcpu=1 old=0x3250d806 new=0x3250c006 rip=0xffffffff9540009d
domid=5 uuid=d5448ca1-abfc-45cd-9566-718742f51fb3 vcpu=1 old=0x3250c006 new=0x3250d806 rip=0xffffffff95400213

1. minivmi架构

minivmi 的策略是:直接调用 Xen 的公开 C API,把“必要的系统边界”明明白白露出来:

  • libxc<xenctrl.h>):枚举 domain、开启 vm_event、配置 ctrlreg 监控

  • xenstore<xenstore.h>):读 /local/domain/<domid>/... 补齐 name/uuid

  • xenevtchn<xenevtchn.h>):event channel 通知,让用户态知道 ring 有事件了 这三块拼起来,就是最小 VMI “底座”。

2. Xen 上“监控 CR3”到底在监控什么:vm_event + ring + evtchn

看到 CR3 事件并打印,背后的链路是:

  1. guest 写 CR3(硬件虚拟化拦截点)触发 VMExit

  2. Xen 生成一个 vm_event_request 写入共享内存(vm_event ring

  3. Xen 通过 event channel(evtchn)通知 dom0 用户态:ring 有数据

  4. dom0 用户态(minivmi)从 ring 读 request,处理后写回 response

  5. minivmi 调 notify,Xen 收到 response 后放行 guest 继续执行

关键点:sync 拦截是“必须写回 response 才能放行 guest”。 所以 VMI 的本质不是“读到了什么”,而是 “拦截—处理—响应—放行”闭环

3. 列出 domain(发现目标 guest)

目标:确认环境可用、能识别目标 guest。

实现思路:

  • libxc 枚举 domain:拿到 domid + flags

  • xenstore 补齐 name/uuid

    • /local/domain/<domid>/name

    • /local/domain/<domid>/vm(内容通常是 "/vm/<uuid>"

3.1 关键代码(完整函数)

int minivmi_domains_snapshot(struct minivmi_domain **out_domains,
                             size_t *out_count,
                             char *err, size_t err_len)
{
    if (!out_domains || !out_count) {
        set_err(err, err_len, "bad args");
        return -1;
    }
    *out_domains = NULL;
    *out_count = 0;

    xc_interface *xch = xc_interface_open(NULL, NULL, 0);
    if (!xch) {
        set_err(err, err_len, "xc_interface_open failed: %s", strerror(errno));
        return -1;
    }

    struct xs_handle *xs = xs_open(XS_OPEN_READONLY);
    if (!xs) {
        set_err(err, err_len, "xs_open failed: %s", strerror(errno));
        xc_interface_close(xch);
        return -1;
    }

    /* 第1步(域枚举):通过一次 libxc hypercall 获取域列表。 */
    const unsigned int cap = 1024;
    xc_domaininfo_t *infos = calloc(cap, sizeof(*infos));
    if (!infos) {
        set_err(err, err_len, "oom");
        xs_close(xs);
        xc_interface_close(xch);
        return -1;
    }

    const int n = xc_domain_getinfolist(xch, 0, cap, infos);
    if (n < 0) {
        set_err(err, err_len, "xc_domain_getinfolist failed: %s", strerror(errno));
        free(infos);
        xs_close(xs);
        xc_interface_close(xch);
        return -1;
    }

    struct minivmi_domain *domains = calloc((size_t)n, sizeof(*domains));
    if (!domains) {
        set_err(err, err_len, "oom");
        free(infos);
        xs_close(xs);
        xc_interface_close(xch);
        return -1;
    }

    for (int i = 0; i < n; i++) {
        const uint32_t domid = (uint32_t)infos[i].domain;
        domains[i].domid = domid;
        domains[i].xen_flags = infos[i].flags;

        /*
         * 第1步(补齐 name/uuid):xenstore 的常见路径约定:
         * - /local/domain/<domid>/name  -> 可读名字
         * - /local/domain/<domid>/vm    -> "/vm/<uuid>"
         */
        char path[256];

        snprintf(path, sizeof(path), "/local/domain/%u/name", domid);
        char *name = xs_read_strdup(xs, path);
        if (name) {
            safe_copy(domains[i].name, sizeof(domains[i].name), name, strlen(name));
            free(name);
        }

        snprintf(path, sizeof(path), "/local/domain/%u/vm", domid);
        char *vm = xs_read_strdup(xs, path);
        if (vm) {
            const char *u = vm;
            if (strncmp(vm, "/vm/", 4) == 0) u = vm + 4; /* 去掉 "/vm/" 前缀 */
            safe_copy(domains[i].uuid, sizeof(domains[i].uuid), u, strlen(u));
            free(vm);
        }
    }

    free(infos);
    xs_close(xs);
    xc_interface_close(xch);

    *out_domains = domains;
    *out_count = (size_t)n;
    return 0;
}

3.2 运行验证

  #在 _build/bin/ 下执行:
  sudo ./list_domains
  #示例输出:
  count=2
  domid=0 hvm=no  ... name='Domain-0' uuid=''
  domid=5 hvm=yes ... name='ubuntu2204' uuid='d5448ca1-abfc-45cd-9566-718742f51fb3'

结论:目标 guest 的 uuid 已可用,后续建议按 uuid 定位(比 domid 更稳定)。

4. attach(建立 vm_event 通道)

attach 阶段的目标是:让 Xen 给我们一个 vm_event ring,并通过 evtchn 通知我们。

流程:

  1. ensure_hvm_domain():确认目标域是 HVM 且存活

  2. xc_monitor_enable():开启 vm_event,得到:

    • 一页共享 ring(mmap 到当前进程)

    • remote_port(给 evtchn bind 用)

  3. xenevtchn_bind_interdomain():把 Xen 给的 port 绑定成 dom0 可监听的本地 port,并获取 fd

  4. ring_init_back():初始化共享 ring 与 back_ring 视图

4.1 关键代码(完整函数)

  struct minivmi_cr3_monitor *minivmi_cr3_monitor_open(uint32_t domid,
                                                       const char *uuid_hint,
                                                       char *err, size_t err_len)
  {
      /*
       * 第2步(attach):建立一条“监控会话”。
       * - 验证目标域是 HVM 且存活
       * - 开启 vm_event(xc_monitor_enable):拿到共享 ring 页 + 一个 remote evtchn port
       * - 建立 event channel(bind interdomain):拿到本地 port + fd,用于 poll 等待事件
       */
      struct minivmi_cr3_monitor *m = calloc(1, sizeof(*m));
      if (!m) {
          set_err(err, err_len, "oom");
          return NULL;
      }

      m->domid = domid;
      if (uuid_hint && uuid_hint[0]) {
          safe_copy(m->uuid, sizeof(m->uuid), uuid_hint, strlen(uuid_hint));
      }

      m->ring_page_len = (unsigned long)getpagesize();

      m->xch = xc_interface_open(NULL, NULL, 0);
      if (!m->xch) {
          set_err(err, err_len, "xc_interface_open failed: %s", strerror(errno));
          minivmi_cr3_monitor_close(m);
          return NULL;
      }

      if (ensure_hvm_domain(m->xch, domid, err, err_len) != 0) {
          minivmi_cr3_monitor_close(m);
          return NULL;
      }

      /*
       * 第2步(关键 hypercall):开启 vm_event。
       * - 返回值:一页 mmap 到本进程的共享内存(ring)
       * - out 参数:remote_port(给 xenevtchn_bind_interdomain 用)
       */
      m->ring_page = xc_monitor_enable(m->xch, domid, &m->remote_port);
      if (!m->ring_page) {
          set_err(err, err_len, "xc_monitor_enable failed for domid=%u: %s", domid, strerror(errno));
          minivmi_cr3_monitor_close(m);
          return NULL;
      }
      m->monitor_enabled = true;

      /*
       * 第2步(事件通道):绑定 interdomain evtchn。
       * - vm_event ring 里有事件时,Xen 会通过 evtchn 唤醒 dom0 用户态
       * - 我们用 poll(fd) 等待它变为可读
       */
      m->xce = xenevtchn_open(NULL, 0);
      if (!m->xce) {
          set_err(err, err_len, "xenevtchn_open failed: %s", strerror(errno));
          minivmi_cr3_monitor_close(m);
          return NULL;
      }

      xenevtchn_port_or_error_t p = xenevtchn_bind_interdomain(m->xce, domid, m->remote_port);
      if (p < 0) {
          set_err(err, err_len, "xenevtchn_bind_interdomain failed: %s", strerror(errno));
          minivmi_cr3_monitor_close(m);
          return NULL;
      }
      m->local_port = (evtchn_port_t)p;

      m->evtchn_fd = xenevtchn_fd(m->xce);
      if (m->evtchn_fd < 0) {
          set_err(err, err_len, "xenevtchn_fd failed: %s", strerror(errno));
          minivmi_cr3_monitor_close(m);
          return NULL;
      }

      ring_init_back(m);
      return m;
  }

注意:同一个 domain 通常只能被一个 monitor 连接,否则 xc_monitor_enable() 可能返回 EBUSY。

5. 开启 CR3 拦截点(monitor_write_ctrlreg)

vm_event 是通道,下一步才是“拦截什么”。

  int minivmi_cr3_monitor_enable(struct minivmi_cr3_monitor *m,
                                 char *err, size_t err_len)
  {
      if (!m) {
          set_err(err, err_len, "bad args");
          return -1;
      }

      /*
       * 第3步(配置拦截点):让 Xen 在“写 CR3”时产生 vm_event 事件。
       * - sync=true:同步拦截(guest 在事件点暂停,直到我们写回 response)
       * - onchangeonly=true:只在 CR3 真变化时触发,减少噪声
       */
      const int rc = xc_monitor_write_ctrlreg(m->xch, m->domid,
                                             VM_EVENT_X86_CR3,
                                             true,  /* enable */
                                             true,  /* sync */
                                             0,     /* bitmask */
                                             true); /* onchangeonly */
      if (rc != 0) {
          set_err(err, err_len, "xc_monitor_write_ctrlreg(CR3) failed: %s", strerror(errno));
          return -1;
      }

      m->cr3_enabled = true;
      return 0;
  }

6. 事件循环(读 ring → 回调 → 写回 response → 放行 guest)

这是整个 VMI 闭环的核心,也是“最小事件管理器”。

流程要点:

  • poll(evtchn_fd):等待 Xen 通知

  • xenevtchn_pending():消费通知(port 会被 mask)

  • drain ring:把 ring 里所有 request 读完

  • 写回 response 并 notify Xen 放行 guest

  • unmask 继续收下一次通知

6.1 关键代码(完整核心循环)

  int minivmi_cr3_monitor_loop(struct minivmi_cr3_monitor *m,
                               minivmi_cr3_cb cb,
                               void *user,
                               volatile sig_atomic_t *stop_flag,
                               char *err, size_t err_len)
  {
      if (!m || !cb || !stop_flag) {
          set_err(err, err_len, "bad args");
          return -1;
      }

      struct pollfd pfd;
      pfd.fd = m->evtchn_fd;
      pfd.events = POLLIN | POLLERR;

      while (!(*stop_flag)) {
          pfd.revents = 0;
          const int prc = poll(&pfd, 1, 200);
          if (prc < 0) {
              if (errno == EINTR) continue;
              set_err(err, err_len, "poll(evtchn) failed: %s", strerror(errno));
              return -1;
          }
          if (prc == 0) continue;

          /*
           * 第3步(等事件 + 消费通知):
           * - poll(fd) 告诉我们“有 evtchn 通知到了”
           * - xenevtchn_pending() 取出哪个 port 触发,并进入 masked 状态
           * - 我们处理完 ring 后,必须 xenevtchn_unmask() 才能继续收下一次通知
           */
          const xenevtchn_port_or_error_t pend = xenevtchn_pending(m->xce);
          if (pend < 0) {
              set_err(err, err_len, "xenevtchn_pending failed: %s", strerror(errno));
              return -1;
          }

          int handled = 0;
          vm_event_request_t req;

          while (ring_pop_req(&m->back_ring, &req)) {
              /*
               * 第3步(写回 response):默认做法是“原样回显”。
               * - 对 minivmi 这个最小 demo 来说:不改寄存器/不注入动作
               * - 只要写回 response 并 notify,Xen 就会放行 guest 继续执行
               */
              vm_event_response_t rsp = req;

              if (req.reason == VM_EVENT_REASON_WRITE_CTRLREG &&
                  req.u.write_ctrlreg.index == VM_EVENT_X86_CR3) {

                  struct minivmi_cr3_event ev;
                  memset(&ev, 0, sizeof(ev));
                  ev.domid = m->domid;
                  safe_copy(ev.uuid, sizeof(ev.uuid), m->uuid, strlen(m->uuid));
                  ev.vcpu = (uint16_t)req.vcpu_id;
                  ev.old_cr3 = req.u.write_ctrlreg.old_value;
                  ev.new_cr3 = req.u.write_ctrlreg.new_value;
                  ev.rip = req.data.regs.x86.rip;

                  /* 第3步(对外暴露):把 CR3 事件交给用户回调。 */
                  cb(&ev, user);
              }

              ring_put_rsp(&m->back_ring, &rsp);
              handled++;
          }

          if (handled) {
              /*
               * 第3步(闭环完成):push responses + notify Xen。
               */
              RING_PUSH_RESPONSES(&m->back_ring);
              if (xenevtchn_notify(m->xce, m->local_port) < 0) {
                  set_err(err, err_len, "xenevtchn_notify failed: %s", strerror(errno));
                  return -1;
              }
          }

          if (xenevtchn_unmask(m->xce, (evtchn_port_t)pend) < 0) {
              set_err(err, err_len, "xenevtchn_unmask failed: %s", strerror(errno));
              return -1;
          }
      }

      return 0;
  }

More from this blog

记一次基于 Linux 5.15.0-139 内核源码中ebpf辅助函数解惑学习

在 eBPF 开发中,辅助函数(Helper Functions)是连接沙箱代码与内核原生的唯一桥梁, 辅助函数既不是动态加载的插件,也不是脆弱的符号引用。它们是在内核启动那一刻,由引导代码根据链接脚本的‘施工图’,强行焊接在内存只读区域的物理基石。 本文起源于我在开发ebpf程序时思考辅助函数到底是什么?它和内核提供的其他函数有什么区别? 。 本文记录了我是如何通过源码分析、逆向思考和底层调试,一步步打通 eBPF 辅助函数逻辑的全过程。所有的代码均出自于5.15.0-139内核源码 第一...

Feb 6, 20265 min read9

记一次Introcore 内核级 Bug 调试记录:从 PUSH 指令到页表模拟崩溃

在虚拟机内省(VMI)的底层开发中,稳定性往往比功能更难攻克。最近在对 Windows 7 进行监控实验时,Bitdefender 的 HVMI (Introcore) 引擎频繁触发致命崩溃。错误指向了页表写入模拟逻辑。 通过对 hvmid 进程进行深度的 GDB 挂载调试,我还原了一个关于指令建模、地址不匹配以及异常处理机制的完整 Bug 链路。 1. 现象:内省引擎的“自杀” 在监控环境下,Introcore 抛出两类致命日志并导致守护进程崩溃: [ERROR] Access at 79c...

Jan 26, 20263 min read32

记一次从零设计 Vmi 代码注入引擎:劫持、执行、清理

0.摘要 在虚拟化安全的研究中,如何在尽量不修改 Guest 内核的前提下执行自定义代码,一直是一个有趣且充满挑战的话题。本文尝试分享一套基于 Hypervisor 的完整代码注入思路,涵盖了从劫持 Guest 执行流、基于 EPT 的透明注入、双向通信协议到生命周期管理的全过程。 仓库位置:[mini-int3-injector].(https://github.com/ania0-art/mini-int3-injector). 1. 引言:一点背景与思考 1.1 传统方案遇到的一些挑战 ...

Jan 25, 20264 min read36

记一次虚拟化下的detour框架实现

1.Detour机制基础 1.1 什么是Detour? 在虚拟化安全监控领域,我们经常需要监控Guest OS内部的关键操作——进程创建、权限提升、内存映射变化等。传统的EPT(Extended Page Table)监控虽然可以捕获内存访问,但存在明显的局限性: 语义鸿沟:从"某个地址被写入"推断"进程被创建"需要复杂的分析 性能开销:细粒度的内存监控会产生大量VM-Exit 覆盖盲区:纯寄存器操作、链表修改等无法通过内存监控捕获 因此,我们需要一种更直接的方法:在关键函数的入口和出...

Jan 17, 20266 min read21

once2twice

8 posts

有的没的杂七杂八