micahflee

尽管营销存在误导,特朗普官员使用的以色列公司 TeleMessage 可以访问明文聊天记录

TeleMessage, used by Trump officials, can access plaintext chat logs 基于我对源代码的分析,TeleMessage 如何存档 Signal 消息的示意图

尽管他们的营销存在误导,但为特朗普高级官员提供修改版 Signal 的公司 TeleMessage 可以访问其客户的明文聊天记录。

在这篇文章中,我将概述 TeleMessage 伪造的 Signal 应用(名为 TM SGNL)的工作原理以及它为何如此不安全。然后,我将深入分析 TM SGNL 的 Android 应用的源代码,以及是什么让我得出 TeleMessage 可以访问明文聊天记录的结论。最后,我将通过尚未公布的关于 TeleMessage 被 黑客攻击 的细节来支持我的分析。

首先,这是一个简短的事件时间线。

这些毁灭性的黑客攻击证实了我在这篇文章中分享的分析:TeleMessage 的服务器——托管在公共 AWS 云上,由一家 以色列公司 运营,该公司由一名前 IDF 特工领导——可以明文访问他们正在存档的 Signal 聊天记录(以及 Telegram、WeChat 和 WhatsApp 的聊天记录)。

如果您觉得这有趣,订阅 ,将这些帖子直接通过电子邮件发送到您的收件箱。如果您想支持我的工作,请考虑成为付费支持者。

目录

概述

TeleMessage 制作了流行消息应用(包括 Signal、WhatsApp、Telegram 和 WeChat)的修改版本。修改后的 Signal 应用几乎与正版 Signal 完全相同,除了它还将每条消息的副本存档到 TeleMessage 客户确定的目的地。(据推测,WhatsApp、Telegram 和 WeChat 的工作方式相同,但我只分析了 TM SGNL 的 Android 源代码。)

TM SGNL 应用

TM SGNL 可与 Signal 互操作。当 TM SGNL 用户注册一个新帐户时,他们正在使用官方 Signal 服务器注册。TM SGNL 用户可以向 Signal 用户发送消息,反之亦然。如果您是 Signal 用户,您无法知道您何时与 TM SGNL 用户交谈,因为这些应用几乎相同并且使用相同的基础设施。

这就是 Mike Waltz 如何意外地将 The Atlantic 主编 Jeffrey Goldberg 添加到一个群聊中,他们在其中讨论了 轰炸一栋满是平民的公寓楼:Waltz 可能正在使用 TM SGNL,而 Goldberg 可能正在使用 Signal。

请注意,我们不知道特朗普官员使用 TM SGNL 有多长时间了。他们有可能从 1 月 20 日特朗普就职典礼前后就开始使用它了。如果是这种情况,那么 Waltz 邀请 Goldberg 加入的 Signal 群组的聊天记录将被 TeleMessage 收集。但也有可能他们只是在 Signalgate 之后 才开始使用 TM SGNL,也许是为了尝试开始遵守记录保存法。到目前为止,我们还不知道时间线。搞清楚这一点对于理解他们已经违反了哪些法律会很有帮助。

无论如何,在 Mike Waltz 的手机上——很可能还有 Pete Hegseth、Marco Rubio、Tulsi Gabbard、JD Vance 和许多其他人,甚至可能包括 Donald Trump——TM SGNL 应用基本上是这样工作的:

TeleMessage 的存档服务器

正如我在 我的初始 OSINT 分析 中讨论的那样,根据这份文档 PDF,使用 TeleMessage 的组织的管理员设置存档计划并将用户分配给它们。

TM SNGL 存档计划的工作方式,在文档 PDF 的第 22 页

每个存档计划都有一个源消息应用(如 TM SGNL)和一个目标,该目标由 TeleMessage 客户控制。目标可以包括 Microsoft 365、电子邮件服务器 (SMTP) 或文件服务器 (SFTP)。管理员将 TeleMessage 用户(如 Mike Waltz)分配给存档计划,该计划决定了他们的聊天记录将被存档到哪里。

