Python: Windowsレジストリの検索

# Python: Windowsレジストリの検索

## はじめに

Windowsのシステムログを見ているとCLSID等をレジストリから調べなければならない事があります。
レジストリエディタから検索するのは不便なので何か便利な方法が知りたいです。
Windowsだけで完結できるPowerShellで検索出来ればよかったのですが、あまりPowerShellは得意ではないのでPythonで調べてみました。
調べた結果、簡単そうなのでPythonで実装してみました。

## 環境

```:OS
エディション  Windows 11 Pro
バージョン 21H2
OS ビルド    22000.318
エクスペリエンス    Windows 機能エクスペリエンス パック 1000.22000.318.0
システムの種類   64 ビット オペレーティング システム、x64 ベース プロセッサ
```

```:python
Python 3.10.1 (tags/v3.10.1:2cd268a, Dec  6 2021, 19:10:37) [MSC v.1929 64 bit (AMD64)]
```

## winreg

Pythonでレジストリを扱う場合は、`winreg`モジュールを使います。
書き込みは考えていないので扱う関数は、

- `winreg.OpenKey(key, sub_key, reserved=0, access=KEY_READ)`
- `winreg.CloseKey(hkey)`
- `winreg.QueryInfoKey(key)`
- `winreg.EnumKey(key, index)`
- `winreg.EnumValue(key, index)`

となります。

### 簡単な使い方

#### サンプル

```python
import winreg

key = winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, r"Installer\Dependencies\Microsoft.VisualStudio.Community.Msi,v17")

subKeyNum, valueNum, modifiedDate = winreg.QueryInfoKey(key)

subKeys = []
for i in range(subKeyNum):
  subKeys.append(winreg.EnumKey(key, i))

values = []
for i in range(valueNum):
  values.append(winreg.EnumValue(key, i))

winreg.CloseKey(key)

print(subKeys)
print(values)
```

#### `winreg.OpenKey(key, sub_key, reserved=0, access=KEY_READ)`

初めに、`winreg.OpenKey(key, sub_key, reserved=0, access=KEY_READ)`でキーを開きます。
`key`には、既に開かれたキーか、`winreg.HKEY_CLASSES_ROOT`などの定数を指定します。
`sub_key`には、サブキーを文字列で指定します。
例えば、"SOFTWARE\Policies\Microsoft\Windows\System"などです。
ここで注意してほしいのが "\SOFTWARE"などと先頭にバックスラッシュを付けていると何故だか分からないですが開けないことがある事です。

また、64bitのOSで実行していると32bitのレジストリが開けない事があります。
そういう場合は、`winreg.OpenKey(key, sub_key, access=KEY_READ|winreg.KEY_WOW64_32KEY)`のように
`access`に`winreg.KEY_WOW64_32KEY`を渡してやる必要があります。

#### `winreg.QueryInfoKey(key)`

次に、`winreg.QueryInfoKey(key)`でサブキーの数、値の数、変更日時を取得します。
返される値は、タプルで

- 0: サブキーの数
- 1: 値の数
- 2: 変更日時

となります。
変更日時は、以下のような値で返されるので状況に応じて変換する必要があります。

```:Pythonドキュメントより
最後のキーの変更が (あれば) いつだったかを表す整数で、1601 年 1 月 1 日からの 100 ナノ秒単位で数えたもの。
```

筆者は、以下の関数で変換してみましたがもっと良い方法があると思います。
(それっぽい値が出たので満足していますが、間違っているかもしれません。)

```python:python
def getDate(t):
  windowsEpoch = time.strptime("1601-01-01 00:00:00", "%Y-%m-%d %H:%M:%S")
  windowsEpoch = datetime.datetime(*windowsEpoch[:6])
  epoch = time.gmtime(0)
  epoch = datetime.datetime(*epoch[:6])
  t = time.localtime(t * 10e-8)
  t = datetime.datetime(*t[:6])
  return t - (epoch - windowsEpoch)
```

#### `winreg.EnumKey(key, index)`

