From 36b27e733c83e02ac54d7a6c1aa0a43938d1fc1f Mon Sep 17 00:00:00 2001
From: cyfraeviolae
- You can test your ciphertext with Go. Run the following in a shell
- and then try opening
- Recall that the AES-GCM ciphertext is computed as the XOR of the
- keystream and the message. One can modify the bits of the
- ciphertext arbitrarily to effect the same change in the decrypted
- plaintext.
+ This attack was shown by Yevgeniy Dodis, Paul Grubbs, Thomas Ristenpart, and Joanne Woodage
+ in Fast Message Franking: From Invisible Salamanders to Encryptment.
+
+ First, we will describe a general strategy to create a ciphertext that yields the same MAC
+ with two different keys. Then we will show how to construct a ciphertext that yields
+ meaningful results when decrypted with those two keys.
+
+ Consider arbitrary keys \(k_1, k_2\), nonce \(n\), and ciphertext
+ \(c\) (additional associated data can be accounted for in a
+ straightforward manner). \(k_1, k_2\) are associated
+ with authentication keys \(h_1, h_2\) and blinds \(s_1, s_2\), respectively.
- Where certain bits of the plaintext are already known, the attacker
- can fully determine the same bits of the forged plaintext. If
- nonces are reused, the keystream will be identical, allowing us to
- recover plaintext via
-
- crib dragging, which makes this attack particularly effective:
- \[
- c' = c \oplus m \oplus m'.
- \]
+ Given a ciphertext of three blocks, we will attempt to add a fourth ciphertext block \(c_3\), set the MACs equal to each other,
+ and solve for \(c_3\). Remember that in \(\mathbb{F}_{2^{128}}\), addition is the same as subtraction.
- However, we still need to compute a new MAC over the forged ciphertext.
- Simplifying for a ciphertext \(c\) of two blocks and no additional
- authenticated data, the GMAC MAC is computed as
+ For the resulting ciphertext of four blocks, the MACs for each key are computed as
\[
- mac = s + \vert c\vert h + c_1h^2 + c_0h^3,
+ mac_{1} = s_1 + \vert c\vert{}h_1 + c_3h_1^2 + c_2h_1^3 + c_1h_1^4 + c_0h_1^5
+ \]
+ \[
+ mac_{2} = s_2 + \vert c\vert{}h_2 + c_3h_2^2 + c_2h_2^3 + c_1h_2^4 + c_0h_2^5
\]
where \(s\) is a constant depending on the AES-GCM key and the nonce, and \(h\)
- is the authentication key depending only on the AES-GCM key.
-
- If we intercept a second ciphertext \(c'\) encrypted under the same key and nonce,
- we can compute
+ is the authentication key depending only on the AES-GCM key. We can now set the MACs equal to each other and solve for \(c_3\).
\[
- mac + mac' = (s + s') + (len + len')h + (c_1 + c'_1)h^2 + (c_0+c'_0)h^3,
+ s_1 + \vert c\vert{}h_1 + c_3h_1^2 + c_2h_1^3 + c_1h_1^4 + c_0h_1^5 = s_2 + \vert c\vert{}h_2 + c_3h_2^2 + c_2h_2^3 + c_1h_2^4 + c_0h_2^5
\]
- Since \(s = s'\) and \(x+x=0\) in \(\mathbb{F}_{2^{128}}\), we are
- left with the polynomial equation
\[
- 0 = (mac + mac') + (len + len')h + (c_1 + c'_1)h^2 + (c_0+c'_0)h^3
+ c_3(h_1^2 + h_2^2) = (s_1 + s_2) + \vert c \vert (h_1 + h_2) + c_2(h_1^3+h_2^3) + c_1(h_1^4 + h_2^4) + c_0(h_1^5 + h_2^5)
\]
- where all variables are known other than \(h\). Thus, recovering \(h\)
- is a matter of finding the roots by factoring the
- polynomial.
-
- We plug \(h\) back into the first equation to recover \(s\), and we
- can forge the MAC for arbitary ciphertext under the same nonce.
- Note that there may be multiple possible monomial roots; in this
- case, one can check each possibility against the enemy.
-
- One can use SageMath to compute factors of a polynomial:
-
- However, the library powering this demonstration implements polynomial factoring over finite fields from scratch, which is an edifying exercise.
-
- We present advice for those who wish to implement polynomial factorization as well:
- first.jpg
and second.bmp
in an image viewer.
+ You can test your ciphertext with Go. Run the following in a shell,
+ then try opening first.jpg
and second.bmp
+ in an image viewer.
- Show testing code.
+ Test script.
TEMP="$(mktemp).go"
@@ -126,85 +127,114 @@ go run "$TEMP" 5c3cb198432b0903e58de9c9647bd241 4a4f5247454c424f52474553 polyglo
go run "$TEMP" df923ae8976230008a081d23205d7a4f 4a4f5247454c424f52474553 polyglot.enc > second.bmp
Attack outline.
-K = GF(2**128, name='x', modulus=x^128+x^7+x^2+x+1)
-x = K.gen()
-S = PolynomialRing(K, 'y')
-y = S.gen()
-p = (1)*y^4 + (x^7)*y^3 + (x^9 + x^4 + 1)*y^2 + (x^12 + x^2)*y + (x^10 + x^5)
-for factor, _ in p.factor():
- if factor.degree() == 1:
- print('Authentication key:', factor - y)
-
-
+ \[
+ c_3 = \frac{(s_1 + s_2) + \vert c \vert (h_1 + h_2) + c_2(h_1^3+h_2^3) + c_1(h_1^4 + h_2^4) + c_0(h_1^5 + h_2^5)}{h_1^2 + h_2^2}
+ \]
+
+ Note that the choice to place the extra block in the final position was arbitrary. For the attack below we will instead need + to change the penultimate block rather than adding a block; the computation is similar. +
++ For the next phase, we construct a ciphertext that decrypts to a valid JPEG under one key and a valid BMP under another. + The basic strategy is to place the JPEG bytes and BMP bytes at different locations, carefully arranging it so + each parser will ignore the other data for the file. JPEG files can include comments, in which we will include the + BMP data. The BMP parser will stop reading as soon as the indicated length of the BMP has been read, after which + we will include the JPEG data. In each decrypted file, the data for the other image will be scrambled as we are using + a different key, but it will not matter as the garbage data will be in a location that is ignored by the image parser. +
+
+ All JPEG files start with the magic bytes ffd8
and end
+ with ffd9
. We will place a JPEG comment immediately
+ after the initial magic bytes, which is indicated by fffe
and is followed by a 2-byte big-endian encoding of the comment length.
+ All BMP files start with the magic bytes 424d
followed by a 4-byte little-endian encoding of the file length.
+
+ Because the JPEG comment has a maximum length, our BMP file will need to be less than ffff=65536
bytes.
+ Thus, we desire the JPEG file to start with ffd8 fffe ffff
and the BMP file to start with 424d wxyz 0000
,
+ where wxyz
is the actual length of our BMP file.
+
- Readers who wish to implement this attack themselves can try
- Cryptopals; specifically
- Set 8 Problem 62.
+ To make it easier, we will only require the first five bytes to match; we can modify ciphertext afterwards to satisfy the final
+ byte of the BMP header. The final byte of the JPEG header will be arbitrary, but this is the least significant part of the
+ comment length, so we will restrict our BMP file length to less than ff00=65280
bytes.
+
+ Since these must be in the same location at the start of the file, + we will need to brute-force search for two keys \(k_1, k_2\) and a nonce that + encrypt to the same ciphertext. The easiest way to do this is via a + birthday attack: fix an arbitrary nonce, then generate random keys + for both the JPEG header and the BMP header. Encrypt each and store + them in a lookup table. Repeat until two keys are found that + encrypt their respective headers to the same ciphertext bytes. +
++ To the ciphertext header, add the encryption of the BMP file + (without the header) under \(k_2\), then pad with arbitrary data to reach the end of the JPEG comment (this will be ignored by the BMP + parser, which has already finished reading the file). After the + JPEG comment is over, add the encryption of the JPEG file (without the header and final magic bytes) under + \(k_1\). +
+
+ Finally, add two more bytes of ciphertext that will make the final two
+ bytes of the JPEG file into the appropriate final magic bytes
+ ffd9
.
+
+ These ciphertexts do not have the same MAC yet. If we tried to use
+ the strategy outlined at the beginning where we add an extra block
+ at the end, the JPEG file would no longer end in ffd9
+ and thus would be invalid. Instead, we need to modify it to change
+ the penultimate block.
+
+ However, we don't want any data from the penultimate block to show
+ up in our JPEG image. Thus, after the JPEG file data ends, we start
+ another comment that will extend until the penultimate block,
+ hiding it from the parser. Care must be taken to ensure the
+ penultimate block is really on a block boundary (the comment can be
+ padded to increase the length if necessary). After this second
+ comment will appear the final magic bytes ffd9
as
+ desired.
-from aesgcmanalysis import xor, gmac, gcm_encrypt, gcm_decrypt, nonce_reuse_recover_secrets - -k = b"tlonorbistertius" -nonce = b"jorgelborges" -m1, aad1 = b"The universe (which others call the Library)", b"" -m2, aad2 = b"From any of the hexagons one can see, interminably", b"" - -c1, mac1 = gcm_encrypt(k, nonce, aad1, m1) -c2, mac2 = gcm_encrypt(k, nonce, aad2, m2) - -# Recover the authentication key and blind from public information -possible_secrets = nonce_reuse_recover_secrets(nonce, aad1, aad2, c1, c2, mac1, mac2) - -# Forge the ciphertext -m_forged = b"As was natural, this inordinate hope" -c_forged, aad_forged = xor(c1, xor(m1, m_forged)), b"" +from aesgcmanalysis import att_merge_jpg_bmp -for h, s in possible_secrets: - print("MAC candidate": gmac(h, s, aad_forged, c_forged))