PA5: Cryptography Spring 2022
Due: Wednesday, June 1st at 11:59pm
This is a group project; you can work in a team of size at most four and submit one project per team. You are not required to work with the same partner(s) on every project. You and your partner(s) should collaborate closely on each part.
You have two late days that you may use to turn in work past the deadline over the entire quarter. A late day is a contiguous 24-hour period. Both you and your partner(s) will be charged for every late day that you use, and you both must have late days to use them. These late days are intended to cover your extension needs for usual circumstances: brief illness, busy with other classes, interviews, travel, extracurricular conflicts, and so on. You do not need to ask permission to use a late day.
The code and other answers you submit must be entirely your team's own work. You may discuss the conceptualization of the project and the meaning of the questions, but you may not look at any part of someone else’s solution or collaborate with anyone other than your partner. You may consult published references, provided that you appropriately cite them (e.g., with program comments).
Solutions must be submitted to Gradescope.
Introduction
In this project, we'll start by investigating Vigenere ciphers, then move on to investigating vulnerabilities in widely used cryptographic hash functions, including length-extension attacks and collision vulnerabilities, and an implementation vulnerability in a popular digital signature scheme.
Index
- Part 1: Vigenere Ciphers
- Part 2: Length extension
- Part 3: MD5 collisions
- Part 4: RSA signature forgery
- Part 5: Writeup
Part 1: Vigenere Ciphers (4 points)
For this problem, solve by hand or write a program (perhaps in Python).
You can read about how the Vigenere cipher works on Wikipedia. Vigenere ciphers can be generally deciphered using Kasiski Examination, which is discussed on the wikipedia page.
You can find some ciphertext produced with the Vigenere cipher under a certain key on Gradescope as the assignment "PA5: Ciphertext".
We also provided sample code for decrypting ciphertext sample decryption code here.
Encrypting a plaintext letter with a key letterA
results in no change, encrypting
with a key letter B
results in an increment by one place in the alphabet, encrypting with key letter C
results in an increment by two places, etc.
Also assume that the original plaintext contains only uppercase letters (A-Z)
and no spaces or punctuation.
For example, encrypting the plaintext: ATTACKATDAWN with the key: BLAISE results in the following ciphertext:
Plaintext: | A | T | T | A | C | K | A | T | D | A | W | N |
Key: | B | L | A | I | S | E | B | L | A | I | S | E |
Ciphertext: | B | E | T | I | U | O | B | E | D | I | O | R |
The goal for this part of the assignment is to figure out what key was used to encrypt your ciphertext.
What to submit A text file named vigenere.key
containing your key.
Historical note: In November 2019, it was discovered that the security company Fortinet was using "XOR encryption with a static key" in some products, which is similar to a Vigenere cipher and has similar (lack of) security properties. https://seclists.org/bugtraq/2019/Nov/38
Part 2: Length extension (4 points)
For many applications, you should use MACs such as HMAC-SHA256 instead of plain cryptographic hash functions (e.g., MD5, SHA-1, or SHA-256) because hashes, also known as digests, do not provide security against an attacker who can control or modify the hash value. What we really want is something that behaves like a pseudorandom function, which HMAC and other MAC functions are constructed to behave like and hash functions alone do not.
One difference between hash functions and pseudorandom functions is that a collision-resistant hash function may still be subject to length extension. Many common hash functions use a design called the Merkle-Damgard construction. Each is built around a compression function f and maintains an internal state s, which is initialized to a fixed constant. Messages are processed in fixed-size blocks by applying the compression function to the current state and current block to compute an updated internal state, i.e., s_{i+1} = f(s_i, b_i). The result of the final application of the compression function becomes the output of the hash function.
A consequence of this design is that if we know the hash of an n-block message, we can find the hash of longer messages by applying the compression function for each block b_{n+1}, b_{n+2}, ... that we want to add. This process is called length extension, and it can be used to attack many applications of hash functions.
Experimenting
To experiment with this idea, we'll use a Python implementation of the MD5 hash
function, though SHA-1 and SHA-256 are vulnerable to length extension in the
same way. You can download the pymd5
module here and
learn how to use it by running pydoc pymd5
. To follow
along with these examples, run Python in interactive mode and run the command
from pymd5 import md5, padding
.
Consider the string "Use HMAC, not hashes". We can compute its MD5 hash by running:
from pymd5 import md5, padding
m = "Use HMAC, not hashes"
h = md5()
h.update(m)
print(h.hexdigest())
or, more compactly,
print(md5(m).hexdigest())
The output should be 3ecc68efa1871751ea9b0b1a5b25004d
.
MD5 processes messages in 512-bit blocks, so internally, the hash function
pads m to a multiple of that length. The padding consists of the bit 1,
followed by as many 0 bits as necessary, followed by a 64-bit count of the
number of bits in the unpadded message. (If the 1 and the count won't fit in
the current block, an additional block is added.) You can use the function
padding(count)
in the pymd5
module to compute the padding that will be
added to a count-bit message.
Even if we didn't know m, we could compute the hash of longer messages of the
general form m + padding(len(m)*8) + suffix
by setting the initial internal
state of our MD5 function to MD5(m)
, instead of the default initialization
value, and setting the function's message length counter to the size of m
plus the padding (a multiple of the block size). To find the padded message
length, guess the length of m and run bits = (length_of_m +
len(padding(length_of_m * 8))) * 8
.
The pymd5
module lets you specify these parameters as additional arguments
to the md5
object:
h = md5(state=bytes.fromhex("3ecc68efa1871751ea9b0b1a5b25004d"), count=512)
Now you can use length extension to find the hash of a longer string that appends the suffix "Good advice". Simply run:
x = "Good advice"
h.update(x)
print(h.hexdigest())
to execute the compression function over x and output the resulting hash.
Verify that it equals the MD5 hash of m.encode("utf-8") + padding(len(m)*8) + x.encode("utf-8")
. In Python 3, we need to convert m
and x
from strings to bytes so that we can add these to the
padding, which is a bytes
type.
Notice that, due to the length-extension property of MD5, we didn't need to know
the value of m
to compute the hash of the longer string - all we needed to
know was m
's length and its MD5 hash.
Conduct a length extension attack
Length extension attacks can cause serious vulnerabilities when people try to construct something like a MAC by using hash(secret || message) using a hash function like MD5, SHA1, or SHA2, that is vulnerable to length extension. (SHA3, unlike SHA-256, SHA1, and MD5, uses a different construction and is not vulnerable to length extension attacks, so SHA3(secret || message) is a secure MAC. This is why cryptography is hard.)
The National Bank of CSE 127, which is not up-to-date on its security practices, hosts an API that allows its client-side applications to perform actions on behalf of a user by loading URLs of the form:
http://bank.cse127.ucsd.edu/pa5/api?token=6c256f4a53dd0068b2d82306d9c09d1c&user=george&command1=ListSquirrels&command2=NoOp
where token
is MD5(user's 8-character password || user=...
[the rest of
the decoded URL starting from user= and ending with the last command]).
Using the techniques that you learned in the previous section and without
guessing the password, apply length extension to create a URL ending with
&command3=UnlockAllSafes
that would be treated as valid by the server API.
Note: Because of its bad security practices, the National Bank of CSE 127 has taken down its website. So you'll have to use gradescope to test if your attack URL would work.
Hint: You might want to use the quote()
function from Python's urllib.parse
module to encode non-ASCII characters in the URL.
Historical fact: In 2009, security researchers found that the API used by the photo-sharing site Flickr suffered from a length-extension vulnerability almost exactly like the one in this exercise.
What to submit A Python 3.x script named len_ext_attack.py
that:
- Accepts a valid URL in the same form as the one above as a command line argument.
- Modifies the URL so that it will execute the
UnlockAllSafes
command as the user. - Prints the new URL to the command line.
You should make the following assumptions:
-
The input URL will have the same form as the sample above, but we may change the server hostname and the values of
token
,user
,command1
, andcommand2
. These values may be of substantially different lengths than in the sample. -
The input URL may be for a user with a different password, but the length of the password will be unchanged.
You can base your code on the following example:
import sys, urllib.parse
from pymd5 import md5, padding
url = sys.argv[1]
# Your code to modify url goes here
print(new_url)
Part 3: MD5 collisions (4 points)
MD5 was once the most widely used cryptographic hash function, but today it is considered dangerously insecure. This is because cryptanalysts have discovered efficient algorithms for finding collisions---pairs of messages with the same MD5 hash value.
The first known collisions were announced on August 17, 2004 by Xiaoyun Wang, Dengguo Feng, Xuejia Lai, and Hongbo Yu. Here's one pair of colliding messages they published:
Message 1:
d131dd02c5e6eec4693d9a0698aff95c 2fcab58712467eab4004583eb8fb7f89
55ad340609f4b30283e488832571415a 085125e8f7cdc99fd91dbdf280373c5b
d8823e3156348f5bae6dacd436c919c6 dd53e2b487da03fd02396306d248cda0
e99f33420f577ee8ce54b67080a80d1e c69821bcb6a8839396f9652b6ff72a70
Message 2:
d131dd02c5e6eec4693d9a0698aff95c 2fcab50712467eab4004583eb8fb7f89
55ad340609f4b30283e4888325f1415a 085125e8f7cdc99fd91dbd7280373c5b
d8823e3156348f5bae6dacd436c919c6 dd53e23487da03fd02396306d248cda0
e99f33420f577ee8ce54b67080280d1e c69821bcb6a8839396f965ab6ff72a70
Copy the above hex strings into file1.hex and file2.hex.
Convert each group of hex strings into a binary file.
(On Linux, run xxd -r -p file.hex > file
.)
- What are the MD5 hashes of the two binary files? Verify that they're the same.
(
openssl dgst -md5 file1 file2
) - What are their SHA-256 hashes? Verify that they're different.
(
openssl dgst -sha256 file1 file2
)
3a. Generating collisions yourself
In 2004, Wang's method took more than 5 hours to find a collision on a desktop PC. Since then, researchers have introduced vastly more efficient collision finding algorithms, and found a collision for SHA1 as well, though that attack is not yet efficient enough to give as an undergraduate assignment. You can compute your own MD5 collisions using a tool written by Marc Stevens that uses a more advanced technique.
You can download the fastcoll
tool here:
http://www.win.tue.nl/hashclash/fastcoll_v1.0.0.5.exe.zip (Windows executable)
or
http://www.win.tue.nl/hashclash/fastcoll_v1.0.0.5-1_source.zip (source code)
If you are compiling fastcoll
from source, you can compile
using this makefile. You will also need to
have installed the Boost libraries. On Ubuntu, you can install using apt-get
install libboost-all-dev
. On OS X, you can install Boost via the Homebrew
package manager using brew install boost
.
-
Generate your own collision with this tool. How long did it take? (
time ./fastcoll -o file1 file2
) -
What are your files? To get a hex dump, run
xxd -p file
. -
What are their MD5 hashes? Verify that they're the same.
-
What are their SHA-256 hashes? Verify that they're different.
What to submit Write your answers in writeup.txt
. This file will also be
used for part 5.
3b. A hash collision attack
The collision attack lets us generate two messages with the same MD5 hash and any chosen (identical) prefix. Due to MD5's length-extension behavior, we can append any suffix to both messages and know that the longer messages will also collide. This lets us construct files that differ only in a binary "blob" in the middle and have the same MD5 hash, i.e. prefix || blobA || suffix and prefix || blobB || suffix.
We can leverage this to create two programs (shell scripts) that have identical
MD5 hashes but wildly different behaviors. We're using shell scripts, but this
could be done using a program in almost any language. Put the following two
lines into a file called prefix
:
#!/bin/bash
cat << "EOF" | openssl dgst -sha256 > DIGEST
and put these four lines (starting with a blank line) into a file called suffix
:
EOF
digest=$(cat DIGEST | sed 's/(stdin)= //' )
echo "The sha256 digest is $digest"
Now use fastcoll
to generate two files with the same MD5 hash that both begin
with prefix
. (fastcoll -p prefix -o col1 col2
.) Then append the suffix to both
(cat col1 suffix > file1.sh; cat col2 suffix > file2.sh
). Verify that file1.sh
and file2.sh
have the same MD5 hash but generate different output.
Extend this technique to produce another pair of programs, good
and evil
, that
also share the same MD5 hash. One program should execute a benign payload: echo
or print "I mean no harm."
The second should execute a pretend malicious payload:
echo or print "You are doomed!"
What to submit Two scripts, good
and evil
, that have the same MD5 hash, have
different SHA-256 hashes, and print the specified messages.
Part 4: RSA signature forgery (4 points)
A secure implementation of RSA encryption or digital signatures requires a proper padding scheme. RSA without padding, also known as textbook RSA, has several undesirable properties. For example, it is trivial for an attacker with only an RSA public key pair (n,e) to produce a mathematically valid message, signature pair by choosing an s and returning (s^e, s).
In order to prevent an attacker from being able to forge valid signatures in this way, RSA implementations use a padding scheme to provide structure to the messages that are encrypted or signed. The most commonly used padding scheme in practice is defined by the PKCS #1 v1.5 standard, which can be found at https://tools.ietf.org/html/rfc2313. The standard defines, among other things, the format of RSA keys and signatures and the procedures for generating and validating RSA signatures.
Validating RSA signatures
You can experiment with validating RSA signatures yourself. Create a file called
key.pub
that contains the following RSA public key:
-----BEGIN PUBLIC KEY-----
MFowDQYJKoZIhvcNAQEBBQADSQAwRgJBALB8X0rLPrfgAfXMW73LjKYb5V9QG5LU
DrmsA9CAittsLvh2c082wHwVyCIiWQ8S3AA/jfW839sFN4zAZkW2S3cCAQM=
-----END PUBLIC KEY-----
You can view the modulus and public exponent of this key by running:
openssl rsa -in key.pub -pubin -text -noout
Create a file containing only the text CSE 127 rul3z!
.
echo -n 'CSE 127 rul3z!' > message
The following is a base64-encoded signature of the file message
using the public
key above.
RaHHC2E0qm1sauuhlV3KBGiaTb5IGmaaNFQn22ykTSu88EIBkBG48gjiamc3l+4HJYUwpZDefcT2dLPyaOHA/w==
Convert this signature into a binary file:
base64 -d -i sig.b64 > sig
Now verify the signature against the file you created.
openssl dgst -sha1 -verify key.pub -signature sig message
We can also use basic math operations in Python to explore this signature further. Remember, RSA ciphertexts, plaintexts, exponents, moduli, and signatures are actually all integers.
Open a Python shell and run the following commands to import the signature as an integer:
from Crypto.PublicKey import RSA
from Crypto.Hash import SHA
signature = int(open('sig', 'rb').read().hex(), 16)
Next, import the public key file that you created earlier:
pubkey = RSA.importKey(open('key.pub').read())
The modulus and exponent are then accessible as pubkey.n
and pubkey.e
, respectively.
Now reverse the signing operation and examine the resulting value in hex:
"{:0128x}".format(pow(signature, pubkey.e, pubkey.n))
You should see something like '0001fffff ... 22c1422dac3c4e5fdd87040b3fb156acd3d83d1f'
Verify that the last 20 bytes of this value match the SHA-1 hash of your file:
SHA.new(b"CSE 127 rul3z!").hexdigest()
PKCS #1 v1.5 signature padding
The signed value you examined in the previous section had been padded using the
PKCS #1 v1.5 signature scheme. PKCS #1 v1.5 padding for RSA signatures is
structured as follows: one 00
byte, one 01
byte, some
FF
bytes, another 00
byte, some special ASN.1 bytes denoting
which hash algorithm was used to compute the hash digest, then the bytes of the
hash digest itself. The number of FF
bytes varies such that the size
of m is equal to the size of the RSA key.
A k-bit RSA key used to sign a SHA-1 hash digest will generate the following padded value of m:
00 01 FF...FF 00 3021300906052B0E03021A05000414 XX...XX
^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^
k/8 - 38 bytes wide || 20-byte SHA-1 digest
ASN.1 "magic" bytes
When PKCS padding is used, it is important for implementations to verify that
every bit of the padded, signed message is exactly as it should be. It is
tempting for an implementer to validate the signature by first stripping off
the 00 01
bytes, then some number of padding FF
bytes, then
00
, and then parse the ASN.1 and verify the hash. If the
implementation does not check the length of the FF
bytes and that the
hash is in the least significant bits of the message, then it is possible for
an attacker to forge values that pass this validation check.
This possibility is particularly troubling for signatures generated with e = 3. If the length of the required padding, ASN.1 bytes, and hash value is significantly less than n^{1/3} then an attacker can construct a cube root over the integers whose most significant bits will validate as a correct signature, ignoring the actual key. To construct a "signature" that will validate against such implementations, an attacker simply needs to construct an integer whose most significant bytes have the correct format, including the hashed message, pad the remainder of this value with zeros or other garbage that will be ignored by the vulnerable implementation, and then take a cube root over the integers, rounding as appropriate.
Constructing forged signatures
The National Bank of CSE 127 has a website that its employees use to initiate wire transfers between bank accounts. To authenticate each transfer request, the control panel requires a signature from a particular 2048-bit RSA key:
-----BEGIN PUBLIC KEY-----
MIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKCAQEAqtMbmgeGOL5l+sylkG5C
AgAmCmCXmN/KNFuIJaF1cxKMoiZzlqew3DVNF+Xs5rkFkzrflw2MVLY8SQl/qyRO
yHNy68OVwXeAbSIyY/8reUh2AXTm013HVS+LvI6yVOgQ4AwvfbuAPQ4B+nYbkK9G
wgHczJrChPMOaZz7yMBBwwEeonqdeNkuAyAsM/E7UmxCsR3FdMF3vuARLY/+7UJx
wDMFO1LMt5zOrQtK3AKiT4GTv4orBMAZ159ocBawpq6Z5emuI6opGavxLrjTlQgG
KagUNHhQXnQ/+pX6wPuMzWVv21z6L6m3Fbm/bWpyLteftEO7d+vMS8HTDzFQgjN2
bwIBAw==
-----END PUBLIC KEY-----
Unfortunately, this control panel is running old software that has not been patched to fix the signature forgery vulnerability.
Using the signature forgery technique described above, produce an RSA signature that validates against the National Bank of CSE 127 site.
Historical fact: This attack was discovered by Daniel Bleichenbacher, who presented it in a lightning talk at the rump session at the Crypto 2006 conference. His talk is described in this mailing list posting: https://www.ietf.org/mail-archive/web/openpgp/current/msg00999.html. At the time, many important implementations of RSA signatures were discovered to be vulnerable to this attack, including OpenSSL. In 2014, the Mozilla library NSS was found to be vulnerable to this type of attack: https://www.mozilla.org/security/advisories/mfsa2014-73/.
What to submit A Python 3.x script called bleichenbacher.py
that:
- Accepts a double-quoted string as command-line argument.
- Prints a base64-encoded forged signature of the input string.
We have provided a Python library, roots.py
, that
provides several useful functions that you may wish to use when implementing
your solution. You can download the library here. Your program
may assume that PyCrypto and roots.py
are available, and may use standard
Python libraries, but should otherwise be self-contained.
In order to use these functions, you will have to import roots.py
. You
may wish to use the following template:
from Crypto.PublicKey import RSA
from Crypto.Hash import SHA
from roots import *
import sys
message = sys.argv[1]
# Your code to forge a signature goes here.
# some example functions from roots
root, is_exact = integer_nthroot(27, 3)
print(integer_to_base64(root).decode())
5. Writeup (4 points)
-
With reference to the construction of HMAC, explain how changing the design of the API in Part 2 to use
token=HMAC(_user's password_)(user=...)
would avoid the length extension vulnerability. -
Briefly explain why the technique you explored in 3b poses a danger to systems that rely on digital signatures to verify the integrity of programs before they are installed or executed. Examples include Microsoft Authenticode and most Linux package managers. (Assume that these systems sign MD5 hashes of the programs.)
-
Since 2010, NIST has specified that RSA public exponents must be at least 2^16 + 1. Briefly explain why Bleichenbacher's attack would not work for these keys.
-
Remember to also add your answer to 3a in the writeup
What to submit Write your answers in writeup.txt
.
Submission Checklist
Submit the following to gradescope:
vigenere.key
(for part 1)len_ext_attack.py
(for part 2)good
andevil
(for part 3b)bleichenbacher.py
(for part 4)writeup.txt