`winreg.EnumKey(key, index)`は、`key`の`index`番目のサブキーを文字列で取得します。
`winreg.QueryInfoKey(key)`でサブキーの数を求めていれば、以下のような形で列挙できます。

```python:python
subKeys = []
for i in range(subKeyNum):
  subKeys.append(winreg.EnumKey(key, i))
```

#### `winreg.EnumValue(key, index)`

`winreg.EnumValue(key, index)`は、`key`の`index`番目の値を取得します。
返される値は、タプルで

- 0: 名前
- 1: データ
- 2: データの型

となります。
`winreg.QueryInfoKey(key)`で値の数を求めていれば、以下のような形で列挙できます。

```python:python
values = []
for i in range(valueNum):
  values.append(winreg.EnumValue(key, i))
```

筆者は、以下のような`namedtuple`で扱うことにしています。

```python:python
import collections
RegistryValue = collections.namedtuple("RegistryValue", "name data type")

values = []
for i in range(valueNum):
  values.append(RegistryValue(*winreg.EnumValue(key, i)))
```

#### `winreg.CloseKey(hkey)`

最後に、開いたキーを`winreg.CloseKey(hkey)`で閉じます。
もっとも、`with`文で開けるので使わないかもしれません。

```python:python
with winreg.OpenKey(key, subKey) as reg:
  #
```

#### その他の注意点(UnicodeErrorについて)

Pythonは、文字列を扱う場合に内部ではエンコードを`utf-8`で扱います。
そしてWindows上で`print()`などを使い出力する場合は、エンコードを`cp932`に変換します。
この時、変換できない文字があると`UnicodeError`例外が発生します。
残念ながらレジストリのデータには、`utf-8`から`cp932`変換できない文字がちらほらと存在します。
そのため、この対策をする必要があります。

筆者は、以下のような関数を使い変換できない文字を置き換えて出力するようにしています。

```python:python
def removeUnicodeErrorForWindows(string):
  return str(string).encode("cp932", "namereplace").decode("cp932")


def printForWindows(string):
  print(removeUnicodeErrorForWindows(string))
```

## コード

レジストリは、ディレクトリとファイルのように木構造になっているので再帰的にデータを取得しないと扱いづらいです。
そこで、`Registry`クラスで指定したサブキー以下のキーをすべて取得するようにしています。
`RegistryString`は、`Registry`の内容を文字列に変換して表示するためのクラスです。
再帰処理で書けばもう少しすっきりとしますが、勉強のためにループで実装しました。

