Exploring Signal: An Unprecedented Look Under the Hood of a Production App
One of the most popular secure messaging apps currently on the market, Signal, stands apart from its competitors with a unique take on development. It’s open source, the subject of numerous independent audits, and ad-free. Signal is widely regarded as a role model showing how secure messaging ought to look.
Today, Signal is wholly owned and maintained by the Signal Foundation, a non-profit based in the US. The organization was founded by Moxie Marlinspike and Brian Acton (co-founder of WhatsApp) in 2018 and has seen widespread use in the mobile secure messaging space.
To any security researcher, Signal provides a fairly unprecedented look under the hood of a production app that is installed and used by millions of devices around the globe whether on iOS, Android, or Desktop (Windows, macOS, Linux). I contend that Signal is an excellent learning aide for anyone looking to:
- Improve their understanding of how secure messaging applications actually work – helpful in the areas of reverse engineering, and forensic tool validation.
- Improve their understanding of Git/GitHub, specifically how to dig into the history of a specific change or even line of code.
The latter point won’t come as a surprise to you if you’ve read my recent personal blog on Git/Github for forensic examiners. I’m an advocate for any examiner wanting to become more familiar with programming, reverse engineering, and source control platforms like Git.
Today we’re going to investigate a recent change to the Signal app on iOS using a combination of the release notes (iOS App Store) and the Signal-iOS repository on Github.
From the version history, it looks as though in version 3.6.1 released on March 6, there was a change to ‘draft message previews in the conversation list’. Cool – let’s see what potential info we can find about this change on Github.
From the Signal-iOS repo homepage, we’ll begin by pulling up the chronological list of commits to the repo.
To do this, click on the commits label (or via this link).
There is often additional insight about code changes that can be gleaned just by reading through commit messages. From our version history, we know that the commit in question must have landed before March 6th, so I started looking from prior to that date. It wasn’t long before I found this change message from March 2, 2020 which seems like a prime suspect:
Let’s click on that change and for clarity, navigate to ‘Split’ view (or click here) to see all of the files changed on this commit. We can see a change summary at the top:
Okay, cool. The first changed file listed is AttachmentKeyboard.swift, and in this file, the only thing that’s changed is inside a comment. Nothing significant here, but it’s worth noting that the left pane is the old (before the change), and the right pane is the new (after the change). Red highlighting indicates deleted, and green highlighting indicates added.
Next I’ll skip ahead to the Localizable.strings file, where we can see that 3 new lines have been added. Best of all, there’s a comment (line 1244) that gives us a bit of an explanation — this string is a prefix indicating that a message preview is a draft. Let’s keep the variable name HOME_VIEW_DRAFT_PREFIX in the back of our mind as well.
So I’d like to point out, even if you randomly found this commit and were trying to figure out what it did, between the commit message and this strings file, we already have a pretty solid foundation for understanding what might’ve changed.
But we’ve still got more digging to do – the ConversationListCell.m file, which has 14 of the 22 changes, comes next.
I’ve marked up the screenshot a bit to try and fill in some information. From this view we can see that in ConversationListCell.m, inside the function attributedSnippetForThread:
Prior to this change, displayableText was set to thread.lastMessageText. This is consistent with the prior behavior we might expect from Signal. (Note: if you read some of the unchanged code above this, there are other scenarios such as if the conversation is Muted or Blocked, but we won’t be going into those scenarios in this article).
In the new world, we now check to see if thread.draftText has anything in it (thread.draftText.length > 0) and if so, as long as we don’t have any unread messages (!hasUnreadMessages), we use that for the snippet instead. If there is a draft, we also attach our prefix, which should be “Draft: “ (HOME_VIEW_DRAFT_PREFIX) which is presented in italics (NSFontAttributeName : self.snippetFont.ows_italic). Remember, if there are unread messages (!hasUnreadMessages), this no longer applies and the unread message takes precedent.
The last changed file is SignalMessaging/ViewModels/ThreadViewModel.swift where a new property was added called draftText and value assigned, which we can see being used in our ConversationListCell.m above.
Alright! So we’ve made our way through all 4 changed files, and now we are ready to make an educated guess:
Before this change: the “preview message” or snippet is generally based on the last message text (lastMessageText). If there are unread messages, it’s also bolded.
After this change: if there happens to be a “draft” (unsent message), the draft is used as the conversation’s preview snippet and is prefixed by “Draft: “ unless we happen to have an unread message waiting in which case the old behavior applies.
As stated, thus far this is only a theory. We can’t say for sure this code is ever executed. So what do we do? One way is to bust out a test phone with an older version of Signal on it, which I happen to have on hand! We’ll start by writing a draft message, then go back to the conversation list.
Great, so now we have an unsent message – ‘hi’. Let’s look at it in the two different versions of Signal:
So as predicted from our experimentation thus far, the message now appears with an italicized prefix – “Draft: ”.
A rather interesting caveat here: despite the draft message being written today (March 22, 2020) the date shown on the conversation remains 2019-11-29– yet another visual nuance with some potential forensic significance.
Now we have one more test to run — if I send our test device a message back, without clearing the draft, we can check if the unread message overrides the draft portion of the snippet, as expected:
The last thing we will do is review the history of code changes to ensure this particular code hasn’t changed again since March 2, 2020. To do this, we use git blame. You can activate this by clicking a line and the [...] button that appears beside it and choosing “View git blame”.
(If you’d like to navigate directly to the blame page, you can follow this link here.)
Git blame allows us to see the detailed history of a file over a period of time. It allows us to see how a file has evolved over time, and even links us to the relevant commits.
We can see that the changes from our “Show drafts on the conversation list” commit are surrounded by edits from 2 years ago! If there were any more recent changes to the code between line 418 to 431, we would see it on here.
Note that the more recent updates are indicated with a deeper orange color on the center vertical border, according to this legend:
Great, but most apps aren’t open source, are these skills likely to be useful elsewhere?
You bet they can! The process we’ve worked through in this blog today could conceivably work for just about any app – instead of Github, we use reverse engineering tools to perform static and dynamic analysis. There we might deal with challenges like code obfuscation, encrypted binaries, anti-debugging methods, and other fun mechanics. But this philosophy of “thinking like a developer” and beginning to understand even a subtle nuance of how an app works, absolutely applies whether you are looking at a totally open source app like Signal or diving into the machine code of a closed source app.
I’m never going to learn Swift/Objective C and Java – all I’ve got time for is a little Python!
You might be surprised to learn that there are a lot of full-time reverse engineers out there who don’t know how to code at all. I first heard this sentiment from folks on SANS FOR610: Reverse Engineering Malware. At the time, I really struggled to understand how such a thing could be possible. But some of the non-coders ended up being some of the most skilled RE folks in the room!
The point is, you don’t need to be an expert with 100% fluency in a language to be able to piece together a theory of how it works. The more exposure you get — to any language — the better equipped you will be to understand how an application might work.
AXIOM and Signal iOS
To finish off, I’m happy to also note that over the last two releases of AXIOM we have updated our support for Signal iOS to work with the latest versions of the app, including the new GRDB. You will still need to obtain a full filesystem and keychain dump (such as with GrayKey), but you can once again bring in content for parsing in the tool.
If you’re not already using AXIOM, you can request a free 30-day trial today.
Feel free to reach out to me at email@example.com or @forensicmike1 on Twitter if you have any feedback, or if you’d like to see more reverse engineering content on this blog!