Windows 下通过 PEB 读取进程的环境变量
这篇文章最后更新的时间在六个月之前,文章所叙述的内容可能已经失效,请谨慎参考!
Windows 下没有 api 能直接读取进程的环境变量。 wmi 和 powershell 也没有方式能读取进程的环境变量。 在 powershell 5.1 里确实可以通过象这样的命令读取环境变量,但这并不是进程的环境变量。
([System.Diagnostics.Process]::GetProcessesByName("php-cgi") | Select-Object -First 1 ).StartInfo.EnvironmentVariables
([System.Diagnostics.Process]::GetProcessesByName("php-cgi") | Select-Object -First 1 ).StartInfo.Environment
进程的 PEB
Windows 下只能通过 PEB 来读取环境变量。 PEB 是一个臃肿的结构体,且 msdn 里没有这个结构体的详细信息。 PEB 的具体结构根据操作系统的版本不同而不同,也和进程的位数有关。 PEB 里除了环境变量,还是有执行目录,可执行文件路径,命令行等信息。 PEB 是 Process Environment Block 进程 环境 块
笔者在网上搜索了很久才在这个站点里找到 PEB 详细的结构 http://terminus.rewolf.pl/terminus
通过 PEB 读取进程的环境变量的具体流程
- 获取进程id- 获取进程id的方式有非常多,最简单的方式就是通过任务管理器
 
- 获取进程的句柄(Handle)- 可以通过这个 OpenProcessapi 来获取
 
- 可以通过这个 
- 根据 Handle 获取 PBI (PROCESS_BASIC_INFORMATION)- 可以通过这个 NtQueryInformationProcessapi 来获取
 
- 可以通过这个 
- 从 PBI 中获取 PEB- 在结构体 PROCESS_BASIC_INFORMATION 中的 PebBaseAddress
 
- 从 PEB 中获取 ProcessParameters- 通过这个 ReadProcessMemoryapi 来获取
 
- 通过这个 
- 从 ProcessParameters 中获取 environment- 通过这个 ReadProcessMemoryapi 来获取
- 在 win10 下,可以通过 ProcessParameters 中的 EnvironmentSize 获取环境变量具体的长度
- win10 以下的版本只能通过逐个读取字符和空字符判断环境变量的长度
- 读取的结果是一个连续的字符串,使用 \0字符来分隔环境变量,使用连续的两个\0字符表示结束
 
