Skip to main content
Dr.Semu monitors malware behavior by intercepting system calls before they enter the Windows kernel. This is achieved using DynamoRIO, a dynamic binary instrumentation framework that operates entirely in user-mode.

How monitoring works

From README.md:19-23:
Dr.Semu uses DynamoRIO (Dynamic Instrumentation Tool Platform) to intercept a thread when it’s about to cross the user-kernel line. It has the same effect as hooking SSDT but from the user-mode and without hooking anything. At this phase, Dr.Semu produces a JSON file, which contains information from the interception.

DynamoRIO fundamentals

What is DynamoRIO?

DynamoRIO is a runtime code manipulation system that:
  • Intercepts application code at execution time
  • Can modify or instrument any instruction
  • Provides APIs for analyzing program behavior
  • Works without source code or debugging symbols

Integration with Dr.Semu

Dr.Semu implements a DynamoRIO client - a DLL that hooks into the target process:
Target.exe

drrun.exe (DynamoRIO runtime)

drsemu_x64.dll or drsemu_x86.dll (Dr.Semu client)

System call interception
The client is loaded at DrSemu.cpp:114-284.

System call interception

Registration

Dr.Semu registers for system call events:
// DrSemu.cpp:264-266
dr_register_filter_syscall_event(event_filter_syscall);
drmgr_register_pre_syscall_event(event_pre_syscall);
drmgr_register_post_syscall_event(event_post_syscall);

Three-phase interception

1

Filter phase

DynamoRIO calls event_filter_syscall to determine if a syscall should be intercepted.
// DrSemu.cpp:414-417
static bool event_filter_syscall(void* drcontext, int sysnum) {
    return dr_semu::syscall::syscall_numbers.find(sysnum) != 
           dr_semu::syscall::syscall_numbers.end();
}
Only syscalls in the configured list are intercepted - others execute normally for performance.
2

Pre-syscall phase

Before the syscall enters the kernel, event_pre_syscall is invoked.
// DrSemu.cpp:420-740
static bool event_pre_syscall(void* drcontext, int sysnum) {
    const auto syscall_name = dr_semu::syscall::syscall_numbers[sysnum];
    
    if (syscall_name == NTWRITEFILE) {
        return dr_semu::filesystem::handlers::NtWriteFile_handler(drcontext);
    }
    // ... handle other syscalls
}
This is where Dr.Semu:
  • Extracts parameters from CPU registers
  • Translates virtual paths to real paths
  • Logs the operation to JSON
  • Can modify parameters or block the call
3

Post-syscall phase

After the kernel executes the syscall, event_post_syscall runs.
// DrSemu.cpp:743-755
void event_post_syscall(void* drcontext, int sysnum) {
    if (syscall_name == NTCREATEUSERPROCESS) {
        dr_semu::process::handlers::NtCreateUserProcess_post_handler(drcontext);
    }
}
Used for:
  • Capturing return values and output parameters
  • Child process instrumentation
  • Post-execution cleanup

Monitored system calls

Dr.Semu monitors over 70 system calls across multiple categories:

Filesystem

19 calls including NtCreateFile, NtWriteFile, NtDeleteFile

Registry

28 calls including NtSetValueKey, NtCreateKey, NtDeleteKey

Processes

15 calls including NtCreateUserProcess, NtWriteVirtualMemory

Networking

6 calls including URLDownloadToFileW, gethostbyname

Objects

10 calls including NtCreateMutant, NtCreateEvent

System

4 calls including NtQuerySystemInformation, NtLoadDriver

