获取 LSASS PID 的十四种方法

获取 LSASS PID 的十四种方法

一、介绍

在注入 shellcode 或内存 dump 之前,对进程进行枚举是必要的。攻击者倾向于使用 Process32FirstProcess32NextCreateToolhelp32Snapshot 来收集正在运行的进程列表。如果他们更精通技术,将直接使用 NtQuerySystemInformation 系统调用。

虽然这篇文章将专注于获取 LSASS 的 PID,但此处描述的方法可以适用于任何进程的 PID。其中一些是众所周知的并且之前已经讨论过,但也有一些很多读者不熟悉的新内容。需要说明的是,有一些方法可以使用 Window 对象获取 PID,但 LSASS 没有,这里不做讨论。

二、获取 LSASS PID 的十四方法

lsass.exe 是一个系统进程,用于 Windows 系统的安全机制,本地安全和登录策略。

2.1 NtQuerySystemInformation / NtQuerySystemInformationEx

CreateToolhelp32Snapshot() 和 EnumProcesses 都将此系统调用与 SystemProcessInformation 类一起使用。在避免使用 Win32 API 时,经常会看到使用此系统调用。返回的数据还包含线程信息,因此被 Thread32First 和 Thread32Next 使用。另一方面,模块必须从每个进程的 PEB 中手动读取,因此 Module32First 和 Module32Next 需要额外的系统调用,如 NtOpenProcessNtReadVirtualMemory

//
// Use NtQuerySystemInformation(SystemProcessInformation)
//
DWORD GetLsaPidFromName(void) {
    NTSTATUS Status;
    DWORD ProcessId = 0, Length = 1024;
    std::vector ProcessList(1024);
    
    do {
        do {
            Status = NtQuerySystemInformation(
                        SystemProcessInformation, 
                        ProcessList.data(), 
                        Length, 
                        &Length
                        );

            if (Status == STATUS_INFO_LENGTH_MISMATCH) {
                ProcessList.resize(Length);
            }
        } while (Status == STATUS_INFO_LENGTH_MISMATCH);

        if (!NT_SUCCESS(Status)) {
            SetLastError(RtlNtStatusToDosError(Status));
            printf("NtQuerySystemInformation(SystemProcessInformation) failed : %ld", GetLastError());
            break;
        }
    
        DWORD Offset = 0;
        PSYSTEM_PROCESS_INFORMATION ProcessEntry = NULL;
    
        do {
            ProcessEntry = (PSYSTEM_PROCESS_INFORMATION)(ProcessList.data() + Offset);

            if (!lstrcmpiW(ProcessEntry->ImageName.Buffer, L"lsass.exe")) {
                ProcessId = HandleToLong(ProcessEntry->UniqueProcessId);
                break;
            }
          
            Offset += ProcessEntry->NextEntryOffset;
        } while (ProcessEntry->NextEntryOffset);
    } while(FALSE);
    
    return ProcessId;
}

2.2 Windows Terminal Services (WTS)

WTSEnumerateProcesses API 使用 RPC 服务来获取进程列表,在内部,该服务使用 NtQuerySystemInformation (SystemProcessInformation),但由于调用是在另一个进程中进行的,因此它有时可能对逃避进程枚举检测很有用。

//
// Use Windows Terminal Services
//
DWORD GetLsaPidFromWTS(void) {
    DWORD ProcessId = 0;
    
    do {
        PWTS_PROCESS_INFO info = NULL;
        DWORD cnt = 0;
        
        BOOL Result;
        Result = WTSEnumerateProcesses(WTS_CURRENT_SERVER_HANDLE, NULL, 1, &info, &cnt);
        
        if (!Result) {
            printf("WTSEnumerateProcesses() failed : %ld\n", GetLastError());
            break;
        }
        
        for(DWORD i=0; i<cnt; i++) {
            if (!lstrcmpiW(info[i].pProcessName, L"lsass.exe")) {
                ProcessId = info[i].ProcessId;
                break;
            }
        }
        WTSFreeMemory(info);
    } while (FALSE);
    
    return ProcessId;
}

2.3 Windows Management Instrumentation (WMI)

我们可以使用 WMI 获取类 Win32_Process 的实例列表,NtQuerySystemInformation() 的调用发生在 WMI 提供程序主机 (wmiprvse.exe) 中,与 WTS 一样,枚举发生在另一个进程中,可能有助于规避检测。