一旦 TM SGNL 应用将聊天记录发送到存档服务器,存档服务器应该执行以下操作:它会查找发送聊天记录的用户,然后查找该用户的存档计划,然后通过 SMTP 或 SFTP 将消息转发到存档计划中定义的目标,并且据推测(但谁又能确定)会从存档服务器中删除聊天记录。

这是整个系统的工作方式的示意图:

基于我对源代码的分析,TeleMessage 如何存档 Signal 消息的示意图

Microsoft 365

存档服务器似乎直接连接到 SMTP 和 SFTP 目标以推送聊天记录。但是,根据 Microsoft 的文档,Microsoft 365 的工作方式相反:Microsoft 365 登录到存档服务器并每天拉取一次聊天记录。他们的文档指出:

您在 Microsoft Purview 门户中创建的 Signal Archiver 连接器每天连接到 TeleMessage 站点,并将过去 24 小时的电子邮件消息传输到 Microsoft Cloud 中一个安全的 Azure Storage 区域。

他们甚至发布了自己的示意图来解释它的工作原理:

使用连接器在 Microsoft 365 中存档 Signal 通信数据,来自 Microsoft 的文档

为什么这如此不安全

Signal 是端到端加密消息应用的金标准。

消息在端点之间加密——无论是运行 Signal 的手机、运行 Signal Desktop 的计算机,甚至是运行 TM SGNL 的手机。Signal 服务器以及任何互联网窃听者都无法访问聊天记录。

但是,一旦它们到达端点,它们就会以明文形式存在(如果不是,您将无法阅读您的文本)。此时,它们受到设备上的各种形式的磁盘加密的保护。这就是 Signal 消息有时最终成为法庭记录中的证据的原因:安装了 Signal 的手机或笔记本电脑在消息已被解密后被搜索。

TM SGNL 完全破坏了这种安全性。TM SGNL 应用与最终存档目的地之间的通信 不是端到端加密的

TeleMessage 在其营销材料中对此撒谎,声称 TM SGNL 支持“从手机到公司档案库的端到端加密”。

这是他们网站的 存档版本 的屏幕截图(因为他们已经删除了所有内容):

TeleMessage 错误地声称 TM SGNL 支持“从手机到公司档案库的端到端加密”

相反,TM SGNL 将聊天记录的明文、已经解密的版本发送到存档服务器。(有时可能涉及一些加密,我在底部的“来自黑客攻击的佐证”部分中介绍了这一点。)

然后,存档服务器将这些转发到目的地。

无论如何,在 TM SGNL 解密消息后,它会将明文聊天记录发送到 TeleMessage 的存档服务器。此时,很多人可能可以访问聊天记录。

存档服务器托管在弗吉尼亚州北部的数据中心内的公共 AWS 云中。这不是存储机密信息的批准地点,并且潜在的恶意 AWS 员工可能可以访问。该服务器对公众开放——世界上的任何人都可以向其发送 HTTP 请求,以尝试在响应中获取聊天记录。周六,其中一个人这样做了

我说服务器“是”托管在 AWS 中,是因为在我们发布有关黑客攻击的故事后不久,TeleMessage 删除了 他们的存档服务器。截至撰写本文时,他们的存档服务器已离线。

TeleMessage 也可能与以色列情报部门共享聊天记录。

TeleMessage 是一家以色列公司。它由 Guy Levit 于 1999 年创立,同一年他离开了他在以色列国防军的工作,在那里他“担任 IDF 情报部门精英技术部门之一的规划和开发负责人”,根据他在 TeleMessage 网站上的个人简介(他们此后已从他们的网站上删除了所有内容)。

Levit 和他公司中的其他人可以访问存档服务器及其包含的所有聊天记录。我不知道以色列版本的《爱国者法案》是什么样的,但我知道 TeleMessage 添加一段代码将所有内容的副本转发给以色列情报部门是很容易的——远比向 Signal 添加代码将其副本转发给自己更容易。

需要明确的是,目前还没有证据表明 TeleMessage 正在与以色列政府共享聊天记录。但他们设计他们的存档系统 不是端到端加密的 这一事实,以及他们对此撒谎的事实,是一个很大的危险信号。

