背景

问题发现

场景描述:

  • 我在复制消息准备粘贴到知乎进行搜索时,手误复制了多余的内容。
  • 粘贴到知乎时,内容变成了如下格式:
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