背景

问题发现

场景描述:

  • 我在复制消息准备粘贴到知乎进行搜索时,手误复制了多余的内容。
  • 粘贴到知乎时,内容变成了如下格式:
    1
    2
    3
    4
    5
     ㅤ: 02-15 19:09:01
    我才没看

    ln: 02-15 19:09:26
    你在知乎每天搜一遍怎么哄女朋友开心
  • 进一步观察:将同样的内容粘贴到QQ聊天输入框时,内容却变成了转发消息的格式:

  • 剪切板历史:按下 Win + V 打开Windows的剪切板历史,看到的内容与粘贴到知乎时一致:

问题思考

  • 剪切板中可能存在一些隐藏的数据格式,导致粘贴到不同应用程序时呈现不同的效果。联想到一些博客网站在复制内容时,会自动添加禁止转载的文案,或者在Word中粘贴时带有奇怪的格式和背景,这些现象进一步坚定了我的猜想。

技术探索

获取剪切板数据

  • 为了验证猜想,我编写了一个Python脚本,使用 win32clipboard 模块来获取剪切板中的所有数据格式及其内容。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    import win32clipboard


    def get_all_clipboard_data():
    """获取剪贴板中所有格式的数据"""
    win32clipboard.OpenClipboard()
    try:
    format_id = 0
    formats = []

    # 枚举所有剪贴板格式
    print("\n=== 剪贴板格式列表 ===")
    while True:
    format_id = win32clipboard.EnumClipboardFormats(format_id)
    if format_id == 0:
    break
    try:
    format_name = win32clipboard.GetClipboardFormatName(format_id)
    except:
    format_name = f"系统格式 {format_id}"
    formats.append((format_id, format_name))
    print(f"格式 ID: {format_id}, 名称: {format_name}")

    print("\n=== 剪贴板数据内容 ===")
    # 读取每种格式的数据
    for fmt, name in formats:
    try:
    data = win32clipboard.GetClipboardData(fmt)
    print(f"\n格式 ID: {fmt} ({name})")

    # 处理文本数据
    if isinstance(data, bytes):
    try:
    decoded_data = data.decode('utf-8')
    print(f"内容 (UTF-8 解码):\n{decoded_data}")
    except UnicodeDecodeError:
    print(f"数据是字节流,无法用 UTF-8 解码 ({len(data)} 字节)")
    elif isinstance(data, str):
    print(f"内容 (字符串):\n{data}")
    else:
    print(f"未知数据类型: {type(data)}")

    except Exception as e:
    print(f"格式 ID {fmt} ({name}) 读取失败: {e}")

    finally:
    win32clipboard.CloseClipboard()


    get_all_clipboard_data()
  • 输出结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    === 剪贴板格式列表 ===
    格式 ID: 49914, 名称: QQ_MultiMsg_RichEdit_Format
    格式 ID: 13, 名称: 系统格式 13
    格式 ID: 49393, 名称: HTML Format
    格式 ID: 16, 名称: 系统格式 16
    格式 ID: 1, 名称: 系统格式 1
    格式 ID: 7, 名称: 系统格式 7

    === 剪贴板数据内容 ===

    格式 ID: 49914 (QQ_MultiMsg_RichEdit_Format)
    数据是字节流,无法用 UTF-8 解码 (163 字节)

    格式 ID: 13 (系统格式 13)
    内容 (字符串):
     ㅤ: 02-15 19:09:01
    我才没看

    ln: 02-15 19:09:26
    你在知乎每天搜一遍怎么哄女朋友开心

    格式 ID: 49393 (HTML Format)
    内容 (UTF-8 解码):
    Version:0.9
    StartHTML:0000000117
    EndHTML:0000000296
    StartFragment:0000000153
    EndFragment:0000000260
    SourceURL:
    <html>
    <body>
    <!--StartFragment--> ㅤ: 02-15 19:09:01
    我才没看

    ln: 02-15 19:09:26
    你在知乎每天搜一遍怎么哄女朋友开心<!--EndFragment-->
    </body>
    </html>

    格式 ID: 16 (系统格式 16)
    内容 (UTF-8 解码):


    格式 ID: 1 (系统格式 1)
    数据是字节流,无法用 UTF-8 解码 (83 字节)

    格式 ID: 7 (系统格式 7)
    数据是字节流,无法用 UTF-8 解码 (83 字节)
  • 结果发现剪切板中存在一个名为 QQ_MultiMsg_RichEdit_Format 的格式,其内容为字节流,无法直接解码。这很可能是导致粘贴到QQ时渲染成消息卡片的原因。只解析出了部分内容,除非知道这个Format的定义,否则无法解析全部内容
    1
    2
    3
    4
    5
    6
    7
    8
    9


    �����g

    ��������gu_WS9xyQOjT3nF6n_5GB_gNQ" *^ ㅤ: 02-15 19:09:01
    我才没看

    ln: 02-15 19:09:26
    你在知乎每天搜一遍怎么哄女朋友开心