- 通过这个 
相关的 结构体 和 api
- PEB
- PROCESS_BASIC_INFORMATION
- PROCESS_BASIC_INFORMATION_WOW64
- RTL_USER_PROCESS_PARAMETERS
- OpenProcess
- IsWow64Process
- NtQueryInformationProcess
- NtWow64QueryInformationProcess64
- ReadProcessMemory
这些 api 的文档里大多有这样的一句声明。
“在未来的Windows版本中可能会修改”
笔者其实不理解象PEB这么重要的结构微软为什么不公开详细的信息
读取进程环境变量的脚本
这是一段使用 powershell 通过 win32api 读取进程 PEB 中的环境变量的脚本。
这段脚本没有做太多的兼容性处理,估计只能运行在 64 位 win10 上,且只能用 64 位的 powershell 查询 64 位的进程。
这段脚本很容易被安全软件判断为恶意脚本。
PEB 的结构每个版本的 Windows 都有一点差异, 32位和64位也有明显的差异,还有考虑 WoW64 的环境,还要考虑各个版本的 powershell 的语法差异。
要写一段兼容性好的脚本是一件麻烦的事,各种环境下都测试一次,笔者没有这样的条件 (笔者已经理解了大概的原理,没有动力完善了)
笔者在网上找到的脚本大多不能直接使用,就是兼容性的问题。
note
- 要先确认当前的 Windows 版本,然后再查找对应 PEB 的结构
- 进程的数据要用 ReadProcessMemory 读取,即使是字符串
- 可以用 IsWow64Process 判断是不是 WoW64 的环境。
- WoW64 的环境的 PBI 要用这个结构体 PROCESS_BASIC_INFORMATION_WOW64
function Get-ProcessEnvironmentVariables {
    param(
        [int]$ProcessId
    )
    BEGIN
    {
        # Native API Definitions
        Add-Type -TypeDefinition @"
            using System;
            using System.Diagnostics;
            using System.Runtime.InteropServices;
            using System.Security.Principal;
            [StructLayout(LayoutKind.Sequential)]
            public struct UNICODE_STRING
            {
                public UInt16 Length;
                public UInt16 MaximumLength;
                public IntPtr Buffer;
            }
            [StructLayout(LayoutKind.Sequential)]
            public struct PROCESS_BASIC_INFORMATION
            {
                public IntPtr ExitStatus;
                public IntPtr PebBaseAddress;
                public IntPtr AffinityMask;
                public IntPtr BasePriority;
                public UIntPtr UniqueProcessId;
                public IntPtr InheritedFromUniqueProcessId;
            }
            public static class ProcessEnvironmentHelper
            {
                [DllImport("kernel32.dll")]
                public static extern Boolean ReadProcessMemory( 
                    IntPtr hProcess, 
                    IntPtr lpBaseAddress,
                    IntPtr lpBuffer,
                    UInt32 dwSize, 
                    ref UInt32 lpNumberOfBytesRead
                );
                [DllImport("ntdll.dll")]
                public static extern UInt32 NtQueryInformationProcess(
                    IntPtr processHandle, 
                    UInt32 processInformationClass,
                    ref PROCESS_BASIC_INFORMATION processInformation,
                    UInt32 processInformationLength,
                    ref UInt32 returnLength
                );
            }
"@
        function Get-PBI {
            param(
                [IntPtr]$hProcess
            )
            $PROCESS_BASIC_INFORMATION = New-Object PROCESS_BASIC_INFORMATION
            $PROCESS_BASIC_INFORMATION_Size = [System.Runtime.InteropServices.Marshal]::SizeOf($PROCESS_BASIC_INFORMATION)
            [UInt32]$RetLen = 0
            $CallResult = [ProcessEnvironmentHelper]::NtQueryInformationProcess(
                $hProcess,
                0,
                [ref]$PROCESS_BASIC_INFORMATION,
                $PROCESS_BASIC_INFORMATION_Size,
                [ref]$RetLen
            )
            if ($CallResult -ne 0) {
                $false
            } else {
                $PROCESS_BASIC_INFORMATION
            }
        }
    }
    PROCESS
    {
        try {
            $hProcess = [System.Diagnostics.Process]::GetProcessById($ProcessId).SafeHandle.DangerousGetHandle()
            # get process info
            # Get-CimInstance -ClassName Win32_Process -Filter "ProcessId = $ProcessId" | Select-Object -First 1 | Format-List *
        } catch {
            Write-Host "Failed to get process handle"
            Write-Host $_
            return 1;
        }
        Write-Host "[+] ProcessId " $ProcessId
        Write-Host "[+] Handle " $hProcess
        # Get process basic information
        $ProcessBasicInformation = Get-PBI -hProcess $hProcess
        Write-Host "[+] PBI " $ProcessBasicInformation
        # Get PEBAddress
        $PEBAddress = $ProcessBasicInformation.PebBaseAddress.ToInt64()
        Write-Host "[+] PEB " $PEBAddress
        # Get ProcessParameters
        $AddressOffset = $PEBAddress + 0x20 # 64位
        $ReadSize = 8
        $BytesRead = 0
        [IntPtr]$lpBuffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($ReadSize)
        $CallResult = [ProcessEnvironmentHelper]::ReadProcessMemory(
            $hProcess,
            $AddressOffset,
            $lpBuffer,
            $ReadSize,
            [ref]$BytesRead
        )
        $ProcessParametersAddress = [System.Runtime.InteropServices.Marshal]::ReadInt64($lpBuffer.ToInt64())
        # Get EnvironmentSize
        $EnvironmentSizeAddress = $ProcessParametersAddress + 0x03F0
        $AddressOffset = $EnvironmentSizeAddress
        $ReadSize = 8
        $BytesRead = 0
        [IntPtr]$lpBuffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($ReadSize)
        $CallResult = [ProcessEnvironmentHelper]::ReadProcessMemory(
            $hProcess,
            $AddressOffset,
            $lpBuffer,
            $ReadSize,
            [ref]$BytesRead
        )
        $EnvironmentSize = [System.Runtime.InteropServices.Marshal]::ReadInt64($lpBuffer.ToInt64())
        Write-Host "[+] EnvironmentSize " $EnvironmentSize
        # Get Environment
        $EnvironmentAddress = $ProcessParametersAddress + 0x0080
        $AddressOffset = $EnvironmentAddress
        $ReadSize = 8
        $BytesRead = 0
        [IntPtr]$lpBuffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($ReadSize)
        $CallResult = [ProcessEnvironmentHelper]::ReadProcessMemory(
            $hProcess,
            $AddressOffset,
            $lpBuffer,
            $ReadSize,
            [ref]$BytesRead
        )
        $AddressOffset = [System.Runtime.InteropServices.Marshal]::ReadInt64($lpBuffer.ToInt64())
        $ReadSize = $EnvironmentSize
        $BytesRead = 0
        [IntPtr]$lpBuffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($ReadSize)
        $CallResult = [ProcessEnvironmentHelper]::ReadProcessMemory(
            $hProcess,
            $AddressOffset,
            $lpBuffer,
            $ReadSize,
            [ref]$BytesRead
        )
        $ProcessEnvironmentVariables = [System.Runtime.InteropServices.Marshal]::PtrToStringAnsi($lpBuffer, $EnvironmentSize)
        # Convert string to hashTable
        $ProcessEnvironmentVariables = [System.Convert]::ToString($ProcessEnvironmentVariables)
        $ProcessEnvironmentVariables = $ProcessEnvironmentVariables.Replace("`0`0", "`n")
        $ProcessEnvironmentVariables = $ProcessEnvironmentVariables.Replace("`0", "")
        if ($ProcessEnvironmentVariables[0] -eq '=') {
            $ProcessEnvironmentVariables = $ProcessEnvironmentVariables.Split("`n")
            $ProcessEnvironmentVariables = [System.Collections.ArrayList]::new($ProcessEnvironmentVariables)
            $ProcessEnvironmentVariables.RemoveAt(0)
            $ProcessEnvironmentVariables = $ProcessEnvironmentVariables.ToArray()
            $ProcessEnvironmentVariables = $ProcessEnvironmentVariables -join "`n"
        }
        $ProcessEnvironmentVariables = $ProcessEnvironmentVariables.Replace("\", "\\")
        $ProcessEnvironmentVariables = ConvertFrom-StringData -StringData $ProcessEnvironmentVariables
        return $ProcessEnvironmentVariables
    }
}
# $Id = ([System.Diagnostics.Process]::GetProcessesByName("php-cgi") | Select-Object -First 1 ).Id
# Get-ProcessEnvironmentVariables -ProcessId $Id
参考
- https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntqueryinformationprocess
- https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-readprocessmemory
- https://docs.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb
- https://docs.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-rtl_user_process_parameters
- http://terminus.rewolf.pl/terminus/structures/ntdll/_PEB_x64.html
- http://terminus.rewolf.pl/terminus/structures/ntdll/_RTL_USER_PROCESS_PARAMETERS_x64.html
- https://www.powershellgallery.com/packages/PowerSploit/1.0.0.0/Content/ReverseEngineering%5CGet-PEB.ps1
- https://stackoverflow.com/questions/38660262/how-to-get-other-processs-environment-variable-using-c-sharp
- https://gist.github.com/xenoscr/99370ecffb07f629ae74e7808cb91450
- http://blog.rewolf.pl/blog/?cat=19