//
// Windows Management Instrumentation (WMI)
//
DWORD GetPidForLsass(IWbemServices *svc) {
    IEnumWbemClassObject *ent = NULL;
    DWORD ProcessId = 0;
    
    do {
        HRESULT hr;
        
        hr = svc->CreateInstanceEnum(
                L"Win32_Process", 
                WBEM_FLAG_RETURN_IMMEDIATELY | 
                WBEM_FLAG_FORWARD_ONLY, 
                NULL, 
                &ent); 
        
        if (FAILED(hr)) {
            printf("IWbemServices::CreateInstanceEnum() failed : %08lX\n", hr);
            break;
        }
          
        for (;!ProcessId;) {
            ULONG cnt = 0;
            IWbemClassObject *obj = NULL;
            
            hr  = ent->Next(INFINITE, 1, &obj, &cnt);

            if(!cnt) break;

            VARIANT name;
            VariantInit(&name);
            
            hr = obj->Get(L"Name", 0, &name, NULL, NULL);

            if (SUCCEEDED(hr)) {
                if (!lstrcmpiW(V_BSTR(&name), L"lsass.exe")) {
                    VARIANT pid;
                    VariantInit(&pid);
                    
                    hr = obj->Get(L"ProcessID", 0, &pid, NULL, NULL);
                
                    if (SUCCEEDED(hr)) {
                        ProcessId = V_UI4(&pid);
                        VariantClear(&pid);
                    }
                }
                VariantClear(&name);
            }
            obj->Release();
        }
    } while(FALSE);
    
    if (ent) ent->Release();
    
    return ProcessId;
}

//
// Read instances of Win32_Process and filter by Name. 
//
DWORD 
GetLsaPidFromWMI(void) {
    IWbemLocator  *loc = NULL;
    IWbemServices *svc = NULL;
    DWORD ProcessId = 0;
    
    do {
        HRESULT hr = CoInitialize(NULL);
        
        if (FAILED(hr)) {
            printf("CoInitialize() failed : %08lX\n", hr);
            break;
        }
        
        hr = CoInitializeSecurity(
                NULL, 
                -1, 
                NULL, 
                NULL, 
                RPC_C_AUTHN_LEVEL_DEFAULT, 
                RPC_C_IMP_LEVEL_IMPERSONATE, 
                NULL, 
                EOAC_NONE, 
                NULL);
        
        if (FAILED(hr)) {
            printf("CoInitializeSecurity() failed : %08lX\n", hr);
            break;
        }
        
        hr = CoCreateInstance(
                CLSID_WbemLocator, 
                0, 
                CLSCTX_INPROC_SERVER, 
                IID_IWbemLocator, 
                (LPVOID*)&loc);

        if (FAILED(hr)) {
            printf("CoInitializeSecurity() failed : %08lX\n", hr);
            break;
        }
        
        hr = loc->ConnectServer(
                    L"root\\cimv2", 
                    NULL, 
                    NULL, 
                    NULL, 
                    0, 
                    NULL, 
                    NULL, 
                    &svc);

        if (FAILED(hr)) {
            printf("IWbemLocator::ConnectServer() failed : %08lX\n", hr);
            break;
        }
        
        ProcessId = GetPidForLsass(svc);
    } while (FALSE);
    
    if (svc) svc->Release();
    if (loc) loc->Release();
    
    CoUninitialize();
    
    return ProcessId;
}

2.4 NtQueryValueKey (LsaPid)

我们可以直接在 HKLM\SYSTEM\CurrentControlSet\Control\Lsa 的注册表值 LsaPid 中读取 PID。

图片[1]-获取 LSASS PID 的十四种方法-零度非安全
直接读 PID
//
// Query registry for LSA process ID.
//
DWORD GetLsaPidFromRegistry(void) {
    UNICODE_STRING                 LsaPath, LsaValue;
    OBJECT_ATTRIBUTES              ObjectAttributes;
    HANDLE                         LsaKey = NULL;
    NTSTATUS                       Status;
    UCHAR                          Buffer[sizeof(KEY_VALUE_PARTIAL_INFORMATION) + sizeof(DWORD)];
    PKEY_VALUE_PARTIAL_INFORMATION PartialInfo = (PKEY_VALUE_PARTIAL_INFORMATION)Buffer;
    ULONG                          Length;
    DWORD                          ProcessId = 0;
    
    do {
        LsaPath = RTL_CONSTANT_STRING(L"\\Registry\\Machine\\SYSTEM\\CurrentControlSet\\Control\\Lsa");

        InitializeObjectAttributes(
            &ObjectAttributes, 
            &LsaPath, 
            OBJ_CASE_INSENSITIVE, 
            NULL, 
            NULL);
        
        Status = NtOpenKey(&LsaKey, KEY_QUERY_VALUE, &ObjectAttributes);
        
        if (!NT_SUCCESS(Status)) {
            SetLastError(RtlNtStatusToDosError(Status));
            printf("NtOpenKey() failed : %ld\n", GetLastError());
            break;
        }
        
        LsaValue = RTL_CONSTANT_STRING(L"LsaPid");
         
        Status = NtQueryValueKey(
                    LsaKey, 
                    &LsaValue, 
                    KeyValuePartialInformation, 
                    Buffer, 
                    sizeof(Buffer), 
                    &Length);
        
        if (!NT_SUCCESS(Status)) {
            SetLastError(RtlNtStatusToDosError(Status));
            printf("NtQueryValueKey() failed : %ld\n", GetLastError());
            break;
        }
        
        PDWORD pData = (PDWORD)&PartialInfo->Data[0];
        ProcessId = pData[0];
    } while(FALSE);
    
    if (LsaKey) NtClose(LsaKey);
    return ProcessId;
}

2.5 QueryServiceStatusEx (SAMSS)