Complete list by category

  • NtWriteFile
  • NtClose
  • NtCreateFile
  • NtOpenFile
  • NtCreateSection
  • NtMapViewOfSection
  • NtQueryInformationFile
  • NtSetInformationFile
  • NtQueryAttributesFile
  • NtDeleteFile
  • NtCreateDirectoryObject
  • NtCreatePagingFile
  • NtCreateIoCompletion
  • NtQueryFullAttributesFile
  • NtQueryDirectoryFile
  • NtQueryDirectoryFileEx
  • NtCreateSymbolicLinkObject
  • NtFlushBuffersFile
  • NtOpenKey, NtOpenKeyEx
  • NtCreateKey
  • NtDeleteValueKey, NtDeleteKey
  • NtQueryValueKey, NtQueryKey
  • NtEnumerateKey, NtEnumerateValueKey
  • NtSetValueKey
  • NtNotifyChangeKey, NtNotifyChangeMultipleKeys
  • NtCreateKeyTransacted, NtOpenKeyTransacted, NtOpenKeyTransactedEx
  • NtCompactKeys, NtCompressKey
  • NtFlushKey, NtFreezeRegistry
  • NtInitializeRegistry
  • NtLoadKey, NtLoadKey2, NtLoadKeyEx
  • NtSaveKey, NtSaveKeyEx
  • NtLockRegistryKey
  • NtQueryMultipleValueKey
  • NtQueryOpenSubKeys, NtQueryOpenSubKeysEx
  • NtCreateUserProcess
  • NtOpenProcess
  • NtCreateProcess, NtCreateProcessEx
  • NtOpenThread
  • NtDelayExecution
  • NtSuspendProcess
  • NtWriteVirtualMemory
  • NtSetInformationProcess
  • NtContinue
  • NtProtectVirtualMemory
  • NtSetContextThread
  • NtQueryVirtualMemory
  • NtQueryInformationProcess
  • WSAStartup
  • URLDownloadToFileW
  • URLDownloadToCacheFileW
  • gethostbyname
  • InternetOpenUrlW
  • InternetOpenUrlA
  • NtCreateMutant, NtOpenMutant
  • NtCreateMailslotFile
  • NtCreateSemaphore, NtOpenSemaphore
  • NtCreateEvent, NtOpenEvent
  • NtWaitForSingleObject
  • NtQueryObject
  • NtQuerySystemInformation
  • NtLoadDriver
  • NtUserSystemParametersInfo
  • NtRaiseHardError

Parameter extraction

Reading parameters

DynamoRIO provides the drcontext handle to access CPU registers:
// Example: NtWriteFile handler
HANDLE file_handle = (HANDLE)dr_syscall_get_param(drcontext, 0);
PVOID buffer = (PVOID)dr_syscall_get_param(drcontext, 4);
ULONG length = (ULONG)dr_syscall_get_param(drcontext, 5);
Parameters are accessed by position matching the syscall’s signature.

Dereferencing pointers

Many syscall parameters are pointers to structures:
// Extract UNICODE_STRING for file path
UNICODE_STRING* file_path_ptr = (UNICODE_STRING*)dr_syscall_get_param(drcontext, 3);
UNICODE_STRING file_path;
if (dr_safe_read(file_path_ptr, sizeof(UNICODE_STRING), &file_path, nullptr)) {
    std::wstring path(file_path.Buffer, file_path.Length / sizeof(wchar_t));
    // Process path...
}
dr_safe_read is used to safely read memory that might be invalid.

Handling nested structures

Complex structures like OBJECT_ATTRIBUTES contain pointers to other structures:
// OBJECT_ATTRIBUTES->ObjectName is a UNICODE_STRING*
OBJECT_ATTRIBUTES* obj_attr = (OBJECT_ATTRIBUTES*)dr_syscall_get_param(drcontext, 2);
UNICODE_STRING* name_ptr = obj_attr->ObjectName;

Behavior logging

JSON structure

Each intercepted call is logged as a JSON object:
{
  "NtCreateFile": {
    "timestamp": "2024-03-03T14:32:10.123",
    "thread_id": 4512,
    "before": {
      "desired_access": "0x120116",
      "file_path": "\\??\\C:\\Users\\User\\malware.exe",
      "creation_disposition": "OPEN_IF"
    },
    "after": {
      "status": "0x00000000",
      "file_handle": "0x000001a4"
    },
    "success": true
  }
}

Concurrent vector

Logs are stored in a thread-safe concurrent vector:
// DrSemu.cpp:369-377
json json_dynamic;
if (!dr_semu::shared_variables::json_concurrent_vector.empty()) {
    json_dynamic = dr_semu::shared_variables::json_concurrent_vector;
} else {
    json_dynamic["empty"] = 1;
}

Writing reports

On process exit, the JSON is written to disk:
// DrSemu.cpp:386-392
const auto out_json_file = dr_open_file(
    (report_directory + "\\" + current_proc_id_string + ".json").c_str(),
    DR_FILE_WRITE_OVERWRITE);