这是 Mike Waltz 使用的应用。虽然我们不太清楚时间线,但参加 19 人 Signalgate 群组的每个人(他们在其中讨论了轰炸也门)也可能正在使用这款应用。其中包括 JD Vance、John Ratcliffe、Marco Rubio、Pete Hegseth、Stephen Miller、Tulsi Gabbard 等人。以色列情报部门很有可能一直在阅读特朗普专制政府中最有权势成员的内部聊天记录。

注册 micahflee

大家好,我是 Micah。我是一名程序员、记者,我帮助人们保持隐私和安全。

订阅

已发送电子邮件!检查您的收件箱以完成注册。

没有垃圾邮件。随时取消订阅。

TM SGNL Android 源代码分析

我只分析了 TM SGNL Android 源代码,而不是 iPhone 源代码。我假设 iPhone 应用的工作方式与 Android 应用相同,并且我从分析 Android 应用中学到的任何东西也适用于 iPhone 应用。

我专注于 Android,因为逆向工程 Android 应用比 iPhone 应用容易得多。有许多优秀的工具可用于分析 Android 应用,例如 apktoolapkeep,甚至官方 Android Studio。此外,Android 应用使用 Java 和 Kotlin(它们都编译为 Java 字节码)进行编程,并且很容易反编译 Java 字节码,将其转换回人类可读的源代码,从而使其更容易使用。

此外,虽然 TeleMessage 发布了链接 到 ZIP 文件,声称这些文件包含 Android 和 iOS 源代码,但 iOS 版本 似乎 实际上是 Signal for iOS 本身的源代码,没有添加任何额外的 TeleMessage 代码。

💡

正如您在源代码附带的 LICENSE 文件中看到的那样,TM SGNL 在 GNU Affero General Public License v3.0 下获得许可。这使我(以及其他所有人)拥有无限的权利来访问、分析、逆向工程以及几乎做我们希望对代码做的任何其他事情,只要我们在同一许可证下发布任何衍生作品即可。

到目前为止,我只有几天时间来查看这些代码——并且我暂停了对它的分析工作,以便我可以打破有关 TeleMessage 被黑客攻击的故事——所以还有很多我仍然没有完全理解。我也可能弄错了——如果情况属实,请 告诉我。在任何情况下,我都在这里展示我的所有工作,以便其他人可以重现它并在其基础上构建。

反编译共享库

TM SGNL 源代码与 Signal for Android 源代码 大部分相同,但也有一些差异。大部分新代码都可以在以下三个位置找到:

app/src/tm/java/org/ 文件夹包含 TeleMessage 代码。

app/src/tm/java/org/ 中的 TeleMessage 代码

app/src/main/java/org/tm/archive/ 文件夹也包含 TeleMessage 代码:

app/src/main/java/org/tm/archive/ 中更多的 TeleMessage 代码

最后,app/libs/ 文件夹包含 TeleMessage 的 Android 存档(.aar)格式的共享库。其中包括 androidcopysdk-signalauthenticatorsdk-signalcommon。共享库是可重用的代码片段,可以导入到不同的项目中。这些库似乎包含也可能在 TeleMessage 的其他 Android 应用(用于 WhatsApp、WeChat 和 Telegram)中共享的代码。Android 存档文件是压缩的 Java 字节码。

app/libs/ 中的共享库

逆向工程的第一步是将这些共享库转换为实际的人类可读(或至少是人类极客可读)的 Java 代码。

有各种本地工具可以做到这一点,但我发现使用在线服务来反编译 Android 应用是最简单的方法。我通过这个 Android 存档反编译器 运行了共享库的发布版本,它给了我带有反编译源代码的 zip 文件。

为了让其他人更容易理解,我在我的 TM-SGNL-Android 存储库中创建了一个名为 libs 的新 git 分支,并将反编译的共享库代码提交到了那里。我也将 libs 分支设为默认分支。您现在可以在 libs 分支的 apps/libs/ 文件夹中访问所有共享库代码:

app/libs/ 文件夹,其中包含充满人类可读 Java 源代码的文件夹

现在我们都可以访问相同的源代码,我将尝试简要介绍我认为与我得出结论相关的代码片段。

重要组件

以下是代码库中的一些重要组件:

现在我将更详细地介绍这些组件,以及它们如何联系在一起。

SignalDatabase

这是 SignalDatabase 类定义 的开头。正如您所看到的,它定义了将用于存储不同类型的 Signal 数据的表:

open class SignalDatabase(private val context: Application, databaseSecret: DatabaseSecret, attachmentSecret: AttachmentSecret) :
 SQLiteOpenHelper(
  context,
  DATABASE_NAME,
  databaseSecret.asString(),
  null,
  SignalDatabaseMigrations.DATABASE_VERSION,
  0,
  SqlCipherErrorHandler(DATABASE_NAME),
  SqlCipherDatabaseHook(),
  true
 ),
 SignalDatabaseOpenHelper, IDatabase<Long> { // TM_SA implement IDatabase
 val messageTable: MessageTable = TeleMessageTable(context, this) // TM_SA TeleMessageTable
 val attachmentTable: AttachmentTable = TeleAttachmentTable(context, this, attachmentSecret) // TM_SA TeleAttachmentTable
 val mediaTable: MediaTable = MediaTable(context, this)
 val threadTable: ThreadTable = ThreadTable(context, this)
 val identityTable: IdentityTable = IdentityTable(context, this)
 val draftTable: DraftTable = DraftTable(context, this)
 val groupTable: GroupTable = GroupTable(context, this)
 val recipientTable: RecipientTable = RecipientTable(context, this)
 val groupReceiptTable: GroupReceiptTable = GroupReceiptTable(context, this)
 val preKeyDatabase: OneTimePreKeyTable = OneTimePreKeyTable(context, this)
 val signedPreKeyTable: SignedPreKeyTable = SignedPreKeyTable(context, this)
 val sessionTable: SessionTable = SessionTable(context, this)
 val senderKeyTable: SenderKeyTable = SenderKeyTable(context, this)
 val senderKeySharedTable: SenderKeySharedTable = SenderKeySharedTable(context, this)
 val pendingRetryReceiptTable: PendingRetryReceiptTable = PendingRetryReceiptTable(context, this)
 val searchTable: SearchTable = SearchTable(context, this)
 val stickerTable: StickerTable = StickerTable(context, this, attachmentSecret)
 val storageIdDatabase: UnknownStorageIdTable = UnknownStorageIdTable(context, this)
 val remappedRecordTables: RemappedRecordTables = RemappedRecordTables(context, this)
 val mentionTable: MentionTable = MentionTable(context, this)
 val paymentTable: PaymentTable = PaymentTable(context, this)
 val chatColorsTable: ChatColorsTable = ChatColorsTable(context, this)
 val emojiSearchTable: EmojiSearchTable = EmojiSearchTable(context, this)
 val messageSendLogTables: MessageSendLogTables = MessageSendLogTables(context, this)
 val avatarPickerDatabase: AvatarPickerDatabase = AvatarPickerDatabase(context, this)
 val reactionTable: ReactionTable = ReactionTable(context, this)
 val notificationProfileDatabase: NotificationProfileDatabase = NotificationProfileDatabase(context, this)
 val donationReceiptTable: DonationReceiptTable = DonationReceiptTable(context, this)
 val distributionListTables: DistributionListTables = DistributionListTables(context, this)
 val storySendTable: StorySendTable = StorySendTable(context, this)
 val cdsTable: CdsTable = CdsTable(context, this)
 val remoteMegaphoneTable: RemoteMegaphoneTable = RemoteMegaphoneTable(context, this)
 val pendingPniSignatureMessageTable: PendingPniSignatureMessageTable = PendingPniSignatureMessageTable(context, this)
 val callTable: CallTable = CallTable(context, this)
 val kyberPreKeyTable: KyberPreKeyTable = KyberPreKeyTable(context, this)
 val callLinkTable: CallLinkTable = CallLinkTable(context, this)

如果这些表不存在,onCreate (https://github.com/micahflee/TM-SGNL-Android/blob/libs/app/src/main/java/org/tm/archive/database/SignalDatabase.kt#L85-L160) 方法会创建这些表。

我需要深入研究代码以查看确切的发生位置,但我相信每当 TM SGNL 应用接收或发送 Signal 消息时,它们都会被插入到这个数据库中。

DataGrabber

这是 DataGrabber 类中的 setMessage (https://github.com/micahflee/TM-SGNL-Android/blob/libs/app/libs/androidcopysdk-signal/sources/com/tm/androidcopysdk/DataGrabber.java#L2035-L2047) 方法:

public synchronized void setMessage(MessageDetailsArchive messageDetailsArchive) {
  Log.d("info", "setTextMessage start");
  String lasttime = getLastMessageByType(this.mContext, MessageType.SMS);
  Long.valueOf(lasttime).longValue();
  ContentValues contentValues = prepareValues(messageDetailsArchive.getProtocol(), messageDetailsArchive.getToPhonesArray(), messageDetailsArchive.getFromPhoneNumber(), messageDetailsArchive.getBody(), messageDetailsArchive.getId(), messageDetailsArchive.getDate(), messageDetailsArchive.getSubject(), messageDetailsArchive.getMyNumber(), messageDetailsArchive.getChatMode(), messageDetailsArchive.getChatName(), messageDetailsArchive.getChatId(), messageDetailsArchive.getFromName(), messageDetailsArchive.getFromValue(), messageDetailsArchive.getToNameArray(), messageDetailsArchive.getToPhoneNumberArrayValue(), messageDetailsArchive.getMessageType());
  contentValues.put("type", MessageType.SMS.name());
  Uri path = this.mContext.getContentResolver().insert(MessageContentProvider.CONTENT_URI, contentValues);
  Log.d("grabber", "insert message and id and time , id: " + path.getPath() + " " + messageDetailsArchive.getDate() + " " + messageDetailsArchive.getId() + " sub = " + messageDetailsArchive.getSubject());
  SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(this.mContext).edit();
  editor.putString(MessageType.SMS.name() + DATE_OF_MESSAGE, messageDetailsArchive.getDate()).apply();
  Log.d("info", "setTextMessage end");
  CommonUtils.startBackupService(this.mContext);
}

请注意,即使我们正在存储 Signal 消息,这一行代码也明确地将消息类型设置为 SMS,这一点稍后会很重要:

contentValues.put("type", MessageType.SMS.name());

这行代码将消息插入到暂存数据库中:

Uri path = this.mContext.getContentResolver().insert(MessageContentProvider.CONTENT_URI, contentValues);

这行代码触发 SyncAdapter 运行:

CommonUtils.startBackupService(this.mContext);

ArchiveMessagesProcessor

这是 messageStoreObserver 中的一个处理器。每当 SignalDatabase 中的消息被创建/修改时,都会调用 processAfterMessageStateChanged (https://github.com/micahflee/TM-SGNL-Android/blob/libs/app/libs/androidcopysdk-signal/sources/com/tm/androidcopysdk/device/ArchiveMessagesProcessor.java#L116-L147) 方法:

@Override // com.tm.androidcopysdk.device.MessageStoreProcessor
protected void processAfterMessageStateChanged(@NotNull ArchiveMessage message, @Nullable ArchiveMessage existing) {
  Intrinsics.checkNotNullParameter(message, "message");
  String cleanAccountPhoneNumber = message.getCleanAccountPhoneNumber();
  if (cleanAccountPhoneNumber == null || cleanAccountPhoneNumber.length() == 0) {
    Log.d(getTag(), "ignoring archive message " + message.getArchiveId() + ", account phone number is missing.");
    return;
  }
  boolean isNewEdit = message.isNewEdit(existing);
  if (!isArchivingSupported(message, isNewEdit) || message.getTimestamp().getValue() <= 0) {
    Log.