LSASS 托管许多服务:

  • CNG 密钥隔离 (KeyIso)
  • 安全客户管理 (SamSs)
  • 凭据管理器 (VaultSvc)

可以打开这些服务中的任何一个来查询状态,并使用 SC_STATUS_PROCESS_INFO 来获取进程 ID。与 WTS 一样,此信息是通过 RPC 获取的,是获取服务 PID 的好方法。

//
// Query samss for LSA process ID.
//
DWORD GetLsaPidFromService(void) {
    SC_HANDLE              ManagerHandle = NULL, ServiceHandle = NULL;
    SERVICE_STATUS_PROCESS ProcessInfo;
    HANDLE                 Handle = NULL;
    DWORD                  Length, ProcessId = 0;
    BOOL                   Result;

    do {
        ManagerHandle = OpenSCManagerW(
                            NULL,
                            NULL,
                            SC_MANAGER_CONNECT
                        );

        if (!ManagerHandle) {
            printf("OpenSCManager() failed : %ld\n", GetLastError());
            break;
        }

        ServiceHandle = OpenServiceW(
                            ManagerHandle,
                            L"samss",
                            SERVICE_QUERY_STATUS
                        );

        if (!ServiceHandle) {
            printf("OpenService() failed : %ld\n", GetLastError());
            break;
        }

        Result = QueryServiceStatusEx(
                    ServiceHandle,
                    SC_STATUS_PROCESS_INFO,
                    (LPBYTE)&ProcessInfo,
                    sizeof(ProcessInfo),
                    &Length
                );

        if (!Result) {
            printf("QueryServiceStatusEx() failed : %ld\n", GetLastError());
            break;
        }

        ProcessId = ProcessInfo.dwProcessId;
    } while(FALSE);

    if (ServiceHandle) {
        CloseServiceHandle(ServiceHandle);
    }

    if (ManagerHandle) {
        CloseServiceHandle(ManagerHandle);
    }

    return ProcessId;
}

2.6 NtQueryInformationFile (lsass.exe)

如果我们打开 LSASS.EXE 的文件句柄,并使用 FileProcessIdsUsingFileInformation 类将其传递给 NtQueryInformationFile,可以获得打开文件的进程 ID 列表,列表中的第一个应该始终属于 LSASS 进程。通常,LSASS 位于 C:\Windows\System32\ 文件夹中,使用 GetSystemDirectory() 之类的方法获取正确路径更安全。

图片[2]-获取 LSASS PID 的十四种方法-零度非安全
使用 FileProcessIdsUsingFileInformation 类将其传递给 NtQueryInformationFile
//
// Query process path for process ID
//
DWORD GetLsaPidFromPath(void) {
    IO_STATUS_BLOCK   StatusBlock;
    NTSTATUS          Status;
    WCHAR             LsassPath[MAX_PATH + 1];
    DWORD             ProcessId = 0;
    HANDLE            FileHandle = NULL;
    OBJECT_ATTRIBUTES ObjectAttributes;
    UNICODE_STRING    NtPath;
    
    do {
        //
        // Get the Windows directory.
        //
        GetSystemDirectoryW(LsassPath, MAX_PATH);
        PathAppendW(LsassPath, L"lsass.exe");
        
        //
        // Convert DOS path to NT path
        //
        RtlDosPathNameToNtPathName_U(
            LsassPath,
            &NtPath,
            NULL,
            NULL
            );
            
        //
        // Open file for reading.
        //
        InitializeObjectAttributes(
            &ObjectAttributes,
            &NtPath,
            OBJ_CASE_INSENSITIVE,
            0,
            NULL
        );

        Status = NtOpenFile(
                &FileHandle,
                FILE_READ_ATTRIBUTES,
                &ObjectAttributes,
                &StatusBlock,
                FILE_SHARE_READ,
                NULL
            );
        
        if (!NT_SUCCESS(Status)) {
            SetLastError(RtlNtStatusToDosError(Status));
            printf("NtOpenFile() failed : %ld\n", GetLastError());
            break;
        }
        
        //
        // Get list of process IDs with this process path opened.
        //
        std::vector Buffer;
        
        for (DWORD Length=4096;;Length += 4096) {
            Buffer.resize(Length);
            
            Status = NtQueryInformationFile(
                        FileHandle, 
                        &StatusBlock, 
                        Buffer.data(), 
                        Buffer.size(), 
                        FileProcessIdsUsingFileInformation
                        );
            
            if (Status != STATUS_INFO_LENGTH_MISMATCH) break;
        }
        
        if (!NT_SUCCESS(Status)) {
            SetLastError(RtlNtStatusToDosError(Status));
            printf("NtQueryInformationFile() failed : %ld\n", GetLastError());
            break;
        }
        
        auto PidFileInfo = (PFILE_PROCESS_IDS_USING_FILE_INFORMATION)Buffer.data();
        
        if (PidFileInfo->NumberOfProcessIdsInList) {
            ProcessId = DWORD(PidFileInfo->ProcessIdList[0]);
        }   
    } while(FALSE);
    
    if (FileHandle) NtClose(FileHandle);
    return ProcessId;
}

2.7 NtFsControlFile (named pipe)

