From c6d2859dd229d6ad058440feac910b9905a761d7 Mon Sep 17 00:00:00 2001 From: cyfraeviolae Date: Sun, 28 Aug 2022 02:32:00 -0400 Subject: k1 for ffff, diagrams int ext --- aesgcmanalysis.py | 15 ++-- app.py | 7 +- static/decrypt-aes-gcm.go | 52 +++++++++++++ static/styles.css | 13 +++- templates/index.html | 8 +- templates/key-commitment.html | 171 ++++++++++++++++++++++++------------------ templates/mac-truncation.html | 4 +- templates/nonce-reuse.html | 4 +- 8 files changed, 179 insertions(+), 95 deletions(-) create mode 100644 static/decrypt-aes-gcm.go diff --git a/aesgcmanalysis.py b/aesgcmanalysis.py index 13bfca3..b73960e 100644 --- a/aesgcmanalysis.py +++ b/aesgcmanalysis.py @@ -828,25 +828,22 @@ def key_search(nonce, init_bytes1, init_bytes2): def att_merge_jpg_bmp(jpg, bmp, aad): # Precomputed with key_search; works for any files - k1 = unhexlify('5c3cb198432b0903e58de9c9647bd241') - k2 = unhexlify('df923ae8976230008a081d23205d7a4f') + k1 = unhexlify('8007941455b5af579bb12fff92ef31a3') + k2 = unhexlify('14ef746e8b1792e52b1d22ef124fae97') nonce = b'JORGELBORGES' - total_len = 6 + (0xff<<8) + 0xff + len(jpg) + total_len = 6 + (0xffff) + len(jpg) + 0xff # get some extra jpgstream, _ = gcm_encrypt(k1, nonce, aad, b'\x00'*total_len) bmpstream, _ = gcm_encrypt(k2, nonce, aad, b'\x00'*total_len) - # 5 bytes - r = xor(jpgstream, b'\xff\xd8\xff\xfe\xff') - - # 1 byte - r += bmpstream[5:6] + # 6 bytes + r = xor(jpgstream, b'\xff\xd8\xff\xfe\xff\xff') # len(bmp) bytes bmpenc = xor(bmp[6:], bmpstream[6:6+len(bmp)]) r += bmpenc - comlen = (0xff << 8) + (jpgstream[5] ^ bmpstream[5]) + comlen = 0xffff # finish comment with padding r += b'\x00'*(comlen - len(bmpenc)) diff --git a/app.py b/app.py index 06c5f36..cf3ac79 100644 --- a/app.py +++ b/app.py @@ -161,9 +161,10 @@ def key_commitment(): if form.is_submitted(): if form.validate(): if form.mode.data == 'sample': - # jpeg_bytes = open('static/axolotl.jpg', 'rb').read() - # bmp_bytes = open('static/kitten.bmp', 'rb').read() - return send_file('static/sample-polyglot.enc', mimetype='application/octet-stream', as_attachment=True, download_name="polyglot.enc") + jpeg_bytes = open('static/axolotl.jpg', 'rb').read() + bmp_bytes = open('static/kitten.bmp', 'rb').read() + # return send_file('static/sample-polyglot.enc', + # mimetype='application/octet-stream', as_attachment=True, download_name="polyglot.enc") else: jpeg_bytes = form.jpeg.data.read() bmp_bytes = form.bmp.data.read() diff --git a/static/decrypt-aes-gcm.go b/static/decrypt-aes-gcm.go new file mode 100644 index 0000000..bab07b5 --- /dev/null +++ b/static/decrypt-aes-gcm.go @@ -0,0 +1,52 @@ +package main + +import ( + "crypto/aes" + "crypto/cipher" + "encoding/hex" + "errors" + "io/ioutil" + "os" +) + +func main() { + err := inner() + if err != nil { + panic(err.Error()) + } +} + +func inner() error { + if len(os.Args) < 3 { + return errors.New("usage: ./decrypt-aes-gcm < input-file > output-file") + } + key, err := hex.DecodeString(os.Args[1]) + if err != nil { + return err + } + nonce, err := hex.DecodeString(os.Args[2]) + if err != nil { + return err + } + ciphertext, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return err + } + block, err := aes.NewCipher(key) + if err != nil { + return err + } + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return err + } + plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return err + } + _, err = os.Stdout.Write(plaintext) + if err != nil { + return err + } + return nil +} diff --git a/static/styles.css b/static/styles.css index 5ea8a95..768aba7 100644 --- a/static/styles.css +++ b/static/styles.css @@ -20,7 +20,7 @@ ul { } pre { - white-space: pre-wrap; + /* white-space: pre-wrap; */ } details[open=""] { @@ -51,6 +51,13 @@ code { max-height: 200px; max-width: 300px; min-height: 150px; - /* width: 48%; */ - /* height: auto; */ +} + +.demo { + border: 1px dotted grey; + display: inline-block; + padding: 20px; + overflow-x: scroll; + width: 100%; + box-sizing: border-box; } diff --git a/templates/index.html b/templates/index.html index accbca7..e864b19 100644 --- a/templates/index.html +++ b/templates/index.html @@ -16,13 +16,13 @@ at cyfraeviolae.org

