ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

用PowerShell和注册表精准检测Windows已安装浏览器

用PowerShell和注册表精准检测Windows已安装浏览器 搞运维的人应该都有过这种经历准备给一批电脑部署企业级软件、做安全巡检或者排查某个页面兼容性问题时领导随口问一句“这台机器上都装了哪些浏览器”。别小看这个问题浏览器是所有Web应用运行的地基它的版本、内核、默认设置都会直接影响后面所有脚本和配置的下发。我最初的做法是跑到每台电脑上挨个翻开始菜单后来发现根本行不通——Windows系统里对浏览器的记录分散在注册表好几个地方稍有不慎就会漏掉Edge这种系统自带的或者把绿色版、便携版也当成了“正常安装”。这篇文章把我整理的一套检测方案完整分享出来专门针对那些通过官方安装程序在系统里留下明确痕迹的浏览器。整体思路是读懂Windows的注册表信息用PowerShell脚本自动枚举覆盖Chrome、Edge、Firefox、360系、QQ浏览器、搜狗等常见品牌同时把误报率压到最低。内容适合运维工程师、技术支持人员、IT资产管理同事参考也适合任何需要对自己电脑做一次浏览器“人口普查”的普通用户。1. 先想清楚什么样的浏览器才算“正常安装”很多人在写这个枚举功能之前没把“正常安装”这四个字定义清楚导致后面逻辑写得模棱两可。在我这里正常安装的含义是浏览器通过官方安装程序或应用商店完成部署在系统里写入了注册表信息、生成了可执行文件入口并且可以被“设置——应用”列表识别。换句话说它属于Windows认可的已安装软件而不是一个随手拷到硬盘上的文件夹。1.1 浏览器安装后Windows会在哪里留下痕迹一个浏览器安装到Windows上至少会在四个地方留下痕迹。第一处是卸载信息注册表路径在HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall一些按用户安装的程序会写到HKEY_CURRENT_USER的同名路径下。这个区域是“已安装应用列表”的数据来源里面包含DisplayName显示名称、DisplayVersion版本、InstallLocation安装位置、DisplayIcon图标路径等关键信息。第二处是StartMenuInternet注册表项位于HKLM\SOFTWARE\Clients\StartMenuInternet和HKCU\SOFTWARE\Clients\StartMenuInternet。这个位置专门记录“可作为互联网浏览器的客户端”Windows默认应用设置面板里的浏览器候选列表就是从这读的。第三处是App Paths路径它记录可执行文件的完整位置用于支持按WinR键直接输入程序名启动。第四处是文件和协议关联比如默认http/https协议的处理器。只有理解了这几处痕迹才能在检测时做到既不遗漏也不误报。1.2 绿色版、便携版为什么不统计有些浏览器以绿色版、便携版形态存在它们没有安装程序只是解压到一个目录里运行exe就能用。这类浏览器不会在卸载注册表里出现StartMenuInternet通常也没有它的条目。它们虽然能正常上网但严格来说并不属于“系统已安装的软件”。我见过一个失败的检测案例某位同事用“扫描磁盘上所有exe文件名字匹配浏览器特征”的方式做统计结果把U盘上解压的便携版Chrome、某个软件内置的Chromium内核组件全部捞了出来最后报表里出现十几个“浏览器”把资产管理同事吓得不轻。所以这个项目的核心原则是——只看安装痕迹不凭exe名称猜身份。凡是来自Uninstall、StartMenuInternet、App Paths、MSIX包这四类可信来源的浏览器才算“正常安装”其他的一律不认。1.3 一套方案怎么覆盖本地和远程场景设计这个枚举工具的时候不能只满足于“在我自己机器上能跑”。实际工作里往往要给几十台甚至几百台电脑做统计所以方案从最开始就考虑了三种形态本地单机执行、通过PowerShell远程会话批量采集、导出结构化数据给其他系统消费。脚本逻辑上要做到不依赖GUI、不需要人工交互、结果可以重定向到文件这样后续做自动化巡检才有基础。接下来进入正题看这几个检测来源怎么选、怎么用。2. 四类检测来源逐一拆解我的取舍思路检测浏览器不只有一个方法但每个方法都有自己的盲区。这一节我把可用的信息来源拆开讲清楚再给出我在最终脚本里的取舍逻辑。2.1 卸载信息注册表信息全但噪声大卸载注册表是大多数人第一时间能想到的来源。它在Windows里的地位相当于“软件花名册”几乎所有通过安装包MSI或Setup程序安装的软件都会在这里登记。对浏览器来说前往这个路径拿DisplayName、DisplayVersion、InstallLocation基本上能拿到80%想要的信息。但这个来源有个典型问题里面混着大量非浏览器软件。如果只靠关键词匹配DisplayName很容易把“搜狗输入法”这种名字里带搜狗、实际是输入法的软件捞出来。另外很多软件内部嵌了Chromium内核比如某些桌面客户端、Teams、Electron应用它们的DisplayName里可能带着“Chromium”字样但绝对不应该被统计成浏览器。所以在实际脚本里我会优先匹配DisplayIcon和UninstallString里出现的浏览器可执行文件名再配合DisplayName的关键词做二次筛选并且始终要过滤掉那些明显不是浏览器的软件名字。还有一处需要注意64位Windows系统上32位软件的卸载信息不会出现在默认的Uninstall路径而是被重定向到HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall。如果脚本只查了一个路径就会漏掉一批老版本的浏览器安装记录。2.2 StartMenuInternet最接近“浏览器身份”的注册位置HKLM\SOFTWARE\Clients\StartMenuInternet是我个人非常偏爱的检测来源因为能出现在这个键下的基本都是真正的浏览器。这个键本来是给Windows“选择默认浏览器”功能用的里面的每一项对应一个可选的浏览器客户端子键名通常是“Google Chrome”“Microsoft Edge”“FIREFOX.EXE”“360Chrome”之类。通过StartMenuInternet拿到的信息有几个好处能直接判断这个浏览器对外暴露的身份它的shell\open\command默认值会给出启动exe的真实路径而且它天然排除了那些嵌在软件里的Chromium内核组件。缺点是它不一定提供版本号而且个别浏览器如果没有写默认协议关联可能不会出现在这里。所以它适合做“浏览器身份判断”不适合单独作为版本信息来源。2.3 App Paths和安装目录兜底不能少App Paths注册表路径是HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths它的作用是把一些exe的完整路径注册到系统里这样在“运行”窗口输入chrome.exe就能直接启动。这个路径下会有很多软件的exe记录对于浏览器检测它可以作为卸载注册表和StartMenuInternet之外的兜底方案。另外还可以直接检查几个主流浏览器的默认安装目录比如C:\Program Files\Google\Chrome\Application\chrome.exe、C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe、C:\Program Files\Mozilla Firefox\firefox.exe。磁盘路径检查虽然不够优雅但在注册表信息损坏或者权限受限时它是“最后一根救命稻草”。我一般会把路径检查的结果标记为Low confidence不至于进入最终报表但会在日志里提示人工复核。2.4 快捷方式和协议关联适合手工复核开始菜单和桌面的快捷方式也可以作为辅助判断依据。它们的优点是展示了用户日常可见的入口缺点是信息来源不规范、含有大量重复项而且某些快捷方式的Target指向的是更新程序而不是主程序。协议关联则是检查HKCU\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice里的ProgId进而找到默认浏览器对应的exe路径它用来判断“默认浏览器是哪一个”非常合适但无法枚举出所有浏览器。所以这两类我通常不用在主检测逻辑里而是在输出报表时作为补充字段帮助使用者分清哪些是默认浏览器、哪些只是候选。2.5 各方案横向对比检测来源能拿到什么优势劣势我的用途Uninstall注册表名称、版本、安装路径、发布者信息最全、同时是系统应用列表依据噪声大混入非浏览器软件主力数据源StartMenuInternet浏览器身份、启动命令、exe路径高度集中在浏览器误报少缺版本号个别浏览器可能缺失主力身份源App Paths / 安装目录exe完整路径可兜底路径直接歧义大容易把组件当浏览器辅助兜底快捷方式 / 协议关联常用入口、默认浏览器贴近用户视角重复多、不代表安装软件报表展示、人工复核3. 落地脚本一套PowerShell检测全部浏览器思路理清之后就是动手写脚本。我选PowerShell而不是Python或C#原因很简单所有Windows 10/11专业版和工作站默认自带PowerShell不需要安装任何运行时适合在没有预装软件的生产环境直接执行。3.1 脚本需求与整体流程在编码之前我把需求整理成四个约束条件第一只输出“正常安装”的浏览器即注册表可信来源能查到的第二能识别多品牌浏览器包括Chrome、Edge、Firefox、IE、Opera、Vivaldi、Brave、360安全浏览器、360极速浏览器、QQ浏览器、搜狗高速浏览器、猎豹、傲游等第三结果必须去重同一个浏览器路径不能出现多条记录第四支持输出到屏幕同时可以导出成CSV或JSON方便接其他系统。执行流程分成五步定义浏览器特征关键词列表——遍历三类Uninstall注册表路径——筛选浏览器并提取信息——遍历StartMenuInternet和App Paths补充身份和路径——按exe路径去重后输出结果。3.2 关键代码与逐段解析下面这段脚本是我目前在实际环境用的精简版可以直接保存为Get-InstalledBrowsers.ps1运行。$browserKeywords ( chrome,chromium,microsoft edge,msedge,firefox,mozilla, internet explorer,iexplore,opera,vivaldi,brave, 360se,360chrome,qqbrowser,sogouexplorer,sogou, liebao,maxthon,2345explorer,baidubrowser,centbrowser ) $uninstallPaths ( HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*, HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*, HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\* ) $results () $seenPaths {} foreach ($path in $uninstallPaths) { $items Get-ItemProperty -Path $path -ErrorAction SilentlyContinue foreach ($item in $items) { $displayName $item.DisplayName if (-not $displayName) { continue } $matchText $displayName $($item.DisplayIcon) $($item.InstallLocation) $($item.UninstallString) $isBrowser $false foreach ($keyword in $browserKeywords) { if ($matchText -match $keyword) { $isBrowser $true break } } if (-not $isBrowser) { continue } # 挑选出最可能是浏览器exe的路径 $exePath if ($item.DisplayIcon) { $exePath $item.DisplayIcon.Trim() } elseif ($item.InstallLocation) { $exePath $item.InstallLocation } if (-not $exePath) { $exePath $item.UninstallString } $normalizedPath $exePath.ToLower() if ($seenPaths.ContainsKey($normalizedPath)) { continue } $seenPaths[$normalizedPath] $true $results [PSCustomObject]{ BrowserName $displayName Version $item.DisplayVersion InstallLocation $item.InstallLocation ExePath $exePath Publisher $item.Publisher Source Uninstall } } }这段脚本的关键点有三个一是用matchText把名称、图标路径、安装位置、卸载字符串拼在一起做匹配而不是只匹配DisplayName这样能提高命中率二是在匹配前不做“先过滤再判断”的优化避免漏掉那些DisplayName正常但Icon路径藏着浏览器特征的情况三是用seenPaths这个hashtable按标准化后的exe路径去重好过按名称去重——因为同一个浏览器可能出现多次Uninstall记录路径才是唯一性的锚点。3.3 补充StartMenuInternet和App Paths数据单纯靠Uninstall还是有可能漏掉新版Edge或者某些特殊安装的浏览器所以我习惯再跑一段补充逻辑$startMenuPaths ( HKLM:\SOFTWARE\Clients\StartMenuInternet, HKLM:\SOFTWARE\WOW6432Node\Clients\StartMenuInternet, HKCU:\SOFTWARE\Clients\StartMenuInternet ) foreach ($regPath in $startMenuPaths) { $clients Get-ChildItem -Path $regPath -ErrorAction SilentlyContinue foreach ($client in $clients) { $clientKey $client.PSPath $displayName (Get-ItemProperty -Path $clientKey -ErrorAction SilentlyContinue).(default) $commandPath (Get-ItemProperty -Path $clientKey\shell\open\command -ErrorAction SilentlyContinue).(default) if (-not $commandPath) { continue } $exePath $commandPath -replace ^([^])[\s\S]*$, $1 $normalizedPath $exePath.ToLower() if ($seenPaths.ContainsKey($normalizedPath)) { continue } $seenPaths[$normalizedPath] $true $results [PSCustomObject]{ BrowserName if ($displayName) { $displayName } else { $client.PSChildName } Version InstallLocation Split-Path $exePath -Parent ExePath $exePath Publisher Source StartMenuInternet } } } $results | Sort-Object BrowserName | Format-Table -AutoSize这里解释一下从StartMenuInternet拿到的commandPath往往带引号和参数比如C:\Program Files\Google\Chrome\Application\chrome.exe --profile-directoryDefault直接用会污染路径信息。我用正则^([^])[\s\S]*$提取第一段引号内的exe路径把参数全部切掉。这段在实测里非常有用很多同事直接复制commandPath去判断文件是否存在结果因为带着参数报错折腾半天。3.4 运行效果与输出示例脚本执行后的输出类似这样BrowserName Version ExePath ---------- ------- ------- Google Chrome 118.0.5993.88 C:\Program Files\Google\Chrome\Application\chrome.exe Microsoft Edge 117.0.2045.47 C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe Mozilla Firefox 119.0 C:\Program Files\Mozilla Firefox\firefox.exe Internet Explorer 11.0.19041.3633 C:\Program Files\Internet Explorer\iexplore.exe 360安全浏览器 13.1.1250.0 C:\Program Files (x86)\360\360se6\Application\360se.exe可以看到新版Edge单独出现了并没有被漏掉。如果你想要JSON输出给巡检平台用可以在脚本末尾加一句$results | ConvertTo-Json -Depth 3想写进CSV就$results | Export-Csv -NoTypeInformation browsers.csv。3.5 想换成Python或C#怎么做PowerShell脚本最贴合Windows环境但如果你的采集端是Linux或者要集成到跨平台CMDB里Python也是很好的选择。核心逻辑不变用winreg模块打开那几处注册表路径遍历子键读DisplayName、DisplayIcon、InstallLocation字段匹配关键词去重。大概是这种结构import winreg paths [ rSOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall, rSOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall, rSOFTWARE\Clients\StartMenuInternet, ] def get_browsers(): result [] for base_path in paths: try: key winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, base_path) except FileNotFoundError: continue for i in range(winreg.QueryInfoKey(key)[0]): sub_key_name winreg.EnumKey(key, i) # 分别读取子键里的字段 return resultPython方案的优点是跨平台部分好集成缺点是在目标机器上要先有Python环境考虑到本文场景是给Windows巡检用PowerShell仍然是首选。4. 实战避坑这几个问题我踩过不止一次只给代码不给坑等于没写。这一节把我在现场遇到过的几种典型问题列出来每一个都对应真实案例。4.1 32位和64位注册表重定向导致漏检早期我最早那版脚本只查了HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall在一台老旧的Windows 10机器上跑出来只有Edge和FirefoxChrome消失得无影无踪。检查之后发现那台机器上的Chrome是32位版本安装到了C:\Program Files (x86)\Google\Chrome它的卸载信息被Windows重定向写进了HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall。这是64位Windows系统的一个经典机制32位程序访问注册表时系统会把写入请求重定向到WOW6432Node节点。做枚举脚本时必须同时检查HKLM和WOW6432Node两个位置否则会漏掉所有32位安装的软件Chrome、Firefox的老版本、360系浏览器都中招过。4.2 新版Edge和Store版浏览器藏在MSIX包里新版Edge在Windows 10/11里默认通过MSIX打包它的卸载信息不在传统Uninstall注册表里所以如果你只扫Uninstall会漏掉Edge。解决方法是专门查Appx包Get-AppxPackage -Name *Edge*或者用StartMenuInternet补充检测。这一点尤其重要——我在给一家企业的资产清单盘点时就发现他们的报表里“没有Edge”但实际上每台Windows 10电脑都带着Edge只是因为检测脚本只认Uninstall路径。如果你要覆盖Microsoft Store安装的浏览器比如商店版Firefox、商店版Opera可以额外扫描Get-AppxPackage并匹配特征。要注意的是商店版浏览器在系统里表现得更像UWP应用版本号获取逻辑也略有区别我在脚本里会把这类标记成Source: MSIX方便下游消费时理解。4.3 浏览器厂商改名字导致特征匹配失败搜狗浏览器历史上叫“搜狗高速浏览器”可执行文件名是SogouExplorer.exe后来新版改过产品名称DisplayName变成了“搜狗浏览器”。如果你只拿“SogouExplorer”去匹配DisplayName新版很可能匹配不上。我自己的做法是做两路匹配一路匹配产品名称关键词比如“搜狗浏览器”“搜狗高速浏览器”都放进列表另一路匹配可执行文件名比如sogouexplorer.exe、sogou.exe。两边任一命中就算。为了保证列表不过时我还会定期从常见浏览器下载页抓一次exe名称作为辅助维护。4.4 远程执行时权限不足和杀毒软件拦截用Invoke-Command做远程批量采集时最常遇到两类问题。一是默认Remote Management服务没开启会连不上二是即使连上了如果当前用户不在目标机器的管理员组读取HKLM下的Uninstall注册表会被拒。解决办法是前置检查WinRM状态并确保使用具备本地管理员权限的域账号。另一个容易被忽视的是杀毒软件拦截。脚本里的Get-ItemProperty、Get-ChildItem都是标准管理操作一般不会触发告警但如果你为了做路径兜底直接遍历C:\Program Files、C:\Program Files (x86)下的所有exe文件某些EDR产品会认为你在做系统枚举扫描可能触发行为告警。所以我建议坚持注册表优先、目录兜底为辅不要全盘扫描文件。4.5 多个版本浏览器共存时如何优雅去重一台电脑上可能出现同一个浏览器的多个版本记录比如手动安装了一个新版本又保留了旧版本的卸载信息。如果按DisplayName去重会把不同版本的新旧记录合并成一条版本号取错的概率很高如果完全不去重又会看到两三行相同的名字。我最终选择按exe路径去重并且优先保留DisplayVersion字段更完整的记录。如果两个记录的exe路径不同但都指向同一个浏览器品牌我会在报表里保留两条并加一列Remark提示“检测到同品牌多版本”让人工决定用哪个。这种处理方式在两个浏览器并行过渡阶段尤其实用。5. 从“看一下”到“自动巡检”两个实用扩展脚本能跑只是第一步真正省力的是把它接进日常运维流程。5.1 结果导出JSON/CSV接入资产清单我习惯在脚本末尾加一个参数允许通过-OutputPath指定导出文件。导出的CSV可以直接用Excel打开做筛选也可以导入到IT资产管理平台。JSON格式则更适合喂给CMDB下游系统解析后自动更新每台终端InstalledBrowsers字段。这样一来浏览器盘点不再是一次性任务而可以纳入每周定时巡检。5.2 用Invoke-Command批量远程采集批量采集时我会准备一个computers.txt文件里面每行一个机器名然后用下面这个命令把脚本推过去执行$scriptContent Get-Content -Path .\Get-InstalledBrowsers.ps1 -Raw $computers Get-Content -Path .\computers.txt $allResults () foreach ($computer in $computers) { try { $session New-PSSession -ComputerName $computer -ErrorAction Stop $result Invoke-Command -Session $session -ScriptBlock { Invoke-Expression $using:scriptContent } $allResults $result Remove-PSSession $session } catch { Write-Warning 计算机 $computer 连接失败: $($_.Exception.Message) } } $allResults | Export-Csv -Path .\browsers_all.csv -NoTypeInformation -Encoding UTF8这段代码里的Remove-PSSession很容易漏但它决定了会不会在目标机器上留下多余的会话进程。连接失败的主机会先记录Warning等巡检结束后单独排查避免因一台机器掉线导致整个脚本中断。5.3 与部署流程联动的建议浏览器检测脚本最常见的一个下游场景是软件部署前检查。比如要给一批终端部署一个只兼容Chrome或Edge的组件部署脚本可以先调用浏览器检测函数判断目标浏览器是否存在、版本是否达到最低要求不满足就直接跳过并在日志里记录原因。相比直接尝试安装、失败后再排查这种前置检查能省下大量沟通成本。另一个场景是安全基线检查检查是否安装了指定版本的浏览器是否存在到期不更新的旧版本。把检测函数封装成模块配合定时任务就能形成持续监控的闭环。最后分享一个我自己的使用心得脚本里永远要保留Source字段哪怕你暂时用不到它。这个字段记录的是“这条浏览器记录从哪来”它写进报表后当一条记录看起来不合理时你能立刻判断出是Uninstall来源还是StartMenuInternet来源顺着线索定位问题。过去几个月我靠这个字段排掉了好几个“重复记录”和“过期版本”的疑惑。希望这套方案也能帮你把电脑上那堆浏览器彻底盘清楚。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进