获取命名管道服务器的 PID 的推荐方法是使用 GetNamedPipeServerProcessId API。在内部将调用 GetNamedPipeAttribute() API,截至 2022 年 8 月,该 API 仍未被 MSDN 记录。调用时,它最终会执行 NtFsControlFile 系统调用。客户端或服务器可以使用它从其对等方获取会话 ID、进程 ID 或计算机名称。

BOOL GetNamedPipeAttribute(
        HANDLE Pipe,
        PIPE_ATTRIBUTE_TYPE AttributeType,
        PSTR AttributeName,
        PVOID AttributeValue,
        PSIZE_T AttributeValueLength);

要获取 PID,请打开命名管道 \Device\NamedPipe\lsass 并使用 NtFsControlFile 发送以 ServerProcessId 作为输入的 FSCTL_PIPE_GET_PIPE_ATTRIBUTE 控制代码,该代码应返回进程 ID。

//
// Get the LSA PID from named pipe.
//
DWORD GetLsaPidFromPipe(void) {
    UNICODE_STRING    LsaName = RTL_CONSTANT_STRING(L"\\Device\\NamedPipe\\lsass");
    HANDLE            LsaHandle;
    IO_STATUS_BLOCK   StatusBlock;
    OBJECT_ATTRIBUTES ObjectAttributes;
    NTSTATUS          Status;
    DWORD             ProcessId = 0;
    
    do {
        //
        // Open named pipe for reading.
        //
        InitializeObjectAttributes(
            &ObjectAttributes,
            &LsaName,
            OBJ_CASE_INSENSITIVE,
            0,
            NULL
        );

        Status = NtOpenFile(
                    &LsaHandle,
                    FILE_READ_ATTRIBUTES,
                    &ObjectAttributes,
                    &StatusBlock,
                    FILE_SHARE_READ,
                    NULL
                );
        
        if (!NT_SUCCESS(Status)) {
            SetLastError(RtlNtStatusToDosError(Status));
            printf("NtOpenFile() failed : %ld\n", GetLastError());
            break;
        }
        
        //
        // Query the server process ID.
        //
        LPSTR Attribute = "ServerProcessId";

        Status = NtFsControlFile(
                     LsaHandle,
                     NULL,
                     NULL,
                     NULL,
                     &StatusBlock,
                     FSCTL_PIPE_GET_PIPE_ATTRIBUTE,
                     Attribute,
                     lstrlenA(Attribute) + 1,
                     &ProcessId,
                     sizeof(DWORD));

        if (!NT_SUCCESS(Status)) {
            SetLastError(RtlNtStatusToDosError(Status));
            printf("NtFsControlFile() failed : %ld\n", GetLastError());
        }
        
    } while(FALSE);
    
    if (LsaHandle) NtClose(LsaHandle);
    
    return ProcessId;
}

2.8 NtQueryOpenSubKeysEx (SAM)

以同样的方式我们可以查询哪个进程打开了一个文件,可以使用 NtQueryOpenSubKeysEx 来获取打开注册表项的 PID 列表。对于 LSASS,它是唯一打开 HKLM\SAM 的进程。不幸的是,此方法需要 SeRestorePrivilege,但如果准备打开 LSASS,则至少需要启用 SYSTEM 或 SeDebugPrivilege。

//
// NtQueryOpenSubKeysEx() : Requires Admin rights to enable restore privilege.
//
DWORD GetLsaPidFromRegName(void) {
    UNICODE_STRING    RegName = RTL_CONSTANT_STRING(L"\\REGISTRY\\MACHINE\\SAM");
    OBJECT_ATTRIBUTES ObjectAttributes;
    NTSTATUS          Status;
    DWORD             ProcessId = 0;
    
    do {
        //
        // The restore privilege is required for this to work.
        //
        BOOLEAN Old;
        Status = RtlAdjustPrivilege(SE_RESTORE_PRIVILEGE, TRUE, FALSE, &Old);

        if (!NT_SUCCESS(Status)) {
            SetLastError(RtlNtStatusToDosError(Status));
            printf("RtlAdjustPrivilege(SE_RESTORE_PRIVILEGE) failed : %ld\n", GetLastError());
            break;
        }
        
        InitializeObjectAttributes(
            &ObjectAttributes,
            &RegName,
            OBJ_CASE_INSENSITIVE,
            NULL,
            NULL
        );
        
        std::vector OutBuffer(1024);
        ULONG OutLength = 1024;
        
        Status = NtQueryOpenSubKeysEx(
                    &ObjectAttributes,
                    OutLength,
                    OutBuffer.data(),
                    &OutLength
                );
        
        if (!NT_SUCCESS(Status)) {
            SetLastError(RtlNtStatusToDosError(Status));
            printf("NtQueryOpenSubKeysEx() failed : %ld\n", GetLastError());
            break;
        }
        
        PKEY_OPEN_SUBKEYS_INFORMATION SubKeyInfo = (PKEY_OPEN_SUBKEYS_INFORMATION)OutBuffer.data();
        ProcessId = HandleToUlong(SubKeyInfo->KeyArray[0].ProcessId);
        
    } while(FALSE);
    
    return ProcessId;
}