```python:windwosRegistry.py
import winreg
import time
import datetime
import collections
import os.path
import re
import sys
import argparse

# 手作業でなくてzipで処理しても良い
Entry = {
  winreg.HKEY_CLASSES_ROOT: "HKEY_CLASSES_ROOT",
  winreg.HKEY_CURRENT_USER: "HKEY_CURRENT_USER",
  winreg.HKEY_LOCAL_MACHINE: "HKEY_LOCAL_MACHINE",
  winreg.HKEY_USERS: "HKEY_USERS",
  winreg.HKEY_PERFORMANCE_DATA: "HKEY_PERFORMANCE_DATA",
  winreg.HKEY_CURRENT_CONFIG: "HKEY_CURRENT_CONFIG",
}

Type = {
  winreg.REG_BINARY: "REG_BINARY",  # winreg.REG_BINARY 何らかの形式のバイナリデータ。
  winreg.REG_DWORD: "REG_DWORD",  # winreg.REG_DWORD 32 ビットの数。
  winreg.REG_DWORD_LITTLE_ENDIAN: "REG_DWORD_LITTLE_ENDIAN",  # winreg.REG_DWORD_LITTLE_ENDIAN 32 ビットのリトルエンディアン形式の数。REG_DWORD と等価。
  winreg.REG_DWORD_BIG_ENDIAN: "REG_DWORD_BIG_ENDIAN",  # winreg.REG_DWORD_BIG_ENDIAN 32 ビットのビッグエンディアン形式の数。
  winreg.REG_EXPAND_SZ: "REG_EXPAND_SZ",  # winreg.REG_EXPAND_SZ 環境変数を参照している、ヌル文字で終端された文字列。(%PATH%)。
  winreg.REG_LINK: "REG_LINK",  # winreg.REG_LINK Unicode のシンボリックリンク。
  winreg.REG_MULTI_SZ: "REG_MULTI_SZ",  # winreg.REG_MULTI_SZ ヌル文字で終端された文字列からなり、二つのヌル文字で終端されている配列。 (Python はこの終端の処理を自動的に行います。)
  winreg.REG_NONE: "REG_NONE",  # winreg.REG_NONE 定義されていない値の形式。
  winreg.REG_QWORD: "REG_QWORD",  # winreg.REG_QWORD 64 ビットの数。
  winreg.REG_QWORD_LITTLE_ENDIAN: "REG_QWORD_LITTLE_ENDIAN",  # winreg.REG_QWORD_LITTLE_ENDIAN 64 ビットのリトルエンディアン形式の数。REG_QWORD と等価。
  winreg.REG_RESOURCE_LIST: "REG_RESOURCE_LIST",  # winreg.REG_RESOURCE_LIST デバイスドライバリソースのリスト。
  winreg.REG_FULL_RESOURCE_DESCRIPTOR: "REG_FULL_RESOURCE_DESCRIPTOR",  # winreg.REG_FULL_RESOURCE_DESCRIPTOR ハードウェアセッティング。
  winreg.REG_RESOURCE_REQUIREMENTS_LIST: "REG_RESOURCE_REQUIREMENTS_LIST",  # winreg.REG_RESOURCE_REQUIREMENTS_LIST ハードウェアリソースリスト。
  winreg.REG_SZ: "REG_SZ",  # winreg.REG_SZ ヌル文字で終端された文字列。
}


def keyFromValue(d, value, unique=True):
  result = [k for k, v in d.items() if v == value]
  if len(result) == 0:
    return None
  return result[0] if unique else result


def getZeroFillString(i, num=10):
  length = len(str(num))
  return str(i).zfill(length)


def getDate(t):
  windowsEpoch = time.strptime("1601-01-01 00:00:00", "%Y-%m-%d %H:%M:%S")
  windowsEpoch = datetime.datetime(*windowsEpoch[:6])
  epoch = time.gmtime(0)
  epoch = datetime.datetime(*epoch[:6])
  t = time.localtime(t * 10e-8)
  t = datetime.datetime(*t[:6])
  return t - (epoch - windowsEpoch)


def join(x, y):
  if x != "":
    return f"{x}\\{y}"
  return y


def removeUnicodeErrorForWindows(string):
  return str(string).encode("cp932", "namereplace").decode("cp932")


def printForWindows(string):
  print(removeUnicodeErrorForWindows(string))


RegistryValue = collections.namedtuple("RegistryValue", "name data type")


class Registry():

  def __init__(self, key, subKey, depth=0, index=0, maxIndex=0, parentKey=None):
    self.keyRoot = key
    self.subKey = subKey
    self.depth = depth  # 表示のためのメンバ。親から見ての階層
    self.index = index  # 表示のためのメンバ。親から見てindex番目のsubKey
    self.maxIndex = maxIndex  # 表示のためのメンバ。親のsubKeyの個数
    self.values = []
    self.valueNum = 0
    self.subKeys = []
    self.subKeyNum = 0
    self.modifiedDate = ""
    self.parentKey = parentKey
    self.init()

  def __repr__(self):
    return f"{self.getKeyString()}: {self.subKeyNum}, {self.valueNum}, {self.modifiedDate}"

  def open(self):
    try:
      key = winreg.OpenKey(self.keyRoot, self.subKey, access=winreg.KEY_READ)
    except FileNotFoundError:
      try:
        key = winreg.OpenKey(self.keyRoot, self.subKey, access=winreg.KEY_READ | winreg.KEY_WOW64_32KEY)  # 64bitのOSで動かすことを前提として、32bitのkeyを表示させる処理
      except FileNotFoundError:
        print(self.getKeyString())
        raise
    return key

  def init(self):
    with self.open() as key:
      self.setQueryInfo(key)
      self.setValues(key)
      self.setSubKeyString(key)
    if self.parentKey is None:  # ここの仕組みが気に入らない
      self.setSubKey()

  def setQueryInfo(self, key):
    info = winreg.QueryInfoKey(key)
    self.subKeyNum = info[0]
    self.valueNum = info[1]
    self.modifiedDate = getDate(info[2])

  def setValues(self, key):
    self.values = [RegistryValue(*winreg.EnumValue(key, i)) for i in range(self.valueNum)]

  def setSubKeyString(self, key):
    self.subKeys = []
    for i in range(self.subKeyNum):
      subKey = winreg.EnumKey(key, i)
      self.subKeys.append(subKey)
      # reg = Registry(self.keyRoot, join(self.subKey, subKey), depth=self.depth + 1, index=i, maxIndex=self.subKeyNum) # 再帰の方がスッキリと書ける。と言うか再帰から変換したから
      # self.subKeys.append(reg)

  def setSubKey(self):
    stack = [self]
    while len(stack) > 0:
      key = stack.pop()
      for i, subKey in enumerate(key.subKeys):
        if isinstance(subKey, Registry):
          return
        reg = Registry(key.keyRoot, join(key.subKey, subKey), depth=key.depth + 1, index=i, maxIndex=key.subKeyNum, parentKey=key)  # 先頭がバックスラッシュだと開けないことがあるのでjoinを使う。winregのバグか?
        key.subKeys[i] = reg
        stack.append(reg)

  def getKeyString(self):
    return f"{Entry.get(self.keyRoot, '')}\\{self.subKey}"

  def searchSelf(self, string, result=None):
    if result is None:
      result = []
    pattern = Registry.getCompiled(string)
    self.searchKey(pattern, result)
    self.searchValue(pattern, result)
    return result

  @staticmethod
  def search(registry, string, result=None):
    if result is None:
      result = []
    registry.searchSelf(string, result)
    if registry in result:
      return result

    stack = registry.subKeys[::-1]
    while len(stack) > 0:
      key = stack.pop()
      key.searchSelf(string, result)
      stack += key.subKeys[::-1]
    return result

  @staticmethod
  def getCompiled(pattern):
    if isinstance(pattern, re.Pattern):
      return pattern
    return re.compile(pattern)

  def searchKey(self, search, result):
    if self in result:
      return
    pattern = Registry.getCompiled(search)
    key = self.getKeyString()
    self.reSearch(pattern, key, result)

  def searchValue(self, search, result):
    if self in result:
      return
    pattern = Registry.getCompiled(search)
    for value in self.values:
      self.reSearch(pattern, value.name, result)
      self.reSearch(pattern, value.data, result)
      # self.reSearch(pattern, value.type, result)

  def reSearch(self, pattern, string, result):
    if not isinstance(string, str):
      return
    match = pattern.search(string)
    if match:
      if self not in result:
        result.append(self)


class RegistryString():
  NoPrint = (winreg.REG_BINARY,)  # dataがREG_BINARYの場合、表示させない
  setIndent = True
  setIndex = False
  setFullPath = True

  def __new__(cls):
    raise TypeError(f"{cls.__name__} cannot be instantiated")

  @staticmethod
  def getKeyString(registry, setFullPath):
    if setFullPath:
      return f"{Entry.get(registry.keyRoot, '')}\\{registry.subKey}"
    return f"{os.path.basename(registry.subKey)}"  #

  @classmethod
  def getValuesString(cls, registry, depth):
    return [cls.getValueString(registry, i, depth) for i in range(registry.valueNum)]

  @classmethod
  def getValueString(cls, registry, i, depth):
    indent = cls.getIndent(depth)
    index = cls.getIndex("V", i, registry.valueNum)
    value = registry.values[i]
    if any(value.type == x for x in RegistryString.NoPrint):
      return f"  {indent}{index}\"{value.name}\" ({Type.get(value.type)})"
    return f"  {indent}{index}\"{value.name}\" = \"{value.data}\" ({Type.get(value.type)})"

  @classmethod
  def getTreeString(cls, registry):
    string = cls.getInfoString(registry, False)
    stack = registry.subKeys[::-1]
    while len(stack) > 0:
      key = stack.pop()
      string += cls.getInfoString(key, False)
      stack += key.subKeys[::-1]
    return string

  @classmethod
  def getQueryString(cls, registry, depth, setFullPath):
    indent = cls.getIndent(depth)
    index = cls.getIndex("K", registry.index, registry.maxIndex)
    return f"{indent}{index}{cls.getKeyString(registry, setFullPath)} ({registry.subKeyNum}, {registry.valueNum}, {registry.modifiedDate})\n"

  @classmethod
  def getInfoString(cls, registry, showSubKey=True):
    depth = registry.depth if cls.setIndent else 0
    string = cls.getQueryString(registry, depth, cls.setFullPath)
    values = cls.getValuesString(registry, depth)
    for value in values:
      string += f"{value}\n"
    if showSubKey:
      for subKey in registry.subKeys:
        string += f"{cls.getQueryString(subKey, depth + 1, False)}"
    return string

  @classmethod
  def getIndent(cls, depth):
    return "  " * depth

  @classmethod
  def getIndex(cls, t, i, n):
    return f"{t}{getZeroFillString(i, n)}: " if cls.setIndex else ""

  @classmethod
  def setOption(cls, setIndent, setIndex, setFullPath):
    cls.setIndent = setIndent
    cls.setIndex = setIndex
    cls.setFullPath = setFullPath


def argumentParser():
  parser = argparse.ArgumentParser()
  parser.add_argument("key", help="レジストリキーを指定する")
  parser.add_argument("-o", "--outputPath", default="", help="出力先のパス。指定されなかったら標準出力へ出力する")
  parser.add_argument("-t", "--tree", action="store_true", help="階層構造を表示する。指定されなったらキーとそのサブキーのみ表示する。")
  parser.add_argument("-s", "--search", default="", help="検索文字列を正規表現で指定する")

  parser.add_argument("-ix", "--setIndex", action="store_true", help="インデックスを表示する")
  parser.add_argument("-it", "--setIndent", action="store_false", help="インデントを設定しない")
  parser.add_argument("-fp", "--setFullPath", action="store_false", help="絶対パスで表示しない")
  parser.add_argument("-a", "--showArgument", action="store_true", help="show arguments.")
  return parser.parse_args()


def getKey(key):
  reEntry = re.compile(r"\\?([^\\]+)(?:\\(.*))?")
  match = reEntry.search(key)
  if match is None:
    return None, None
  return keyFromValue(Entry, match.groups("")[0]), match.groups("")[1]


if __name__ == "__main__":
  args = argumentParser()
  if args.showArgument:
    print(args)

  keyRoot, subKey = getKey(args.key)

  print(f"\"{Entry.get(keyRoot, '')}\", \"{subKey}\"")
  if keyRoot is None:
    print(f"invalid key. {args.key}")
    sys.exit(1)

  reg = Registry(keyRoot, subKey)
  output = ""
  RegistryString.setOption(args.setIndent, args.setIndex, args.setFullPath)
  if args.tree:
    output = RegistryString.getTreeString(reg)
  else:
    output = RegistryString.getInfoString(reg)

  if args.search != "":
    # RegistryString.setOption(False, args.setIndex, args.setFullPath)
    result = Registry.search(reg, args.search)
    output += "\nsearch result:\n"
    for i in result:
      output += RegistryString.getInfoString(i)

  if args.outputPath != "":
    with open(args.outputPath, "w", encoding="utf-8") as file:
      file.write(output)
  else:
    printForWindows(output)

```

