飘云阁

 找回密码
 加入我们

QQ登录

只需一步,快速开始

查看: 25|回复: 0

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

[复制链接]

该用户从未签到

发表于 1 小时前 | 显示全部楼层 |阅读模式
本帖最后由 鲲鹏 于 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编码处理。

代码如下。
[Python] 纯文本查看 复制代码
#!/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 [group]
            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)[1]:
        local_extension = os.path.splitext(local_name)[1]
        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[4:20]
    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[20:24], "little")
    pdb_name = cv_data[24:].split(b"\x00", 1)[0].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[8:12], "little")
    age = int.from_bytes(cv_data[12:16], "little")
    pdb_name = cv_data[16:].split(b"\x00", 1)[0].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[0]} <pe_file>")
        return 2

    path = sys.argv[1]

    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())

PYG19周年生日快乐!
您需要登录后才可以回帖 登录 | 加入我们

本版积分规则

快速回复 返回顶部 返回列表