2.9 RegQueryValueExW (HKEY_PERFORMANCE_DATA)

有些人可能已经知道 sysinternals 的 pslist 是使用 性能计数器 来获取进程信息,也许现在这在进程枚举中不受欢迎的原因是因为它在获取信息时非常杂乱,首先一个就是它的文档记录很差。网上当然有资源演示如何使用计数器,通常可以追溯到 1990 年代。

最初,这种方法似乎非常隐蔽,但当你意识到获取进程列表所需的操作量时,它显然不是最好的方法。尽管如此,性能数据仍然是 Windows 没有得到足够重视的领域之一。

//
// Read LSASS process ID from performance counters.
//
DWORD GetLsaPidFromPerf(void) {
    DWORD ProcessId = 0;
    std::vector Buffer;
      
    //
    // Read performance data for each process.
    //
    for (DWORD Length=8192;;Length += 8192) {
        Buffer.resize(Length);
        
        DWORD rc = RegQueryValueExW(
                        HKEY_PERFORMANCE_DATA,
                        L"230",
                        NULL,
                        0,
                        Buffer.data(),
                        &Length);
                
        if (rc == ERROR_SUCCESS) break;
    }

    //
    // Read offset for the process ID.
    //
    auto pPerf = (PPERF_DATA_BLOCK)Buffer.data();
    auto pObj = (PPERF_OBJECT_TYPE) ((PBYTE)pPerf + pPerf->HeaderLength);
    auto pCounterDef = (PPERF_COUNTER_DEFINITION) ((PBYTE)pObj + pObj->HeaderLength);
    
    DWORD ProcessIdOffset = 0;
    
    for (auto i=0; iNumCounters; i++) {
        if (pCounterDef->CounterNameTitleIndex == 784) {
            ProcessIdOffset = pCounterDef->CounterOffset;
            break;
        }
        pCounterDef++;
    }
    
    //
    // Read process name and compare with lsass
    //
    auto pInst = (PPERF_INSTANCE_DEFINITION) ((PBYTE)pObj + pObj->DefinitionLength);
    
    for (auto i=0; iNumInstances; i++) {
        auto pName = (PWSTR) ((PBYTE)pInst + pInst->NameOffset);
        auto pCounter = (PPERF_COUNTER_BLOCK) ((PBYTE)pInst + pInst->ByteLength);

        if (*pName && !lstrcmpiW(pName, L"lsass")) {
            ProcessId = *((LPDWORD) ((PBYTE)pCounter + ProcessIdOffset));
            break;
        }
        pInst = (PPERF_INSTANCE_DEFINITION) ((PBYTE)pCounter + pCounter->ByteLength);
    }
    
    RegCloseKey(HKEY_PERFORMANCE_DATA);
    return ProcessId;
}

2.10 NtDeviceIoControlFile (TCP Table)

从 Vista 开始,可以从网络存储接口服务 (NSI) 获取网络连接的进程 ID,LSASS 通常至少有一个端口侦听传入连接,如果我们知道该端口,可以使用一些代码读取 PID。以下例子将 NtDeviceIoControlFile 与未记录的 IOCTL 代码和未记录的结构一起使用。遗留系统的另一种方法是打开套接字句柄并发送 IOCTL_TDI_QUERY_INFORMATION。

typedef struct _NSI_CONNECTION_INFO {
    PVOID               Buffer;
    SIZE_T              Size;
} NSI_CONNECTION_INFO, *PNSI_CONNECTION_INFO;

typedef struct _NSI_CONNECTION_ENTRIES {
    NSI_CONNECTION_INFO Address;
    NSI_CONNECTION_INFO Reserved;
    NSI_CONNECTION_INFO State;
    NSI_CONNECTION_INFO Process;
} NSI_CONNECTION_ENTRIES, *PNSI_CONNECTION_ENTRIES;

typedef enum _NPI_MODULEID_TYPE {
    MIT_GUID = 1,
    MIT_IF_LUID,
} NPI_MODULEID_TYPE;

typedef struct _NPI_MODULEID {
    USHORT            Length;
    NPI_MODULEID_TYPE Type;
    union {
        GUID          Guid;
        LUID          IfLuid;
    };
} NPI_MODULEID, *PNPI_MODULEID;

NPI_MODULEID NPI_MS_TCP_MODULEID = {
	sizeof(NPI_MODULEID),
	MIT_GUID,
	{0xEB004A03, 0x9B1A, 0x11D4, {0x91, 0x23, 0x00, 0x50, 0x04, 0x77, 0x59, 0xBC}}
};

// the following structures were reverse engineered and won't be correct.
typedef struct _NSI_CONNECTION_TABLE {
    DWORD                  Unknown1[4];
    PNPI_MODULEID          ModuleId;
    DWORD64                TypeId;
    ULONG64                Flags;
    NSI_CONNECTION_ENTRIES Entries;
    DWORD                  NumberOfEntries;
} NSI_CONNECTION_TABLE, *PNSI_CONNECTION_TABLE;

typedef union _NSI_IP_ADDR_U {
    sockaddr_in  v4;
    sockaddr_in6 v6;
} NSI_IP_ADDR_U, *PNSI_IP_ADDR_U;