@@ -76,7 +76,7 @@ m = \alpha^{128}+\alpha^7 + \alpha^2 + \alpha + 1 \] \[ - \mathbb{K} = \mathbb{F}(2^{128})/m. + \mathbb{K} = \mathbb{F}_{2^{128}}/m. \]

@@ -84,7 +84,7 @@ interpreted as the set of polynomials with coefficients in \(\mathbb{F}_2\) of degree less than \(128\). Multiplication is performed modulo \(m\). This field is of characteristic 2; - e.g., \((\alpha^5 + 1)+(\alpha^5 + 1) = 0\). + e.g., \(2=0\), \(a=-a\), \((\alpha^5 + 1)+(\alpha^5 + 1) = 0\).

We interpret 16-byte blocks as elements in \(\mathbb{K}\) diff --git a/templates/key-commitment.html b/templates/key-commitment.html index ab72df2..af1e656 100644 --- a/templates/key-commitment.html +++ b/templates/key-commitment.html @@ -16,13 +16,13 @@ at cyfraeviolae.org

@@ -79,17 +79,17 @@ key, it will look identical to the BMP file. -

- Key 1: 5c3cb198432b0903e58de9c9647bd241 -
- Key 2: df923ae8976230008a081d23205d7a4f -
- Nonce: 4a4f5247454c424f52474553 -

+

+ Key 1: 8007941455b5af579bb12fff92ef31a3 +
+ Key 2: 14ef746e8b1792e52b1d22ef124fae97 +
+ Nonce: 4a4f5247454c424f52474553 +


- +
@@ -99,35 +99,17 @@

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. + then open /tmp/polyglot-first.jpg and /tmp/polyglot-second.bmp + in an image viewer. You may need to alter the path of polyglot.enc to reflect + your download directory.

-
- - Test script. - -
-TEMP="$(mktemp).go"
-cat > "$TEMP" <<EOF
-package main
-import ("crypto/aes"; "crypto/cipher"; "encoding/hex"; "os")
-func main() {
-  var key, nonce, ciphertext, plaintext []byte; var block cipher.Block; var aesgcm cipher.AEAD; var err error
-  if len(os.Args) < 4 { panic("usage: go run salamander.go   ") }
-  if key, err = hex.DecodeString(os.Args[1]); err != nil { panic(err.Error()) }
-  if nonce, err = hex.DecodeString(os.Args[2]); err != nil { panic(err.Error()) }
-  if ciphertext, err = os.ReadFile(os.Args[3]); err != nil { panic(err.Error()) }
-  if block, err = aes.NewCipher(key); err != nil { panic(err.Error()) }
-  if aesgcm, err = cipher.NewGCM(block); err != nil { panic(err.Error()) }
-  if plaintext, err = aesgcm.Open(nil, nonce, ciphertext, nil); err != nil { panic(err.Error()) }
-  if _, err = os.Stdout.Write(plaintext); err != nil { panic(err.Error()) }
-}
-EOF
-go run "$TEMP" 5c3cb198432b0903e58de9c9647bd241 4a4f5247454c424f52474553 polyglot.enc > first.jpg
-go run "$TEMP" df923ae8976230008a081d23205d7a4f 4a4f5247454c424f52474553 polyglot.enc > second.bmp
+
+curl -L -o /tmp/decrypt-aes-gcm.go https://cyfraeviolae.org/forbidden-salamanders/static/decrypt-aes-gcm.go
+go build -o /tmp/decrypt-aes-gcm /tmp/decrypt-aes-gcm.go
+< polyglot.enc /tmp/decrypt-aes-gcm 5c3cb198432b0903e58de9c9647bd241 4a4f5247454c424f52474553 > /tmp/polyglot-first.jpg
+< polyglot.enc /tmp/decrypt-aes-gcm df923ae8976230008a081d23205d7a4f 4a4f5247454c424f52474553 > /tmp/polyglot-second.bmp
 
-
-
+ Attack outline. @@ -135,6 +117,7 @@ go run "$TEMP" df923ae8976230008a081d23205d7a4f 4a4f5247454c424f52474553 polyglo This attack was shown by Yevgeniy Dodis, Paul Grubbs, Thomas Ristenpart, and Joanne Woodage in Fast Message Franking: From Invisible Salamanders to Encryptment.

+

Colliding MACs

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 @@ -174,67 +157,111 @@ go run "$TEMP" df923ae8976230008a081d23205d7a4f 4a4f5247454c424f52474553 polyglo 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.

+

Magic Bytes

For the next phase, we construct a ciphertext that decrypts to a valid JPEG under one key and a valid BMP under another. + Recall that the ciphertext of AES-GCM, as in AES-CTR, is computed by taking the XOR of the keystream and the message. The keystream + is computed from the cipher key and the nonce. +

+

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. + a different key, but it will not matter as the junk data will be in a location that is ignored by the image parser. +

+

+ All JPEG files start with the magic bytes \(\mathtt{ffd8}\) and end + with \(\mathtt{ffd9}\). We will place a JPEG comment immediately + after the initial magic bytes, which is indicated by \(\mathtt{fffe}\) and is followed by a 2-byte big-endian encoding of the comment length \(J\). + Let \(J_i\) indicate the \(i\)th byte of \(J\); \(J_0\) being the least significant byte.

