I'm working on a C++ program to track newly launched processes using the Toolhelp32Snapshot
API. Here's the relevant code snippet:
std::optional<Process> getNewProcess() {
static DWORD last_created_process_id = 0;
DWORD newest_process_id = 0;
FILETIME newest_creation_time = {};
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) {
return std::nullopt;
}
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32);
if (!Process32First(hSnapshot, &pe32)) {
CloseHandle(hSnapshot);
return std::nullopt;
}
do {
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pe32.th32ProcessID);
if (hProcess) {
FILETIME creation_time, exit_time, kernel_time, user_time;
if (GetProcessTimes(hProcess, &creation_time, &exit_time, &kernel_time, &user_time)) {
if (CompareFileTime(&creation_time, &newest_creation_time) > 0) {
newest_creation_time = creation_time;
newest_process_id = pe32.th32ProcessID;
}
}
CloseHandle(hProcess);
}
} while (Process32Next(hSnapshot, &pe32));
CloseHandle(hSnapshot);
if (newest_process_id == 0) {
return std::nullopt;
} else {
if (newest_process_id != last_created_process_id && newest_process_id != GetCurrentProcessId()) {
last_created_process_id = newest_process_id;
return Process(newest_process_id);
} else {
return std::nullopt;
}
}
}
}
This works well for most cases. However, when a launched program triggers a UAC prompt, my code detects "consent.exe" and "ctfmon.exe" instead of the actual program that initiated the UAC prompt.
I'm also exploring the possibility of using a hook to monitor CreateFile calls and extract the program name from lpFileName
. However, I haven't been successful in implementing this yet.
Is there a way to reliably identify the program path that caused the UAC prompt? Thank you!
I found two solutions to know when a user launches an application before UAC is being prompted.
Microsoft-Windows-Kernel-Process
provider, which monitors process, thread and image activity. There's a good explanation on it on Windows 10 System Programming, Part 1 by Pavel Yosifovich.I chose the ETW solution for my project. ETW offered me the performance I needed without the complexity and potential vulnerabilities of writing a kernel driver.
I wrote a very simple example of how I achieved the consumption of ETW process events:
#include <iostream>
#include <thread>
#include <ranges>
#include <functional>
#include <string>
#include <vector>
#define INITGUID
#include <guiddef.h>
#include <wbemidl.h>
#include <wmistr.h>
#include <evntrace.h>
#include <comdef.h>
#include <guiddef.h>
#include <tdh.h>
#pragma comment(lib, "tdh.lib")
// logman query providers
DEFINE_GUID( Microsoft_Windows_Kernel_Process,
0x22fb2cd6,
0x0e7b,
0x422b,
0xa0, 0xc7, 0x2f, 0xad, 0x1f, 0xd0, 0xe7, 0x16 );
class Tracer {
public:
Tracer(std::wstring name, const DWORD flags)
: session_name( std::move( name ) ) {
buffer.resize( sizeof( EVENT_TRACE_PROPERTIES ) + ( session_name.length() + 1 ) * sizeof(
std::wstring::value_type ), 0 );
// Trace Session
auto &props = *reinterpret_cast< EVENT_TRACE_PROPERTIES * >( buffer.data() );
props.Wnode.BufferSize = buffer.size();
props.Wnode.ClientContext = 1;
props.LogFileMode = EVENT_TRACE_REAL_TIME_MODE;
props.LoggerNameOffset = sizeof( EVENT_TRACE_PROPERTIES );
auto status = StartTraceW( &handler, session_name.data(), &props );
if ( status == ERROR_ALREADY_EXISTS ) {
Tracer::stop();
status = StartTraceW( &handler, session_name.data(), &props );
}
if ( status != ERROR_SUCCESS ) {
std::cerr << "StartTraceW failed with error " << status << std::endl;
Tracer::stop();
return;
}
ENABLE_TRACE_PARAMETERS params{};
params.Version = ENABLE_TRACE_PARAMETERS_VERSION_2;
status = EnableTraceEx2( handler, &Microsoft_Windows_Kernel_Process, EVENT_CONTROL_CODE_ENABLE_PROVIDER,
TRACE_LEVEL_VERBOSE, flags, 0, 0, ¶ms );
if ( status != ERROR_SUCCESS ) {
std::cerr << "EnableTraceEx2 failed with error " << status << std::endl;
Tracer::stop();
return;
}
// Establish a session
EVENT_TRACE_LOGFILEW trace{};
trace.LoggerName = session_name.data();
trace.LogFileName = nullptr;
trace.Context = this;
trace.EventRecordCallback = [] (auto *record) {
if ( record->UserContext ) static_cast< Tracer * >( record->UserContext )->process_trace( record );
};
trace.ProcessTraceMode = PROCESS_TRACE_MODE_EVENT_RECORD | PROCESS_TRACE_MODE_REAL_TIME;
handler = OpenTraceW( &trace );
if ( handler == INVALID_PROCESSTRACE_HANDLE ) {
const auto err = GetLastError();
std::cerr << "OpenTraceW failed with error " << err << std::endl;
Tracer::stop();
throw err;
}
}
void run(const std::function< void(EVENT_RECORD *) > &func) {
process_trace = func;
const auto status = ProcessTrace( &handler, 1, 0, 0 );
if ( status != ERROR_SUCCESS && status != ERROR_CANCELLED ) {
std::cerr << "ProcessTrace() failed: " << status << std::endl;
Tracer::stop();
}
}
void stop() {
ControlTraceW( 0, session_name.data(), reinterpret_cast< EVENT_TRACE_PROPERTIES * >( buffer.data() ),
EVENT_TRACE_CONTROL_STOP );
CloseTrace( handler );
}
~Tracer() { Tracer::stop(); }
private:
std::vector< std::uint8_t > buffer;
std::wstring session_name;
TRACEHANDLE handler = 0;
std::function< void(EVENT_RECORD *) > process_trace;
};
void recordFunction(EVENT_RECORD *record) {
if ( IsEqualGUID( record->EventHeader.ProviderId, Microsoft_Windows_Kernel_Process ) &&
record->EventHeader.EventDescriptor.Opcode == EVENT_TRACE_TYPE_INFO ) { return; }
std::vector< std::uint8_t > buffer( sizeof( TRACE_EVENT_INFO ) );
TRACE_EVENT_INFO *p_info = nullptr;
TDHSTATUS status = 0;
for ( const auto i: std::views::iota( 0, 2 ) ) {
auto buffer_size = static_cast< DWORD >( buffer.size() );
p_info = reinterpret_cast< TRACE_EVENT_INFO * >( buffer.data() );
status = TdhGetEventInformation( record, 0, nullptr, p_info, &buffer_size );
if ( status == ERROR_INSUFFICIENT_BUFFER && i == 0 ) { buffer.resize( buffer_size ); }
}
if ( status != ERROR_SUCCESS ) {
std::cerr << "TdhGetEventInformation failed with error " << status << std::endl;
return;
}
for ( auto i = 0; i < p_info->TopLevelPropertyCount; i++ ) {
const auto info = p_info->EventPropertyInfoArray[ i ];
auto property = std::wstring(
reinterpret_cast< LPWSTR >( reinterpret_cast< BYTE * >( p_info ) + info.NameOffset ) );
if ( property == L"ImageName" ) {
PROPERTY_DATA_DESCRIPTOR descriptor;
descriptor.ArrayIndex = 0;
descriptor.PropertyName = reinterpret_cast< ULONGLONG >( property.data() );
ULONG data_size;
auto result = TdhGetPropertySize( record, 0, nullptr, 1, &descriptor, &data_size );
if ( result != ERROR_SUCCESS ) {
std::cerr << "TdhGetPropertySize failed with error " << result << std::endl;
return;
}
std::vector< BYTE > data( data_size );
descriptor = PROPERTY_DATA_DESCRIPTOR{};
descriptor.ArrayIndex = 0;
descriptor.PropertyName = reinterpret_cast< ULONGLONG >( property.data() );
result = TdhGetProperty( record, 0, nullptr, 1, &descriptor, data.size(),
data.data() );
if ( result != ERROR_SUCCESS ) {
std::cerr << "TdhGetProperty failed with error " << result << std::endl;
return;
}
std::wcout << "Process ID: " << record->EventHeader.ProcessId << std::endl;
std::cout << "Image Name: ";
if ( info.nonStructType.InType == TDH_INTYPE_ANSISTRING ) {
std::cout << std::string( data.begin(), data.end() - 1 ) << std::endl;
}
else if ( info.nonStructType.InType == TDH_INTYPE_UNICODESTRING ) {
std::wcout << std::wstring( reinterpret_cast< const wchar_t * >( data.data() ),
( data.size() - 1 ) / sizeof( wchar_t ) ) << std::endl;
}
else { std::cout << "Unknown" << std::endl; }
}
}
}
int main() {
static constexpr auto WinEvent_Process = 0x10u;
static constexpr auto WinEvent_Thread = 0x20u;
static constexpr auto WinEvent_Image = 0x40u;
Tracer tracer( L"Agent", WinEvent_Process | WinEvent_Thread | WinEvent_Image );
std::thread th( [&tracer]() { tracer.run( recordFunction ); } );
th.join();
}