typedef struct _NSI_CONNECTION_ADDRESS {
    NSI_IP_ADDR_U Local;
    NSI_IP_ADDR_U Remote;
} NSI_CONNECTION_ADDRESS, *PNSI_CONNECTION_ADDRESS;

typedef struct _NSI_CONNECTION_STATE {
    PULONG        ulState;
    ULONG         ulTimestamp;
} NSI_CONNECTION_STATE, *PNSI_CONNECTION_STATE;

typedef struct _NSI_CONNECTION_PROCESS {
    DWORD         dwOwningPidUdp;
    BOOL          bFlag;
    DWORD         liCreateTimestampUdp;
    DWORD         dwOwningPidTcp;
    LARGE_INTEGER liCreateTimestamp;
    ULONGLONG     OwningModuleInfo;
} NSI_CONNECTION_PROCESS, *PNSI_CONNECTION_PROCESS;

#define FSCTL_TCP_BASE     FILE_DEVICE_NETWORK

#define _TCP_CTL_CODE(Function, Method, Access) \
    CTL_CODE(FSCTL_TCP_BASE, Function, Method, Access)
    
#define NSI_IOCTL_GET_INFORMATION _TCP_CTL_CODE(0x006, METHOD_NEITHER, FILE_ANY_ACCESS)

//
// Read the LSA PID from TCP table.
//
DWORD
GetLsaPidFromTcpTable(void) {
    DWORD                    ProcessId = 0;
    HANDLE                   NsiHandle = NULL;
    NTSTATUS                 Status;
    IO_STATUS_BLOCK          IoStatusBlock;
    PNSI_CONNECTION_ADDRESS  Address = NULL;
    PNSI_CONNECTION_STATE    State = NULL;
    PNSI_CONNECTION_PROCESS  Process = NULL;
    
    do {
        //
        // Open handle to Network Store Interface Service. (nsiproxy.sys)
        //
        NsiHandle = CreateFileW(
                        L"\\\\.\\Nsi", 
                        0, 
                        FILE_SHARE_READ | FILE_SHARE_WRITE, 
                        NULL, 
                        OPEN_EXISTING, 
                        0, 
                        NULL);
                        
        if (NsiHandle == INVALID_HANDLE_VALUE) break;
        
        //
        // Tell service to return information for TCP connections.
        // The first call obtains the number of entries available.
        //
        NSI_CONNECTION_TABLE TcpTable={0};
        
        TcpTable.ModuleId = &NPI_MS_TCP_MODULEID;
        TcpTable.TypeId = 3; // TCP connections.
        TcpTable.Flags = 1 | 0x100000000;
        
        Status = NtDeviceIoControlFile(
                    NsiHandle,
                    NULL,    // no event object. making a synchronous request.
                    NULL,
                    NULL,
                    &IoStatusBlock,
                    NSI_IOCTL_GET_INFORMATION,  // ioctl code to return all information
                    &TcpTable,          // in
                    sizeof(TcpTable),
                    &TcpTable,          // out
                    sizeof(TcpTable)
                    );

        if (!NT_SUCCESS(Status)) {
            SetLastError(RtlNtStatusToDosError(Status));
            printf("NtDeviceIoControlFile() failed : %ld\n", GetLastError());
            break;
        }
        
        //
        // Allocate memory for entries.
        //
        Address = (PNSI_CONNECTION_ADDRESS)calloc(TcpTable.NumberOfEntries + 2, sizeof(NSI_CONNECTION_ADDRESS));
        State = (PNSI_CONNECTION_STATE)calloc(TcpTable.NumberOfEntries + 2, sizeof(NSI_CONNECTION_STATE));
        Process = (PNSI_CONNECTION_PROCESS)calloc(TcpTable.NumberOfEntries + 2, sizeof(NSI_CONNECTION_PROCESS));
        
        //
        // Assign buffers and the size of each structure.
        // Then try again.
        //
        TcpTable.Entries.Address.Buffer = Address;
        TcpTable.Entries.Address.Size = sizeof(NSI_CONNECTION_ADDRESS);
        
        TcpTable.Entries.State.Buffer = State;
        TcpTable.Entries.State.Size = sizeof(NSI_CONNECTION_STATE);
        
        TcpTable.Entries.Process.Buffer = Process;
        TcpTable.Entries.Process.Size = sizeof(NSI_CONNECTION_PROCESS);
        
        Status = NtDeviceIoControlFile(
                    NsiHandle,
                    NULL,    // no event object. making a synchronous request.
                    NULL,
                    NULL,
                    &IoStatusBlock,
                    NSI_IOCTL_GET_INFORMATION,  // ioctl code to return all information
                    &TcpTable,          // in
                    sizeof(TcpTable),
                    &TcpTable,          // out
                    sizeof(TcpTable)
                    );

        if (!NT_SUCCESS(Status)) {
            SetLastError(RtlNtStatusToDosError(Status));
            printf("NtDeviceIoControlFile() failed : %ld\n", GetLastError());
            break;
        }
        
        //
        // Loop through each entry to find LSASS ports.
        // When found, return the Process ID.
        //
        for (DWORD i=0; i= 49664 && lport <= 49667 || lport == 49155) {
                ProcessId = pid;
                break;
            }
        }
        
    } while (FALSE);
    
    if (Address) free(Address);
    if (State) free(State);
    if (Process) free(Process);
    if (NsiHandle) NtClose(NsiHandle);
    
    return ProcessId;
}