- 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. + All BMP files start with the magic bytes \(\mathtt{424d}\) followed by a 4-byte little-endian encoding of the file length. + Because we need the BMP file to fit inside the JPEG comment, we set + \[ + \begin{array}{|c|c|}\hline + & 0 & 1 & 2 & 3 & 4 & 5 & \ldots & -2 & -1 \\\hline + \mathsf{JPEG} & \mathtt{ff} & \mathtt{d8} & \mathtt{ff} & \mathtt{fe} & J_1 & J_0 & \ldots & \mathtt{ff} & \mathtt{d9} \\ + \mathsf{BMP} & \mathtt{42} & \mathtt{4d} & J_0 & J_1 & \mathtt{00} & \mathtt{00} & \ldots & & \\\hline + \end{array} + \]

- 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. + In addition to the file length at the beginning, BMP files also + include the size of the color array (the pixels of the image) in + the initial metadata. BMP parsers ignore any data after the color + array is supposed to be over, even if the file length has not been + exhausted yet. That means we can set \(J=\mathtt{ffff}=65536\), and the + resulting header will be valid for any BMP file less than \(J\) bytes.

- 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 headers must be in the same location at the start of the file, + we search for two keys \(k_1, k_2\) and a nonce \(n\) such that + \[ \operatorname{AES-GCTR}(k_1, n, \mathtt{ffd8fffeffff}) = \operatorname{AES-GCTR}(k_2, n, \mathtt{424dffff0000}), \] + where \(\operatorname{AES-GCTR}\) returns the ciphertext portion of \(\operatorname{AES-GCM}\) but not the MAC.

- 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 + 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. + the ciphertext in a lookup table. Repeat until two keys are found that + encrypt their respective headers to the same ciphertext bytes. The search + takes less than a minute on a desktop computer.

- 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\). + We have now computed the ciphertext header \(C_{H}\) and two keys + which will decrypt it to the correct header bytes for both files. + Note that \(C_{H}\) only depends on the maximum size of + the BMP file, and thus can be precomputed. The remainder of the + attack that depends on the specific images is very fast.

+

Finishing the Polyglot

- 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. + As explained before, we place the BMP bytes in the JPEG comment, + add padding to finish the comment, and add the JPEG bytes after the comment is over. + Below we show the structure of the ciphertext. \(\downarrow\) indicates + that this part of the ciphertext is the encryption of the BMP cells under \(k_2\), and similarly \(\uparrow\) + indicates the encryption of the JPEG cells under \(k_1\). + \[ + \begin{array}{|c|c|}\hline + \mathsf{JPEG} && & \mathtt{00}^{J-\vert \textrm{BMP}\vert} & \textrm{JPEG} & \mathtt{ffd9} \\ + C & C_H & \downarrow & \uparrow & \uparrow & \uparrow\\ + \mathsf{BMP} && \textrm{BMP} & & & \\\hline + \end{array} + \] +

+

+ Here, \(\textrm{JPEG}\) is the bytes of the JPEG file without the initial and final magic bytes, + and similarly \(\textrm{BMP}\) is the bytes of the BMP file without the initial magic bytes.

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. + at the end, the JPEG file would no longer end in \(\mathtt{ffd9}\) + and would be invalid. Instead, we modify it to change + the penultimate block. The collision algorithm will result in a + ciphertext block \(X\). +

+

+ However, we don't want any data from the penultimate block to + corrupt our JPEG image. After \(\textrm{JPEG}\) ends, we start + another comment that will include the penultimate block, hiding it + from the parser. Care must be taken to ensure the penultimate block + is really on a block boundary. For AES-GCM, the block size is 16 bytes.

- 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. + Below is the final structure of the polyglot ciphertext. + \[ + J' = 16 - (6 + J + \vert \textrm{JPEG} \vert + 4) \pmod{16} + \] + \[ + \begin{array}{|c|c|}\hline + \mathsf{JPEG} && & \mathtt{00}^{J-\vert \textrm{BMP}\vert} & \mathrm{JPEG} & \mathtt{fffe} & J' & \mathtt{00}^{J'} & & \mathtt{00}^{14} & \mathtt{ffd9} \\ + C & C_{H} & \downarrow & \uparrow & \uparrow & \uparrow & \uparrow & \uparrow & X & \uparrow & \uparrow \\ + \mathsf{BMP} && \textrm{BMP} & & \\\hline + \end{array} + \]

diff --git a/templates/mac-truncation.html b/templates/mac-truncation.html index 1cbd629..9b36de9 100644 --- a/templates/mac-truncation.html +++ b/templates/mac-truncation.html @@ -16,13 +16,13 @@ at cyfraeviolae.org

diff --git a/templates/nonce-reuse.html b/templates/nonce-reuse.html index 249d00e..9049f4e 100644 --- a/templates/nonce-reuse.html +++ b/templates/nonce-reuse.html @@ -16,13 +16,13 @@ at cyfraeviolae.org

-- cgit v1.2.3