
博主逆向苹果私有的脏话词库,发现其词汇分类机制
来源标题:I dumped Apple’s word list
开发者通过逆向 Lexicon.framework 发现苹果系统中存在一个脏话词库,用于记录和分类粗俗词汇。该词库由 LXEntry 的 32 位标志位定义词条分类,并通过 _KSUserWordsSynchroniser 在设备间同步。作者尝试手动触发脏话记录未能成功,具体触发条件仍不明确。
以下正文由 AI 翻译自 Hacker News(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
一个 _KSUserWordsSynchroniser sharedAcrossLanguagesVulgarWordUsageValue 符号表明,它会像你其他学到的词一样在设备之间同步。我试着往里填充。用 agent-desktop 打开一个新的 TextEdit 文档,输入 well fuck this shit, damn it.,先作为文本插入,再逐个按键输入。什么都没变:没有新行,没有日志记录,文件的修改日期仍停留在 2024 年。我不知道为什么,也搞不明白。也许 Mac 根本不在这里记录任何东西,也许它只在某些 app 或设置下才记录。如果你的数据库里有行,告诉我你是怎么做到的。
那本该是篇短文。但数据库只统计那些已被认定为粗俗的词。那么,是谁来决定哪些词算粗俗呢?
在 dyld 共享缓存里搜索 "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 的 enum 用例就紧挨着粗俗词的代码,而三个相邻的 meta 位正好与之对应。在 en_US 中,一个词最多属于一个层级:它们从不重叠。在 199,019 个条目中,有 4,271 个带有一个层级。大致来说:vulgar 是脏话和性俚语,offensive 是针对群体的蔑称,而 sensitive 是
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。