c++winapiuac

How to Detect the Actual Program Name Triggering UAC?


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!


Solution

  • I found two solutions to know when a user launches an application before UAC is being prompted.

    1. Kernel-Mode Driver: Writing a kernel-mode driver to subscribe to process creation events using PsSetCreateProcessNotifyRoutineEx and communicate with a user-mode application using IOCTL. There's an example that I found online here.
    2. Event Tracing for Windows (ETW): Using ETW to consume events from the 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, &params );
            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();
    }
    

    Which results in: enter image description here