### 使い方

ファイル名を `windowsRegistry.py` で保存したとします。

```
>py .\windowsRegistry.py -h                                                       
usage: windowsRegistry.py [-h] [-o OUTPUTPATH] [-t] [-s SEARCH] [-ix] [-it] [-fp] [-a] key

positional arguments:
  key                   レジストリキーを指定する

options:
  -h, --help            show this help message and exit
  -o OUTPUTPATH, --outputPath OUTPUTPATH
                        出力先のパス。指定されなかったら標準出力へ出力する
  -t, --tree            階層構造を表示する。指定されなったらキーとそのサブキーのみ表示する。
  -s SEARCH, --search SEARCH
                        検索文字列を正規表現で指定する
  -ix, --setIndex       インデックスを表示する
  -it, --setIndent      インデントを設定しない
  -fp, --setFullPath    絶対パスで表示しない
  -a, --showArgument    show arguments.
```

### 例

必須パラメータであるキーだけを指定するとそのキーだけを表示します。
現状、表示されない階層構造も再帰的に取得してしまいます。

```console:console
>py windowsRegistry.py "HKEY_CLASSES_ROOT\*"
```

`HKEY_CLASSES_ROOT\*`の階層を表示したい場合は、`-t`オプションを指定します。

```console:console
>py windowsRegistry.py "HKEY_CLASSES_ROOT\*" -t
```

