At Blackhat 2016 Jean-Philippe Aumasson and Markus Vervier were a bit
bored and decided to take a peek at the
Signal source code. This actually evolved
into a longer hunt for bugs in the high profile messenger recommended by
Snowden. Since two of the bugs for the Java reference implementation of
Signal have been publicly fixed after our disclosure, we think we should
give a little description about what we found.
We checked common pitfalls of Java, Objective-C and C/C++ code and
common attack vectors, by reviewing the Signal code base (Signal
protocol libraries, mobile clients, service). We also reviewed the
general architecture of Signal and its attack surface.
We found several issues of varying impact, about which we will blog in
the coming months, starting with this post.
MAC Validation Bypass For Attachments
Signal attachments are encrypted and authenticated in order to prevent a
third party from reading or changing them. This also includes the Signal
maintainers. The encrypt-then-MAC approach used by Signal is the most
secure way to do this, compared to MAC-then-encrypt (as found in TLS) or
encrypt-and-MAC (as found in SSH).
If a message is sent with an attachment, this attachment is downloaded
separately from a server located on AWS, such as
https://whispersystems-textsecure-attachments.s3.amazonaws.com/.
Attachments are encrypted (by the sender) using AES-128-CBC with PKCS7
padding, and authenticated with HMAC-SHA-256,
also using a 128-bit key.
After downloading the attachment via HTTPS and storing it on the Android
storage, the file’s MAC is checked by the following code from the Signal
Service, in libsignal-service-java/java/src/main/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherInputStream.java:
As seen above remainingData is of type int and calculated from the
length of the file subtracted by the MAC length. Since file.length()
will return a value of type long and files may be larger than
Integer.MAX_VALUE, remainingData will wrap around.
Unlike C(++), Java is “memory safe” and therefore this will not lead to
any classical memory corruption condition. Instead we can use this
overflow to subvert the program logic. Now if the file is of size 4GB +
1 byte + X, the value will wrap around and remainingData will be set
to X.
Here is the trick: Signal stores all attachments on AWS S3, fetches
them over HTTPS and uses the system certificate store to check the
server’s cert (note that the Signal server on S3 serves a wildcard
certificate for *.s3.amazonaws.com). An entity with access to Amazon
S3 or having access to any of the CA certificates commonly found in
trust stores on Android or other systems, could modify attachments in
the following way:
Watch for a request to fetch an attachment.
Fetch the original attachment of size X.
Pad the attachment with data of size 4GB + 1byte, resulting in a
total size of X + 4GB + 1.
As described above this will result in X bytes being checked with
verifyMAC(), for which the original MAC is valid. Therefore we can add
arbitrary data to the file without the MAC check failing!
Note that a MITM attacker does not really need to send more than 4GB of
data over any network connection: If we use HTTP stream compression with
gzip we can create 4GB files which compress down to 4.5MB.
A PoC was created that works as a proxy for the Signal attachment
storage server, padding the content as described above:
Looking into Android logcat we now see the following Exception:
We just successfully bypassed the MAC check :-)
After checking the MAC, the constructor of the class
AttachmentCipherInputStream will create an instance of class
javax.crypto.Cipher:
Reaching this state we can now make the cipher decrypt the ciphertext
of our choice. Besides the obvious problem that such large attachments
are decompressed and stored on the Android storage, quickly using up all
space, being able to supply arbitrary ciphertext as a suffix may allow
padding oracle attacks against AES-CBC with PKCS7 (a.k.a. PKCS5)
padding. This would require a communication channel back to the attacker
that indicates failed padding or successful decryption. We will discuss
if this is really possible and feasible in upcoming writeups.
Open Whisper Systems rapidly fixed this issue after our disclosure, with
to this commit.
Arithmetic Underflow Via Malformed RTP Packets
The CallAudioManager class defined in the Signal Android client at
src/org/thoughtcrime/redphone/audio/CallAudioManager.java is using
native code whose source is in jni/redphone/CallAudioManager.cpp.
In line 129 of callAudioManager.cpp, the following code serves to
receive audio data into a buffer of 4096 bytes:
The function receive receives data into the buffer and will create a new
instance of class RtpPacket defined in jni/redphone/RtpPacket.cpp:
When packetLen is smaller than sizeof(RtpHeader) the value of
payloadLen set in the class constructor will become negative:
This results in payloadLen being incorrectly stored as negative value.
Offending values to for packetLen are in the range of [0,
sizeof(RtpHeader] which is between 0 and 12.
So what does this actually mean? After constructing such an RtpPacket with a short size, the
srtpStream.decrypt method is called on this packet:
As seen above, packet->getSequenceNumber() is called and reads data
out of bounds which is passed to sequenceCounter.convertNext(). Then
decrypt is called:
The integer underflow at 4. will cause a negative value to be passed on
to the HMAC function:
Since the parameter n is of type size_t, the value will be casted into a
very large value near LONG_MAX (on 64bit architectures). This will for sure cause an out of
bounds read, hitting unmapped memory addresses and therefore crashing
the Signal application.
We currently consider this issue as non-exploitable in sense of creating
a write primitive because of the second arithmetic underflow happening
at 4. If the subsequent crash in HMAC would be prevented, things might be
different depending on the creativity and skill. However there is still an impact now, as it is possible to remotely crash Signal.
Also if this code is used in other applications, other code paths might lead
to exploitable conditions. Therefore we collected a list of all the
consequences of this underflow
Methods RtpPacket::getPayload(), RtpPacket::getSsrc() and
RtpPacket::getTimestamp() will return out of bounds memory on the heap
behind *packet`:
Method RtpPacket::setTimestamp(uint32_t timestamp) will write data out of bounds on the heap:
Method RtpPacket::getPayloadLen() will return a large positive number:
Method RtpPacket::getSerializedPacketLen() will actually return the
correct size of data received (due to a reverse integer overflow again):
Please check your code in case you are using the RtpPacket class.
This issue has been fixed in the Signal Android client thanks to this
commit.
Timeline
2016-08-03 Start of review
2016-09-13 Disclosure of initial findings to vendor
2016-09-13 Vendor releases and publishes two fixes