鲲鹏 发表于 5 天前

调试笔记:微软符号服务器EXE/PDB链接自动生成工具

本帖最后由 鲲鹏 于 2026-7-23 14:07 编辑

调试Windows程序时,需向微软符号服务器请求资源,而链接的生成依赖两类PE元数据:


[*]PDB符号标识:来自PE调试目录中的CodeView记录(RSDS/NB10格式);
[*]EXE模块标识:来自PE头的`TimeDateStamp`(32位时间戳)和`SizeOfImage`(32位镜像大小),二者组合为EXE模块的唯一标识。

工具基于`pefile`库解析PE文件。核心流程:

[*]校验本地文件为PE格式;
[*]解析PE的版本资源段提取原始文件名(优先于本地文件名,避免篡改问题);
[*]遍历调试目录筛选CodeView记录,兼容RSDS(PDB 7.0+)和NB10(PDB 2.0)两种格式;
[*]从PE头提取`TimeDateStamp`和`SizeOfImage`,生成EXE模块唯一标识;
[*]按照微软符号服务器的标准格式(`https://msdl.microsoft.com/download/symbols/<模块名>/<标识>/<模块名>`)拼接URL,对文件名做URL编码处理。

代码如下。
#!/usr/bin/env python3

import os
import struct
import sys
from urllib.parse import quote

import pefile

SYMBOL_SERVER = "https://msdl.microsoft.com/download/symbols"


def get_original_filename(pe):
    """Return the PE's OriginalFilename version field, if present."""
    try:
      file_info_groups = getattr(pe, "FileInfo", [])
      for group in file_info_groups:
            entries = group if isinstance(group, list) else
            for file_info in entries:
                if getattr(file_info, "Key", None) != b"StringFileInfo":
                  continue
                for table in getattr(file_info, "StringTable", []):
                  value = table.entries.get(b"OriginalFilename")
                  if value:
                        return value.decode(errors="replace")
    except (AttributeError, TypeError):
      return None

    return None


def get_pe_symbol_name(pe, local_path):
    """Return the binary name used by Microsoft Symbol Server."""
    local_name = os.path.basename(local_path)
    original_name = get_original_filename(pe)

    if not original_name:
      return local_name

    original_name = os.path.basename(original_name)
    if not os.path.splitext(original_name):
      local_extension = os.path.splitext(local_name)
      original_name += local_extension

    return original_name


def get_pe_identity(pe):
    """Return the PE image identifier: TimeDateStamp + SizeOfImage."""
    timestamp = pe.FILE_HEADER.TimeDateStamp & 0xFFFFFFFF
    image_size = pe.OPTIONAL_HEADER.SizeOfImage
    return f"{timestamp:08X}{image_size:X}"


def get_codeview_records(pe):
    """Yield all CodeView records in the PE debug directory."""
    for entry in getattr(pe, "DIRECTORY_ENTRY_DEBUG", []):
      if entry.struct.Type != pefile.DEBUG_TYPE["IMAGE_DEBUG_TYPE_CODEVIEW"]:
            continue
      yield pe.get_data(
            entry.struct.AddressOfRawData,
            entry.struct.SizeOfData,
      )


def parse_rsds(cv_data):
    """Parse a PDB 7.0 RSDS CodeView record."""
    if len(cv_data) < 25 or cv_data[:4] != b"RSDS":
      return None

    guid_bytes = cv_data
    data1, data2, data3, *data4 = struct.unpack("<IHH8B", guid_bytes)
    guid = (
      f"{data1:08X}"
      f"{data2:04X}"
      f"{data3:04X}" + "".join(f"{x:02X}" for x in data4)
    )
    age = int.from_bytes(cv_data, "little")
    pdb_name = cv_data.split(b"\x00", 1).decode("utf-8", errors="replace")
    pdb_name = os.path.basename(pdb_name)

    if not pdb_name:
      return None

    return pdb_name, f"{guid}{age:X}"


def parse_nb10(cv_data):
    """Parse a legacy PDB 2.0 NB10 CodeView record."""
    if len(cv_data) < 17 or cv_data[:4] != b"NB10":
      return None

    timestamp = int.from_bytes(cv_data, "little")
    age = int.from_bytes(cv_data, "little")
    pdb_name = cv_data.split(b"\x00", 1).decode("utf-8", errors="replace")
    pdb_name = os.path.basename(pdb_name)

    if not pdb_name:
      return None

    return pdb_name, f"{timestamp:08X}{age:X}"


def get_pdb_symbol_url(pe):
    """Return the Microsoft Symbol Server PDB URL, if one can be derived."""
    for cv_data in get_codeview_records(pe):
      pdb_info = parse_rsds(cv_data) or parse_nb10(cv_data)
      if pdb_info:
            pdb_name, identifier = pdb_info
            encoded_name = quote(pdb_name, safe="")
            return f"{SYMBOL_SERVER}/{encoded_name}/{identifier}/{encoded_name}"
    return None


def get_pe_symbol_url(pe, path):
    """Return the Microsoft Symbol Server PE image URL."""
    image_name = get_pe_symbol_name(pe, path)
    identity = get_pe_identity(pe)
    encoded_name = quote(image_name, safe="")
    return f"{SYMBOL_SERVER}/{encoded_name}/{identity}/{encoded_name}"


def get_symbol_links(path):
    """Return PE image and PDB symbol URLs for a local PE file."""
    pe = pefile.PE(path, fast_load=False)
    try:
      return {
            "image": get_pe_symbol_url(pe, path),
            "pdb": get_pdb_symbol_url(pe),
      }
    finally:
      pe.close()


def main():
    if len(sys.argv) != 2:
      print(f"Usage: {sys.argv} <pe_file>")
      return 2

    path = sys.argv

    if not os.path.isfile(path):
      print(f"[-] File not found: {path}")
      return 1

    try:
      symbols = get_symbol_links(path)
    except Exception as e:
      print(f"[-] Failed: {e}")
      return 1

    print("\n[+] PE Symbol URL:")
    print(symbols["image"])
    print("\n[+] PDB Symbol URL:")
    print(symbols["pdb"] or "Not found")
    return 0


if __name__ == "__main__":
    sys.exit(main())

linxiansen 发表于 5 天前

#在这里快速回复PYG有你更精彩!#

chchhau 发表于 5 天前

PYG21周年生日快乐!

ope087 发表于 4 天前

上源码的最好了,学用兼备

linxiansen 发表于 4 天前


PYG21周年生日快乐!

飞天梦 发表于 3 天前

谢谢分享
页: [1]
查看完整版本: 调试笔记:微软符号服务器EXE/PDB链接自动生成工具