キー、値の名前、値のデータのどれかに正規表現で表されるパターンが含まれているか検索する場合、`-s pattern`を指定します。

```console:console
>py windowsRegistry.py "HKEY_CLASSES_ROOT\*" -s "shell"
```

結果をファイルに出力したい場合は、`-o OUTPUTPATH`を指定します。

```console:console
>py windowsRegistry.py "HKEY_CLASSES_ROOT\*" -t -o "output.txt"
```


```shell:実行結果例
> py .\windowsRegistry.py "HKEY_CLASSES_ROOT\*\shell\UpdateEncryptionSettingsWork" -t             
"HKEY_CLASSES_ROOT", "*\shell\UpdateEncryptionSettingsWork"
HKEY_CLASSES_ROOT\*\shell\UpdateEncryptionSettingsWork (1, 8, 2021-06-05 21:13:52)
  "AppliesTo" = "System.StorageProviderId:<>"network" AND System.StorageProviderProtectionMode:<>1 AND System.StorageProviderProtectionMode:<>2" (REG_SZ)
  "AttributeMask" = "8192" (REG_DWORD_LITTLE_ENDIAN)
  "AttributeValue" = "8192" (REG_DWORD_LITTLE_ENDIAN)
  "ExtendedSubCommandsKey" = "*\shell\UpdateEncryptionSettingsWork" (REG_SZ)
  "ImpliedSelectionModel" = "0" (REG_DWORD_LITTLE_ENDIAN)
  "MUIVerb" = "@efscore.dll,-101" (REG_SZ)
  "MultiSelectModel" = "Player" (REG_SZ)
  "Position" = "Bottom" (REG_SZ)
  HKEY_CLASSES_ROOT\*\shell\UpdateEncryptionSettingsWork\Shell (1, 0, 2021-06-05 21:13:52)
    HKEY_CLASSES_ROOT\*\shell\UpdateEncryptionSettingsWork\Shell\Decrypt (1, 7, 2021-06-05 21:13:52)
      "AttributeMask" = "8192" (REG_DWORD_LITTLE_ENDIAN)
      "AttributeValue" = "8192" (REG_DWORD_LITTLE_ENDIAN)
      "CommandStateHandler" = "{5B6D1451-B1E1-4372-90F5-88E541B4DAB9}" (REG_SZ)
      "Icon" = "edputil.dll,-1002" (REG_SZ)
      "MUIVerb" = "@efscore.dll,-103" (REG_SZ)
      "ShowAsDisabledIfHidden" = "" (REG_SZ)
      "UpdateType" = "0" (REG_DWORD_LITTLE_ENDIAN)
      HKEY_CLASSES_ROOT\*\shell\UpdateEncryptionSettingsWork\Shell\Decrypt\command (0, 1, 2021-06-05 21:13:52)
        "DelegateExecute" = "{5B6D1451-B1E1-4372-90F5-88E541B4DAB9}" (REG_SZ)

```