2.11 安全事件日志

事件 ID 4608 又名 “Windows 正在启动” 包含 LSASS 的进程 ID,它可以使用 Windows 事件日志 API 提取。当然还有其他事件需要考虑:例如登录事件。4608 刚好从 Vista 到 Windows 11/Server 2022 是一致的。

//
// Query the PID for LSASS from the Security event log.
//
DWORD GetLsaPidFromEventLogs(void) {
    EVT_HANDLE hResults = NULL, hContext = NULL, hEvent = NULL;
    DWORD      dwProcessId = 0;
    
    do {
        //
        // Get all records from the Security log with event ID 4608
        //
        hResults = EvtQuery(
                    NULL, 
                    L"Security", 
                    L"*/*[EventID=4608]", 
                    EvtQueryTolerateQueryErrors
                    );
                    
        if (!hResults) {
            printf("EvtQuery(Security, EventID=4608) failed : %ld\n", GetLastError());
            break;
        }
        
        //
        // Move position of results to the last entry. (the latest available)
        //
        BOOL Result;
        Result = EvtSeek(hResults, 0, NULL, 0, EvtSeekRelativeToLast);
        
        if (!Result) {
            printf("EvtSeek() failed : %ld\n", GetLastError());
            break;
        }

        //
        // Read last event.
        //
        DWORD dwReturned = 0;
    
        Result = EvtNext(
                    hResults, 
                    1, 
                    &hEvent, 
                    INFINITE, 0, 
                    &dwReturned);
       
        if (!Result || dwReturned != 1) {
            printf("EvtNext() failed : %ld\n", GetLastError());
            break;
        }
        
        //
        // Create a render context so that we only extract the PID
        //
        LPCWSTR ppValues[] = {L"Event/System/Execution/@ProcessID"};
        
        hContext = EvtCreateRenderContext(
                    ARRAYSIZE(ppValues), 
                    ppValues, 
                    EvtRenderContextValues);
        
        if (!hContext) { 
            printf("EvtCreateRenderContext failed : %ld\n", GetLastError());
            break;
        }
        
        //
        // Extract the PID.
        //
        EVT_VARIANT pProcessId={0};
        
        EvtRender(
            hContext, 
            hEvent, 
            EvtRenderEventValues, 
            sizeof(EVT_VARIANT), 
            &pProcessId, 
            &dwReturned, 
            NULL
            );
        
        //
        // Save it.
        //
        dwProcessId = pProcessId.UInt32Val;
    } while (FALSE);
    
    if (hEvent) EvtClose(hEvent);
    if (hContext) EvtClose(hContext);
    if (hResults) EvtClose(hResults);
    
    return dwProcessId;
}

2.12 Brute Forcing PIDs

PID 值递增 4,因此可以轻松循环遍历潜在的 PID 并查询镜像。

BOOL GetProcessNameById(DWORD ProcessId, WCHAR ImageName[MAX_PATH + 1]) {
    WCHAR                          ImageBuffer[512];
    SYSTEM_PROCESS_ID_INFORMATION  ProcessInformation;
    NTSTATUS                       Status;
    DWORD                          Length;
    
    //
    // Query the system for image name of process ID.
    //
    ProcessInformation.ProcessId               = LongToHandle(ProcessId);
    ProcessInformation.ImageName.Buffer        = ImageBuffer;
    ProcessInformation.ImageName.Length        = 0;
    ProcessInformation.ImageName.MaximumLength = sizeof(ImageBuffer);

    Status = NtQuerySystemInformation(
              SystemProcessIdInformation, 
              &ProcessInformation,
              sizeof(ProcessInformation), 
              NULL
              );

    if (!NT_SUCCESS(Status)) {
        SetLastError(RtlNtStatusToDosError(Status));
        //printf("NtQuerySystemInformation(SystemProcessIdInformation) failed : %ld\n", GetLastError());
        return FALSE;
    }
    
    //
    // Strip path and copy name to buffer.
    // 
    PathStripPathW(ImageBuffer);
    Length = ProcessInformation.ImageName.Length;
    
    if (Length > (MAX_PATH * sizeof(WCHAR))) { 
        Length = MAX_PATH * sizeof(WCHAR);
    }
    
    memcpy(
      ImageName, 
      ImageBuffer, 
      Length
      );
      
    return TRUE;
}

//
// Run a loop. Increment PID and resolve an image name. Then compare with lsass.exe
//
DWORD
GetLsaPidFromBruteForce(void) {
    DWORD ProcessId = 0;
    
    WCHAR ImageName[MAX_PATH + 1]={0};
    
    //
    // 0xFFFFFFFC is the maximum PID but probably too much for this.
    //
    for (DWORD pid=8; pid<0xFFFFFFFC; pid += 4) {
        if (GetProcessNameById(pid, ImageName)) {
            if (!lstrcmpiW(ImageName, L"lsass.exe")) {
                ProcessId = pid;
                break;
            }
        }
    }
    return ProcessId;
}

