
开发者逆向分析苹果系统词典中的脏话词表机制
来源标题:I Dumped Apple's Fucking Word List
开发者通过逆向 dyld 共享缓存,发现苹果系统内置了带 LX 前缀的私有不文明用语词库 API(Lexicon.framework),用于标记和同步脏话词汇。测试中向 TextEdit 输入脏话未触发词库记录,数据库仅统计已被标记为不文明用语的词。他说不清具体判定和同步逻辑,征询其他用户是否见过类似数据。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
一个 _KSUserWordsSynchroniser sharedAcrossLanguagesVulgarWordUsageValue 符号暗示,它会像你其他学过的词一样在设备之间同步。
我试着往里面填内容。用 agent-desktop,我打开一个新的 TextEdit 文档,输入了 well fuck this shit, damn it.,先是以文本插入的方式,然后改成逐个按键输入。什么都没变:没有新增行,没有日志记录,文件的修改日期也还停在 2024 年。
我不知道原因,也没能弄清楚。也许这台 Mac 根本不在这里记录任何东西,也许它只在某些 App 或某些设置下才记录。如果你那边有数据行,告诉我你是怎么做到的。
那样的话这篇文章就太短了。但这个数据库只统计已被认定为粗俗的词。那么,是谁来决定哪些词算粗俗?
在 dyld shared cache 里搜索 "vulgar",找到了一个带 LX 前缀的 C API(LXVulgarWordUsageDatabaseCreate、LXVulgarWordUsageDatabaseCanAutocorrect……),更有用的是词典条目上的两个函数:
_LXEntryGetCategoryFlags _LXEntryGetMetaFlags
LX 指的是 Lexicon.framework,一个私有框架,它读取 /System/Library/LinguisticData/ 下按语言划分的词典。这些词典里的每个词都是一个 LXEntry,带两个 32 位的标志字段。如果粗俗度评级存在于某处,那一定就在这些位里。
我去看了。NSSpellChecker、UITextChecker、NaturalLanguage 和 UILexicon 都不暴露粗俗度。SensitiveContentAnalysis 覆盖的是图像和视频,不是文本。在 developer.apple.com 上,"profanity" 只出现在 App Store 年龄分级和审核指南里。
所以:只能用私有函数。Lexicon 没有头文件,但那些 C 函数是导出的,而且它们的签名很容易猜,因为它们遵循 CoreFoundation 的惯例。下面这个程序打开某个 locale 的词典,遍历每一个条目,并打印两个标志字段:
// lx.c — dump every word in a system lexicon with its flags. // clang -O1 -o lx lx.c -F/System/Library/PrivateFrameworks -framework Lexicon -framework CoreFoundation #include <CoreFoundation/CoreFoundation.h> #include <stdbool.h> #include <stdio.h> typedef const void *LXLexiconRef, *LXCursorRef, *LXEntryRef; extern CFStringRef kLXLexiconLocaleKey; LXLexiconRef LXLexiconCreate(CFDictionaryRef options, CFErrorRef *error); LXCursorRef LXLexiconCreateRootCursor(LXLexiconRef); void LXCursorEnumerateEntriesRecursively(LXCursorRef, void (^)(LXEntryRef, bool *stop)); CFStringRef LXEntryCopyString(LXEntryRef); uint32_t LXEntryGetCategoryFlags(LXEntryRef); uint32_t LXEntryGetMetaFlags(LXEntryRef); int main(int argc, char **argv) { CFStringRef locale = CFStringCreateWithCString(NULL, argc > 1 ? argv[1] : "en_US", kCFStringEncodingUTF8); const void *keys[] = {kLXLexiconLocaleKey}, *values[] = {locale}; CFDictionaryRef options = CFDictionaryCreate(NULL, keys, values, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); LXLexiconRef lexicon = LXLexiconCreate(options, NULL); if (!lexicon) return 1; LXCursorEnumerateEntriesRecursively(LXLexiconCreateRootCursor(lexicon), ^(LXEntryRef entry, bool *stop) { char word[512] = ""; CFStringRef s = LXEntryCopyString(entry); if (s) { CFStringGetCString(s, word, sizeof word, kCFStringEncodingUTF8); CFRelease(s); } printf("%08x\t%08x\t%s\n", LXEntryGetCategoryFlags(entry), LXEntryGetMetaFlags(entry), word); }); }
$ ./lx en_US | wc -l 199019 $ ./lx en_US | awk -F'\t' '$3 == "fuck" || $3 == "table" || $3 == "Trump"' 00000000 00800002 fuck 00000000 00000000 table 00002000 02000081 Trump
同样的东西用 Swift 写,通过 dlopen 和 dlsym 在运行时加载这些函数,这样就不需要链接私有框架。它只打印带有三个层级之一的词:
// lexflags.swift — swiftc -O lexflags.swift -o lexflags && ./lexflags en_US import Foundation let lexicon = dlopen("/System/Library/PrivateFrameworks/Lexicon.framework/Lexicon", RTLD_NOW)! func sym<T>(_ name: String, as _: T.Type) -> T { unsafeBitCast(dlsym(lexicon, name)!, to: T.self) } typealias LXRef = UnsafeRawPointer let create = sym("LXLexiconCreate", as: (@convention(c) (CFDictionary, UnsafeMutablePointer<Unmanaged<CFError>?>?) -> LXRef?).self) let rootCursor = sym("LXLexiconCreateRootCursor", as: (@convention(c) (LXRef) -> LXRef?).self) let enumerate = sym("LXCursorEnumerateEntriesRecursively", as: (@convention(c) (LXRef, @convention(block) (LXRef, UnsafeMutablePointer<Bool>) -> Void) -> Void).self) let copyString = sym("LXEntryCopyString", as: (@convention(c) (LXRef) -> Unmanaged<CFString>?).self) let metaFlags = sym("LXEntryGetMetaFlags", as: (@convention(c) (LXRef) -> UInt32).self) let category = sym("LXEntryGetCategoryFlags", as: (@convention(c) (LXRef) -> UInt32).self) let localeKey = dlsym(lexicon, "kLXLexiconLocaleKey")!.load(as: CFString.self) let locale = CommandLine.arguments.dropFirst().first ?? "en_US" guard let lex = create([localeKey: locale] as CFDictionary, nil), let root = rootCursor(lex) else { fatalError("could not open lexicon for (locale)") let tiers: [(UInt32, String)] = [(0x0080_0000, "vulgar"), (0x0100_0000, "offensive"), (0x0200_0000, "sensitive")] enumerate(root) { entry, _ in let meta = metaFlags(entry) guard let tier = tiers.first(where: { meta & $0.0 != 0 })?.1, let word = copyString(entry)?.takeRetainedValue() else { return } print(tier, String(format: "%05x", category(entry)), word, separator: "\t")
两个程序都是只读的。它们打开的是键盘所用的同一本词典,并把它打印出来。
Apple 没有为这些标志提供文档,所以下面每个含义都是推断出来的:我按位对词分组,然后阅读样本,直到每一组的主题都变得明显。系统代码在其中一项上提供了线索。一个名为 offensiveVulgarSensitive 的枚举 case 就紧挨着粗俗词相关的代码,而三个相邻的 meta 位正好与它对应。
在 en_US 中,一个词最多只属于一个层级:它们从不重叠。在 199,019 个条目中,有 4,271 个带有一个层级。
大致来说:vulgar 是脏话和性俚语,offensive 是针对特定人群的蔑称,而 sensitive 是
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。