Hook验证

  • 为了进一步验证QQ在粘贴时是否使用了这个格式,我使用 frida 工具对QQ的 GetClipboardData 函数进行了Hook。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    import frida


    def get_process_pid(target_name):
    """ 获取目标进程的 PID """
    device = frida.get_local_device()
    processes = device.enumerate_processes()

    matched_pids = [p.pid for p in processes if p.name.lower() == target_name.lower()]

    if not matched_pids:
    raise ValueError(f"未找到 {target_name} 进程")

    if len(matched_pids) == 1:
    return matched_pids[0]

    print(f"发现多个 {target_name} 进程,请选择一个 PID:")
    for idx, pid in enumerate(matched_pids):
    print(f"[{idx}] PID: {pid}")

    selection = int(input("请输入索引选择目标进程: "))
    return matched_pids[selection]


    def hook_clipboard():
    """ Hook GetClipboardData 监听粘贴格式 """
    process_name = "QQ.exe" # 目标进程,如 QQ
    target_pid = get_process_pid(process_name)

    session = frida.attach(target_pid)

    script_code = """
    var user32 = Module.findExportByName("user32.dll", "GetClipboardData");

    Interceptor.attach(user32, {
    onEnter: function (args) {
    var format_id = args[0].toInt32(); // 获取 format_id
    send(format_id); // 发送给 Python 端
    }
    });
    """

    def on_message(message, data):
    if message["type"] == "send":
    print("[Hooked] 粘贴时获取的格式 ID:", message["payload"])
    else:
    print("[Error]", message)

    script = session.create_script(script_code)
    script.on("message", on_message)
    script.load()

    print(f"成功 Hook 进程 {process_name} (PID: {target_pid}), 等待粘贴操作...")
    input("按回车退出监听...\n")


    if __name__ == "__main__":
    hook_clipboard()
  • Hook结果
    1
    2
    3
    4
    5
    [Hooked] 粘贴时获取的格式 ID: 13
    [Hooked] 粘贴时获取的格式 ID: 49815
    [Hooked] 粘贴时获取的格式 ID: 49393
    [Hooked] 粘贴时获取的格式 ID: 49815
    [Hooked] 粘贴时获取的格式 ID: 49914
  • QQ在粘贴时确实使用了 QQ_MultiMsg_RichEdit_Format 格式,这验证了之前的猜想。

进一步探索

  • 为了更深入地了解剪切板的变化,我编写了一个简单的轮询脚本,用于监听剪切板格式的变化。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    import time
    import win32clipboard


    def get_clipboard_formats():
    """获取当前剪贴板中的所有格式"""
    win32clipboard.OpenClipboard()
    try:
    format_id = 0
    formats = []
    while True:
    format_id = win32clipboard.EnumClipboardFormats(format_id)
    if format_id == 0:
    break
    try:
    format_name = win32clipboard.GetClipboardFormatName(format_id)
    except:
    format_name = f"系统格式 {format_id}"
    formats.append((format_id, format_name))
    return formats
    finally:
    win32clipboard.CloseClipboard()


    # 轮询检测剪贴板的变化
    prev_formats = []
    while True:
    formats = get_clipboard_formats()
    if formats != prev_formats:
    print("\n剪贴板格式发生变化:")
    for fmt, name in formats:
    print(f"格式 ID: {fmt}, 名称: {name}")
    prev_formats = formats
    time.sleep(1)
  • 该脚本可以实时监控剪切板格式的变化,帮助开发者更好地理解不同应用程序如何操作剪切板。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    剪贴板格式发生变化:
    格式 ID: 49393, 名称: HTML Format
    格式 ID: 49292, 名称: Rich Text Format
    格式 ID: 13, 名称: 系统格式 13
    格式 ID: 1, 名称: 系统格式 1
    格式 ID: 49398, 名称: UniformResourceLocator
    格式 ID: 49918, 名称: JAVA_DATAFLAVOR:application/x-java-jvm-local-objectref; class=com.intellij.codeInsight.editorActions.FoldingData
    格式 ID: 49971, 名称: JAVA_DATAFLAVOR:application/x-java-jvm-local-objectref; class=com.intellij.codeInsight.editorActions.ReferenceData
    格式 ID: 49915, 名称: JAVA_DATAFLAVOR:application/x-java-serialized-object; class=com.intellij.openapi.editor.impl.EditorCopyPasteHelperImpl$CopyPasteOptionsTransferableData
    格式 ID: 16, 名称: 系统格式 16
    格式 ID: 7, 名称: 系统格式 7

    剪贴板格式发生变化:
    格式 ID: 49914, 名称: QQ_MultiMsg_RichEdit_Format
    格式 ID: 13, 名称: 系统格式 13
    格式 ID: 49393, 名称: HTML Format
    格式 ID: 16, 名称: 系统格式 16
    格式 ID: 1, 名称: 系统格式 1
    格式 ID: 7, 名称: 系统格式 7

    剪贴板格式发生变化:
    格式 ID: 49393, 名称: HTML Format
    格式 ID: 13, 名称: 系统格式 13
    格式 ID: 49815, 名称: Chromium internal source URL
    格式 ID: 16, 名称: 系统格式 16
    格式 ID: 1, 名称: 系统格式 1
    格式 ID: 7, 名称: 系统格式 7