2.13 Section Object

LSASS 创建一个 Section Object 来存储有关其性能的信息,命名为 “\LsaPerformance”,其中数据是 PID。性能监视器用户具有读取权限,而系统和管理员具有完全访问权限。数据结构未记录,PID 可能出现在不同的偏移量,因此这需要努力提高可靠性。

//
// Only tested on Windows 10. The offset of PID differs across versions.
//
DWORD GetLsaPidFromSection(void) {
    UNICODE_STRING    SectionName = RTL_CONSTANT_STRING(L"\\LsaPerformance");
    OBJECT_ATTRIBUTES ObjectAttributes;
    NTSTATUS          Status;
    DWORD             ProcessId = 0;
    HANDLE            SectionHandle = NULL;
    PVOID             ViewBase = NULL;
    
    do {
        InitializeObjectAttributes(
            &ObjectAttributes,
            &SectionName,
            OBJ_CASE_INSENSITIVE,
            NULL,
            NULL
        );
        
        Status = NtOpenSection(
                    &SectionHandle, 
                    SECTION_MAP_READ, 
                    &ObjectAttributes
                    );
              
        if (!NT_SUCCESS(Status)) {
            SetLastError(RtlNtStatusToDosError(Status));
            printf("NtOpenSection() failed : %ld\n", GetLastError());
            break;
        }

        LARGE_INTEGER SectionOffset={0};
        SIZE_T ViewSize = 0;
        
        Status = NtMapViewOfSection(
                  SectionHandle,
                  NtCurrentProcess(),
                  &ViewBase,
                  0,              // ZeroBits
                  0,              // CommitSize
                  &SectionOffset,
                  &ViewSize,
                  ViewShare,
                  0,
                  PAGE_READONLY
                  );

        if (!NT_SUCCESS(Status)) {
            SetLastError(RtlNtStatusToDosError(Status));
            printf("NtMapViewOfSection() failed : %ld\n", GetLastError());
            break;
        }
        
        PDWORD data = (PDWORD)ViewBase;
        
        ProcessId = data[32]; // Windows 10. it appears at different offsets for different builds.
    } while(FALSE);
    
    if (ViewBase) NtUnmapViewOfSection(NtCurrentProcess(), ViewBase);
    if (SectionHandle) NtClose(SectionHandle);
    
    return ProcessId;
}

2.14 NtAlpcQueryInformation

自 Windows 10 版本 1909 “19H2” 以来可用的是查询 ALPC 服务的会话和进程 ID 的能力。下面这个例子使用 \RPC Control\samss lpc,它应该在 LSASS 进程中运行。这并不总是可靠的。有时 NtAlpcQueryInformation() 会因 STATUS_INVALID_PARAMETER 而失败。

//
// Query process ID from session information. Only works on 19H2 builds.
//
DWORD GetLsaPidFromAlpc(void) {
    HANDLE            AlpcPort = NULL;
    UNICODE_STRING    AlpcName;
    DWORD             ProcessId = 0;
    NTSTATUS          Status;
    OBJECT_ATTRIBUTES ObjectAttributes;

    do {
        //
        // Connect to RPC service.
        //
        AlpcName = RTL_CONSTANT_STRING(L"\\RPC Control\\samss lpc");
        
        Status = NtAlpcConnectPort(
                    &AlpcPort,
                    &AlpcName,
                    NULL,
                    NULL,
                    0,
                    NULL,
                    NULL,
                    NULL,
                    NULL,
                    NULL,
                    NULL);
        
        if (!NT_SUCCESS(Status)) {
            SetLastError(RtlNtStatusToDosError(Status));
            printf("NtAlpcConnectPort() failed : %ld\n", GetLastError());
            break;
        }
        
        //
        // Query session information.
        //
        ALPC_SERVER_SESSION_INFORMATION SessionInfo={0};

        Status = NtAlpcQueryInformation(
                    AlpcPort,
                    AlpcServerSessionInformation,
                    &SessionInfo,
                    sizeof(SessionInfo),
                    NULL);
                    
        if (!NT_SUCCESS(Status)) {
            SetLastError(RtlNtStatusToDosError(Status));
            printf("NtAlpcQueryInformation() failed : %ld\n", GetLastError());
            break;
        }
        
        ProcessId = SessionInfo.ProcessId;
    } while(FALSE);
    
    if (AlpcPort) NtClose(AlpcPort);
    
    return ProcessId;
}

三、总结

有许多方法可以获得进程的 PID,对于 LSASS,从命名管道读取 ServerProcessId 属性似乎是最优雅的方法。它不需要任何内存分配,至少可以在 Vista 到 Windows 11 上运行。此处未讨论的另一个问题涉及枚举句柄并计算一个进程打开了多少个安全令牌。得分最高的应该是 LSASS,当然也有可能用这种方法识别出错误的进程。

四、参考

© 版权声明
THE END
喜欢就支持一下吧
点赞1 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容