const auto json_str = json_dynamic.dump();
dr_write_file(out_json_file, json_str.data(), json_str.length());
dr_close_file(out_json_file);
Each process gets its own JSON file named by PID (e.g., 1234.json).

Path translation

Virtual to real path conversion

When malware accesses a file, Dr.Semu translates the path: Malware sees:
C:\Windows\System32\cmd.exe
Dr.Semu logs:
{
  "virtual_path": "C:\\Windows\\System32\\cmd.exe",
  "real_path": "C:\\Users\\User\\AppData\\Local\\Temp\\dr_semu_0\\Windows\\System32\\cmd.exe"
}

Device path translation

Dr.Semu converts DOS device paths to long paths:
// DrSemu.cpp:42-57
inline std::wstring get_virtual_root_device_form() {
    TCHAR device_name[MAX_PATH]{};
    const std::wstring virtual_drive_name(
        dr_semu::shared_variables::virtual_filesystem_path, 0, 2);
    QueryDosDevice(virtual_drive_name.c_str(), device_name, MAX_PATH);
    
    const std::wstring device_path(device_name, wcslen(device_name));
    return device_path + std::wstring{
        dr_semu::shared_variables::virtual_filesystem_path, 2};
}
Example:
  • DOS path: C:\Windows\System32
  • Device path: \Device\HarddiskVolume3\Windows\System32

Module load tracking

Dr.Semu tracks when DLLs are loaded:
// DrSemu.cpp:772-790
void module_load_event(void* drcontext, const module_data_t* mod, bool Loaded) {
    // Wrap COM functions
    wrap_function(mod->handle, "CoCreateInstance", 
                  dr_semu::com::handlers::pre_co_create_instance, nullptr);
    
    // Wrap networking functions
    wrap_function(mod->handle, "URLDownloadToFileW",
                  dr_semu::networking::handlers::pro_url_download_to_file, nullptr);
    // ...
}
This allows Dr.Semu to intercept high-level API calls in addition to system calls.

Performance impact

Overhead sources

  1. Code cache - DynamoRIO translates and caches code blocks
  2. Instrumentation - Additional instructions injected at syscall boundaries
  3. Logging - JSON serialization and I/O
  4. Path translation - String operations for every filesystem access

Measured impact

From testing:
  • Light applications: 2-3x slowdown
  • I/O heavy applications: 5-10x slowdown
  • CPU heavy applications: 1.5-2x slowdown
The slowdown is detectable and may alert anti-analysis malware. Consider this when analyzing sophisticated samples.

Optimization strategies

  1. Selective monitoring - Only intercept necessary syscalls
  2. Lazy logging - Defer JSON serialization until process exit
  3. Efficient data structures - Use concurrent containers for thread safety without locks
  4. Minimal instrumentation - DynamoRIO’s lightweight mode

Time limits

Dr.Semu can terminate analysis after a timeout:
// DrSemu.cpp:34-40
void sleep_and_die(void* limit) {
    const auto time_limit = reinterpret_cast<DWORD>(limit);
    dr_sleep(time_limit SECONDS);
    dr_exit_process(0);
}
Set via --time_limit option:
DrSemu.exe --target malware.exe --time_limit 120
Default is 120 seconds (LauncherCLI.cpp:93).

Process termination handling

Soft kills

Dr.Semu implements “soft kills” for graceful shutdown:
// DrSemu.cpp:86-94
static bool soft_kill_event(process_id_t pid, int exit_code) {
    const auto result = dr_nudge_client_ex(pid, client_id,
        NUDGE_TERMINATE_PROCESS | static_cast<uint64>(exit_code) << 32, 0);
    return result == DR_SUCCESS;
}
This allows instrumented processes to flush logs before terminating.

Exit event

On process exit, cleanup is performed:
// DrSemu.cpp:344-410
static void event_exit() {
    // Calculate execution duration
    const auto duration = std::chrono::duration_cast<std::chrono::seconds>(
        end_time - dr_semu::shared_variables::initial_time).count();
    
    // Write JSON report
    // Unregister event handlers
    // Notify launcher
}

See also

Filesystem API

Complete list of filesystem syscalls

Registry API

Complete list of registry syscalls

DynamoRIO integration

Advanced DynamoRIO client details

JSON schema

Behavior report format

Build docs developers (and LLMs) love