-
Notifications
You must be signed in to change notification settings - Fork 65
Open
Description
I've tried so many variations of extracting command line arguments from running processes in a VM with kvm-vmi but I am currently at a loss.
// vmi_probe.c
// Host-side VM introspection, Milestone 1 (fixed API usage).
// - Prints QEMU mappings for the VM (hugepages/memfd/shm hints).
// - Walks Windows EPROCESS list to find a target process by ImageFileName.
// - Reads that process's PEB->ProcessParameters->CommandLine.
// - Resolves DTB/CR3 with vmi_pid_to_dtb and uses access_context_t for reads.
// Falls back to PID-based _va reads if DTB resolution fails.
//
// Build:
// gcc -O2 -march=native -Wall -Wextra vmi_probe.c -o vmi_probe $(pkg-config --cflags --libs libvmi)
// If pkg-config isn't available for libvmi, try:
// gcc -O2 -march=native -Wall -Wextra -I/usr/local/include -L/usr/local/lib -o vmi_probe vmi_probe.c -lvmi
//
// Run:
// sudo ./vmi_probe <vm_name> <process_name>
// e.g., sudo ./vmi_probe win10 notepad.exe
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdbool.h>
#include <string.h>
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <libvmi/libvmi.h>
// ------------------------------
// Utils
// ------------------------------
static void to_lower(char *s) { for (; *s; ++s) *s = (char)tolower((unsigned char)*s); }
static bool strcasestr_bool(const char *hay, const char *needle) {
if (!hay || !needle) return false;
size_t nlen = strlen(needle);
if (!nlen) return false;
char *hcopy = strdup(hay);
char *ncopy = strdup(needle);
if (!hcopy || !ncopy) { free(hcopy); free(ncopy); return false; }
to_lower(hcopy); to_lower(ncopy);
bool found = strstr(hcopy, ncopy) != NULL;
free(hcopy); free(ncopy);
return found;
}
static bool read_cmdline(pid_t pid, char *buf, size_t buflen) {
char path[128];
snprintf(path, sizeof(path), "/proc/%d/cmdline", pid);
int fd = open(path, O_RDONLY);
if (fd < 0) return false;
ssize_t n = read(fd, buf, (ssize_t)(buflen - 1));
close(fd);
if (n <= 0) return false;
for (ssize_t i = 0; i < n; i++) if (buf[i] == '\0') buf[i] = ' ';
buf[n] = '\0';
return true;
}
static pid_t find_qemu_pid_for_guest(const char *guest_name) {
DIR *d = opendir("/proc");
if (!d) return -1;
struct dirent *de;
pid_t found = -1;
char cmdbuf[8192];
while ((de = readdir(d)) != NULL) {
if (de->d_type != DT_DIR) continue;
pid_t pid = (pid_t)atoi(de->d_name);
if (pid <= 0) continue;
char exe_path[256];
snprintf(exe_path, sizeof(exe_path), "/proc/%d/exe", pid);
char target[512];
ssize_t len = readlink(exe_path, target, sizeof(target)-1);
if (len < 0) continue;
target[len] = '\0';
if (!strstr(target, "qemu-system-")) continue;
if (!read_cmdline(pid, cmdbuf, sizeof(cmdbuf))) continue;
if (strcasestr_bool(cmdbuf, guest_name)) { found = pid; break; }
}
closedir(d);
return found;
}
static void print_qemu_mem_maps(pid_t qemu_pid) {
char path[128];
snprintf(path, sizeof(path), "/proc/%d/maps", qemu_pid);
FILE *f = fopen(path, "r");
if (!f) { perror("fopen maps"); return; }
printf("\n[+] QEMU PID %d memory maps:\n", qemu_pid);
char *line = NULL; size_t cap = 0;
while (getline(&line, &cap, f) != -1) {
bool is_huge = (strstr(line, "huge") || strstr(line, "Huge") || strstr(line, "hugetlb"));
bool is_shm = (strstr(line, "/dev/shm"));
bool is_memfd= (strstr(line, "memfd:"));
bool is_anon = (strstr(line, "anon_inode") || strstr(line, "[anon]"));
char tag[128] = "";
if (is_huge) strcat(tag, " [huge]");
if (is_shm) strcat(tag, " [shm]");
if (is_memfd)strcat(tag, " [memfd]");
if (is_anon) strcat(tag, " [anon]");
if (tag[0]) {
size_t L = strlen(line);
if (L && line[L-1] == '\n') line[L-1] = '\0';
printf("%s%s\n", line, tag);
} else {
printf("%s", line);
}
}
free(line); fclose(f);
snprintf(path, sizeof(path), "/proc/%d/smaps", qemu_pid);
f = fopen(path, "r");
if (!f) { perror("fopen smaps"); return; }
printf("\n[+] Selected /proc/%d/smaps flags (hugepage hints):\n", qemu_pid);
char *hdr = NULL; size_t hcap = 0; line = NULL; cap = 0;
while (getline(&line, &cap, f) != -1) {
if (strchr(line, '-') && strstr(line, " r")) {
free(hdr); hdr = strdup(line);
}
if (strstr(line, "Huge") || strstr(line, "VmFlags:")) {
if (hdr) printf("%s", hdr);
printf("%s", line);
}
}
free(hdr); free(line); fclose(f);
}
// ------------------------------
// Windows offsets bundle
// ------------------------------
typedef struct WinOffsets {
addr_t eprocess_list;
addr_t eprocess_pid;
addr_t eprocess_name;
addr_t eprocess_peb;
addr_t peb_process_parameters; // _PEB.ProcessParameters
addr_t peb_ldr; // _PEB.Ldr
addr_t rupp_commandline; // _RTL_USER_PROCESS_PARAMETERS.CommandLine (UNICODE_STRING)
addr_t ldr_inloadorder; // _PEB_LDR_DATA.InLoadOrderModuleList
addr_t ldr_entry_dllbase; // _LDR_DATA_TABLE_ENTRY.DllBase
addr_t ldr_entry_basedllname; // _LDR_DATA_TABLE_ENTRY.BaseDllName (UNICODE_STRING)
addr_t ldr_entry_inloadlinks; // _LDR_DATA_TABLE_ENTRY.InLoadOrderLinks
addr_t unicode_string_length; // _UNICODE_STRING.Length
addr_t unicode_string_buffer; // _UNICODE_STRING.Buffer
} WinOffsets;
static bool get_offsets(vmi_instance_t vmi, WinOffsets *wo) {
memset(wo, 0, sizeof(*wo));
if (VMI_FAILURE == vmi_get_kernel_struct_offset(vmi, "_EPROCESS", "ActiveProcessLinks", &wo->eprocess_list) ||
VMI_FAILURE == vmi_get_kernel_struct_offset(vmi, "_EPROCESS", "UniqueProcessId", &wo->eprocess_pid) ||
VMI_FAILURE == vmi_get_kernel_struct_offset(vmi, "_EPROCESS", "ImageFileName", &wo->eprocess_name) ||
VMI_FAILURE == vmi_get_kernel_struct_offset(vmi, "_EPROCESS", "Peb", &wo->eprocess_peb)) {
fprintf(stderr, "[-] Failed to get _EPROCESS offsets (check Rekall/Volatility profile).\n");
return false;
}
// PEB & RUPP
vmi_get_kernel_struct_offset(vmi, "_PEB", "ProcessParameters", &wo->peb_process_parameters);
vmi_get_kernel_struct_offset(vmi, "_PEB", "Ldr", &wo->peb_ldr);
vmi_get_kernel_struct_offset(vmi, "_RTL_USER_PROCESS_PARAMETERS", "CommandLine", &wo->rupp_commandline);
// LDR lists and entries
vmi_get_kernel_struct_offset(vmi, "_PEB_LDR_DATA", "InLoadOrderModuleList", &wo->ldr_inloadorder);
vmi_get_kernel_struct_offset(vmi, "_LDR_DATA_TABLE_ENTRY", "DllBase", &wo->ldr_entry_dllbase);
vmi_get_kernel_struct_offset(vmi, "_LDR_DATA_TABLE_ENTRY", "BaseDllName", &wo->ldr_entry_basedllname);
vmi_get_kernel_struct_offset(vmi, "_LDR_DATA_TABLE_ENTRY", "InLoadOrderLinks", &wo->ldr_entry_inloadlinks);
// UNICODE_STRING
vmi_get_kernel_struct_offset(vmi, "_UNICODE_STRING", "Length", &wo->unicode_string_length);
vmi_get_kernel_struct_offset(vmi, "_UNICODE_STRING", "Buffer", &wo->unicode_string_buffer);
printf("[dbg] Offsets: EPROCESS.Peb=0x%llx PEB.ProcessParameters=0x%llx RUPP.CommandLine=0x%llx PEB.Ldr=0x%llx LDR.InLoadOrder=0x%llx\n",
(unsigned long long)&wo -> eprocess_peb,
(unsigned long long)&wo -> peb_process_parameters,
(unsigned long long)&wo -> rupp_commandline,
(unsigned long long)&wo -> peb_ldr,
(unsigned long long)&wo -> ldr_inloadorder);
return true;
}
// ------------------------------
// Read helpers: UNICODE_STRING via DTB context OR PID fallback
// ------------------------------
static bool read_unicode_string_with_dtb(vmi_instance_t vmi, addr_t us_va, addr_t dtb, const WinOffsets *wo, char **out_utf8) {
*out_utf8 = NULL;
if (!us_va) return false;
ACCESS_CONTEXT(ctx,
.translate_mechanism = VMI_TM_PROCESS_DTB,
.dtb = dtb,
.addr = 0);
// Length (USHORT)
uint16_t len = 0;
ctx.addr = us_va + (wo->unicode_string_length ? wo->unicode_string_length : 0);
if (VMI_SUCCESS != vmi_read_16(vmi, &ctx, &len)) return false;
// Buffer (PWSTR)
addr_t bufptr = 0;
ctx.addr = us_va + (wo->unicode_string_buffer ? wo->unicode_string_buffer : (sizeof(uint16_t)*2));
if (VMI_SUCCESS != vmi_read_addr(vmi, &ctx, &bufptr)) return false;
if (!len || !bufptr) { *out_utf8 = strdup(""); return true; }
uint8_t *raw = (uint8_t*)malloc(len + 2);
if (!raw) return false;
ctx.addr = bufptr;
size_t br = 0;
if (VMI_SUCCESS != vmi_read(vmi, &ctx, len, raw, &br) || br != len) {
free(raw);
return false;
}
size_t outlen = len / 2;
char *utf8 = (char*)calloc(outlen + 1, 1);
if (!utf8) { free(raw); return false; }
for (size_t i = 0; i < outlen; i++) utf8[i] = (char)raw[i*2];
utf8[outlen] = '\0';
free(raw);
*out_utf8 = utf8;
return true;
}
static bool read_unicode_string_with_pid(vmi_instance_t vmi, addr_t us_va, vmi_pid_t pid, const WinOffsets *wo, char **out_utf8) {
*out_utf8 = NULL;
if (!us_va) return false;
uint16_t len = 0;
addr_t bufptr = 0;
if (VMI_SUCCESS != vmi_read_16_va(vmi, us_va + (wo->unicode_string_length ? wo->unicode_string_length : 0), pid, &len))
return false;
if (VMI_SUCCESS != vmi_read_addr_va(vmi, us_va + (wo->unicode_string_buffer ? wo->unicode_string_buffer : (sizeof(uint16_t)*2)), pid, &bufptr))
return false;
if (!len || !bufptr) { *out_utf8 = strdup(""); return true; }
uint8_t *raw = (uint8_t*)malloc(len + 2);
if (!raw) return false;
if (VMI_SUCCESS != vmi_read_va(vmi, bufptr, pid, len, raw, NULL)) { free(raw); return false; }
size_t outlen = len / 2;
char *utf8 = (char*)calloc(outlen + 1, 1);
if (!utf8) { free(raw); return false; }
for (size_t i = 0; i < outlen; i++) utf8[i] = (char)raw[i*2];
utf8[outlen] = '\0';
free(raw);
*out_utf8 = utf8;
return true;
}
static vmi_init_data_t* make_kvmi_init_data(const char *socket_path) {
// Allocate entries array dynamically (1 entry)
size_t sz = sizeof(vmi_init_data_t) + sizeof(vmi_init_data_entry_t);
vmi_init_data_t *init = (vmi_init_data_t*)malloc(sz);
if (!init) return NULL;
memset(init, 0, sz);
init->count = 1;
init->entry[0].type = VMI_INIT_DATA_KVMI_SOCKET;
init->entry[0].data = strdup(socket_path);
if (!init->entry[0].data) { free(init); return NULL; }
return init;
}
// ------------------------------
// Main
// ------------------------------
int main(int argc, char **argv) {
if (argc < 3) {
fprintf(stderr, "Usage: %s <vm_name> <guest_process_name>\n", argv[0]);
return 1;
}
const char *vm_name = argv[1];
const char *target_name = argv[2];
// 1) QEMU PID + maps
pid_t qpid = find_qemu_pid_for_guest(vm_name);
if (qpid > 0) {
printf("[+] Found QEMU PID for '%s': %d\n", vm_name, qpid);
char cmdbuf[8192];
if (read_cmdline(qpid, cmdbuf, sizeof(cmdbuf))) printf("[+] QEMU cmdline: %s\n", cmdbuf);
print_qemu_mem_maps(qpid);
} else {
fprintf(stderr, "[!] Could not find QEMU PID for guest '%s' (continuing with LibVMI).\n", vm_name);
}
// 2) LibVMI init (domain name via /etc/libvmi.conf)
vmi_instance_t vmi = NULL;
vmi_init_error_t err = VMI_INIT_ERROR_NONE;
const char *kvmi_socket = "/tmp/introspector";
vmi_init_data_t *init_data = make_kvmi_init_data(kvmi_socket);
if (VMI_FAILURE == vmi_init_complete(
&vmi,
vm_name,
VMI_INIT_DOMAINNAME,
init_data,
VMI_CONFIG_GLOBAL_FILE_ENTRY,
NULL,
&err)) {
fprintf(stderr, "[-] LibVMI init failed (err=%d)\n", err);
return 1;
}
printf("\n[+] LibVMI initialized for VM '%s'\n", vm_name);
if (vmi_get_ostype(vmi) != VMI_OS_WINDOWS) {
fprintf(stderr, "[-] Guest OS is not Windows.\n");
vmi_destroy(vmi);
return 1;
}
printf("[+] Guest OS detected: Windows\n");
// 3) Offsets
WinOffsets wo;
if (!get_offsets(vmi, &wo)) {
vmi_destroy(vmi);
return 1;
}
// 4) Resolve PsActiveProcessHead VA, then read first Flink
addr_t list_head_va = 0;
if (VMI_FAILURE == vmi_translate_ksym2v(vmi, "PsActiveProcessHead", &list_head_va) || !list_head_va) {
fprintf(stderr, "[-] Could not translate PsActiveProcessHead.\n");
vmi_destroy(vmi);
return 1;
}
printf("[+] PsActiveProcessHead @ 0x%llx\n", (unsigned long long)list_head_va);
addr_t next = 0;
if (VMI_FAILURE == vmi_read_addr_va(vmi, list_head_va, 0, &next)) { // 0 => kernel address space
fprintf(stderr, "[-] Failed to read Flink from PsActiveProcessHead.\n");
vmi_destroy(vmi);
return 1;
}
// 5) Walk EPROCESS list
char target_lower[260];
strncpy(target_lower, target_name, sizeof(target_lower)-1);
target_lower[sizeof(target_lower)-1] = '\0';
to_lower(target_lower);
addr_t target_eproc = 0;
vmi_pid_t target_pid = 0;
addr_t target_dtb = 0;
bool have_dtb = false;
printf("[+] Scanning EPROCESS list for '%s'...\n", target_lower);
while (next != list_head_va) {
addr_t eproc = next - wo.eprocess_list;
// read next Flink early (kernel VA)
if (VMI_FAILURE == vmi_read_addr_va(vmi, next, 0, &next)) {
fprintf(stderr, "[-] Failed to read next Flink.\n");
break;
}
// ImageFileName (char[15]) — kernel VA
char imagename[16] = {0};
if (VMI_SUCCESS != vmi_read_va(vmi, eproc + wo.eprocess_name, 0, sizeof(imagename)-1, imagename, NULL))
continue;
imagename[15] = '\0';
char lowername[16];
strncpy(lowername, imagename, sizeof(lowername));
lowername[sizeof(lowername)-1] = '\0';
to_lower(lowername);
// UniqueProcessId (HANDLE/pointer-sized) — kernel VA
uint64_t pid64 = 0;
if (VMI_SUCCESS != vmi_read_64_va(vmi, eproc + wo.eprocess_pid, 0, &pid64))
continue;
// Match by name (suffix or exact)
if (strcmp(lowername, target_lower) == 0 || (strlen(lowername) < strlen(target_lower) && strcasestr_bool(target_lower, lowername))) {
target_eproc = eproc;
target_pid = (vmi_pid_t)pid64;
// Resolve DTB/CR3 for process
addr_t dtb = 0;
if (VMI_SUCCESS == vmi_pid_to_dtb(vmi, target_pid, &dtb) && dtb) {
target_dtb = dtb;
have_dtb = true;
}
printf("[+] Candidate: %-15s PID=%" PRIu64 " EPROCESS=0x%llx DTB=0x%llx\n",
imagename, (uint64_t)target_pid,
(unsigned long long)target_eproc, (unsigned long long)target_dtb);
break;
}
}
if (!target_eproc) {
fprintf(stderr, "[-] Target process '%s' not found.\n", target_name);
vmi_destroy(vmi);
return 1;
}
// 6) Verify via PEB -> ProcessParameters -> CommandLine
addr_t peb = 0;
if (VMI_SUCCESS != vmi_read_addr_va(vmi, target_eproc + wo.eprocess_peb, 0, &peb) || !peb) {
fprintf(stderr, "[-] Failed to read PEB pointer.\n");
} else {
addr_t proc_params = 0;
if (have_dtb) {
ACCESS_CONTEXT(ctx,
.translate_mechanism = VMI_TM_PROCESS_DTB,
.dtb = target_dtb,
.addr = peb + wo.peb_process_parameters);
if (VMI_SUCCESS == vmi_read_addr(vmi, &ctx, &proc_params) && proc_params) {
addr_t us_cmdline_va = proc_params + wo.rupp_commandline; // UNICODE_STRING is inline
char *cmd = NULL;
if (read_unicode_string_with_dtb(vmi, us_cmdline_va, target_dtb, &wo, &cmd)) { //end replace
printf("[+] Verified CommandLine: %s\n", cmd ? cmd : "");
free(cmd);
} else {
printf("[!] Could not decode CommandLine (DTB path).\n");
}
} else {
printf("[!] Could not read PEB->ProcessParameters (DTB path).\n");
}
} else {
// Fallback: PID-based helpers
if (VMI_SUCCESS == vmi_read_addr_va(vmi, peb + wo.peb_process_parameters, target_pid, &proc_params) && proc_params) {
addr_t us_cmdline_va = proc_params + wo.rupp_commandline; // inline UNICODE_STRING
char *cmd = NULL;
if (read_unicode_string_with_pid(vmi, us_cmdline_va, target_pid, &wo, &cmd)) { //end replace
printf("[+] Verified CommandLine: %s\n", cmd ? cmd : "");
free(cmd);
} else {
printf("[!] Could not decode CommandLine (PID path).\n");
}
} else {
printf("[!] Could not read PEB->ProcessParameters (PID path).\n");
}
}
}
// 7) Bonus: first loaded module base bytes (DTB preferred, PID fallback)
if (wo.peb_ldr && wo.ldr_inloadorder) {
addr_t peb_ldr = 0;
if (have_dtb) {
ACCESS_CONTEXT(ctx,
.translate_mechanism = VMI_TM_PROCESS_DTB,
.dtb = target_dtb,
.addr = peb + wo.peb_ldr);
if (VMI_SUCCESS == vmi_read_addr(vmi, &ctx, &peb_ldr) && peb_ldr) {
addr_t list = 0;
ctx.addr = peb_ldr + wo.ldr_inloadorder;
if (VMI_SUCCESS == vmi_read_addr(vmi, &ctx, &list) && list) {
addr_t flink = 0;
ctx.addr = list;
if (VMI_SUCCESS == vmi_read_addr(vmi, &ctx, &flink) && flink && flink != list) {
addr_t ldr_entry = flink - wo.ldr_entry_inloadlinks;
addr_t dllbase = 0;
ctx.addr = ldr_entry + wo.ldr_entry_dllbase;
if (VMI_SUCCESS == vmi_read_addr(vmi, &ctx, &dllbase) && dllbase) {
uint8_t buf[32] = {0};
ctx.addr = dllbase;
size_t br2 = 0;
if (VMI_SUCCESS == vmi_read(vmi, &ctx, sizeof(buf), buf, &br2) && br2 == sizeof(buf)) {
printf("[+] First module DllBase=0x%llx ; first 32 bytes:", (unsigned long long)dllbase);
for (size_t i = 0; i < sizeof(buf); i++) {
if (i % 16 == 0) printf("\n ");
printf("%02x ", buf[i]);
}
printf("\n");
} else {
printf("[!] Failed to read %zu bytes from module base VA 0x%llx.\n",
sizeof(buf), (unsigned long long)dllbase);
}
}
}
}
}
} else {
if (VMI_SUCCESS == vmi_read_addr_va(vmi, peb + wo.peb_ldr, target_pid, &peb_ldr) && peb_ldr) {
addr_t list = 0;
if (VMI_SUCCESS == vmi_read_addr_va(vmi, peb_ldr + wo.ldr_inloadorder, target_pid, &list) && list) {
addr_t flink = 0;
if (VMI_SUCCESS == vmi_read_addr_va(vmi, list, target_pid, &flink) && flink && flink != list) {
addr_t ldr_entry = flink - wo.ldr_entry_inloadlinks;
addr_t dllbase = 0;
if (VMI_SUCCESS == vmi_read_addr_va(vmi, ldr_entry + wo.ldr_entry_dllbase, target_pid, &dllbase) && dllbase) {
uint8_t buf[32] = {0};
if (VMI_SUCCESS == vmi_read_va(vmi, dllbase, target_pid, sizeof(buf), buf, NULL)) {
printf("[+] First module DllBase=0x%llx ; first 32 bytes:", (unsigned long long)dllbase);
for (size_t i = 0; i < sizeof(buf); i++) {
if (i % 16 == 0) printf("\n ");
printf("%02x ", buf[i]);
}
printf("\n");
}
}
}
}
}
}
}
printf("\n[+] Target process summary:\n");
printf(" Name : %s\n", target_name);
printf(" PID : %" PRIu64 "\n", (uint64_t)target_pid);
printf(" EPROCESS : 0x%llx\n", (unsigned long long)target_eproc);
printf(" DTB/CR3 : 0x%llx (%s)\n", (unsigned long long)target_dtb, (have_dtb ? "resolved via vmi_pid_to_dtb" : "fallback to PID reads"));
vmi_destroy(vmi);
printf("[+] LibVMI destroyed. Milestone 1 complete.\n");
return 0;
}
This always fails to decode the command line for any given process. I feel like I'm going crazy here because I can successfully get _all other information:
[+] LibVMI initialized for VM 'win10'
[+] Guest OS detected: Windows
[dbg] Offsets: EPROCESS.Peb=0x7ffe96f5a018 PEB.ProcessParameters=0x7ffe96f5a020 RUPP.CommandLine=0x7ffe96f5a030 PEB.Ldr=0x7ffe96f5a028 LDR.InLoadOrder=0x7ffe96f5a038
[+] PsActiveProcessHead @ 0xfffff80211e1e270
[+] Scanning EPROCESS list for 'fivem_gtaprocess.exe'...
[+] Candidate: FiveM_GTAProce PID=9132 EPROCESS=0xffffbe0c7b3ee080 DTB=0x31bf14000
[!] Could not read PEB->ProcessParameters (DTB path).
[+] Target process summary:
Name : TestProcess.exe
PID : 9132
EPROCESS : 0xffffbe0c7b3ee080
DTB/CR3 : 0x31bf14000 (resolved via vmi_pid_to_dtb)
--Destroying KVM driver
[+] LibVMI destroyed.
However I believe the DTB is wrong, it never resolves to userspace only the kernel's DTB which I think could be part of it. Any ideas?
Metadata
Metadata
Assignees
Labels
No labels