En fouillant dans le code assembleur de la librairie Android Widevine libwvhidl.so (API Level 26, Android 8.0) mais également dans libwvdrmengine.so (API Level 32, Android 12.0), je suis tombé sur quelque chose de bizarre : un UUID qui n’avait rien à faire là.

Les versions plus récentes sont stripped et ont un peu changé, mais j’ai trouvé une mention similaire dans sub_50F54 dans android.hardware.drm-service.widevine , (API Level 36, Android 15.0) en cherchant la séquence de bytes EA 83 BC F2 :

bool __fastcall sub_50F54(_QWORD *a1)
{
  if ( *a1 == 0xCE4AD679A98BEFEDLL && a1[1] == 0xED211DD5DC27C8A3LL )
    return 1;
  return *a1 == 0x344AC73CE41F7029LL && a1[1] == 0x479A43C790AE5B8CLL;
}

Découverte du code suspect

La fonction isCryptoSchemeSupported de Widevine contenait cette vérification intéressante :

bool __fastcall wvdrm::isWidevineUUID(wvdrm *this, const unsigned __int8 *a2)
{
  return !(*(_QWORD *)this ^ 0xCE4AD679A98BEFEDLL | *((_QWORD *)this + 1) ^ 0xED211DD5DC27C8A3LL)
      || (*(_QWORD *)this ^ 0x344AC73CE41F7029LL | *((_QWORD *)this + 1) ^ 0x479A43C790AE5B8CLL) == 0;
}

Deux UUID étaient hardcodés directement dans le binaire. Le premier, je le reconnaissais c’était clairement du Widevine. Mais le second ? Aucune idée.

La logique était simple : si l’UUID passé en paramètre correspond à l’une de ces deux valeurs, la fonction retourne true. Sinon, false. Rien d’extraordinaire, mais pourquoi deux UUID ?

Décodage des UUID depuis leur représentation mémoire

Décoder les UUID à partir de leur représentation little-endian était pénible. Les spécifications UUID sont claires sur le format, mais quand on manipule des valeurs 64-bit directement en mémoire, il faut reconstituer les champs dans le bon ordre.

def uuid_from_little_endian_bytes(low64, high64):
    # Conversion des valeurs 64-bit en bytes (little-endian)
    bytes_low = low64.to_bytes(8, 'little')
    bytes_high = high64.to_bytes(8, 'little')
    
    # Reconstruction des champs UUID 
    # (attention : les 3 premiers champs sont en big-endian dans l'UUID final)
    time_low = int.from_bytes(bytes_low[0:4], 'big')
    time_mid = int.from_bytes(bytes_low[4:6], 'big') 
    time_hi = int.from_bytes(bytes_low[6:8], 'big')
    clock_seq = int.from_bytes(bytes_high[0:2], 'big')
    node = int.from_bytes(bytes_high[2:8], 'big')
    
    return f"{time_low:08x}-{time_mid:04x}-{time_hi:04x}-{clock_seq:04x}-{node:012x}"

# Décodage des deux UUID trouvés
print(uuid_from_little_endian_bytes(0xCE4AD679A98BEFED, 0xED211DD5DC27C8A3))
# Résultat: edef8ba9-79d6-4ace-a3c8-27dcd51d21ed (UUID officiel Widevine)

print(uuid_from_little_endian_bytes(0x344AC73CE41F7029, 0x479A43C790AE5B8CLL))
# Résultat: 29701fe4-3cc7-4a34-8c5b-ae90c7439a47 (UUID mystérieux)

Le premier UUID correspondait bien à l’identifiant officiel Widevine selon DASHIF. Mais le second est introuvable dans la documentation officielle.

L’enquête sur l’UUID mystérieux

J’ai creusé dans les archives et forums pour comprendre d’où venait ce 29701fe4-3cc7-4a34-8c5b-ae90c7439a47. L’historique est le suivant :

2015 : Première mention sur les forums Apple Developer, soupçonné d’être lié à FairPlay 2.0.

2017 : Une issue officielle sur DASH-IF propose de le remplacer par l’UUID FairPlay officiel 94ce86fb-07ff-4f43-adb8-93d2fa968ca2.

2018 : Commit dans Shaka Packager qui ajoute le support de cet UUID pour FairPlay.

2023 : Shaka Packager migre vers l’UUID officiel, mais garde l’ancien “for backwards compatibility only”.

2025 : Un pastebin mentionne que Netflix utiliserait encore cet UUID non-officiel pour FairPlay.

Bref, cet UUID était un identifiant FairPlay “sauvage” utilisé avant la standardisation officielle.

Ce que j’ai obtenu au final

La fonction isCryptoSchemeSupported de Widevine accepte donc deux UUID :

  • L’UUID officiel Widevine (edef8ba9-79d6-4ace-a3c8-27dcd51d21ed)
  • Un ancien UUID FairPlay non-officiel (29701fe4-3cc7-4a34-8c5b-ae90c7439a47)
long double __usercall wvdrm::hardware::drm::V1_4::widevine::WVCryptoFactory::isCryptoSchemeSupported@<Q0>(
        wvdrm *this@<X1>,
        __int64 a2@<X8>)
{
  bool isWidevineUUID;
  long double result;

  // Vérifie si l'UUID correspond à Widevine OU à l'ancien FairPlay
  isWidevineUUID = wvdrm::isWidevineUUID(this, (const unsigned __int8 *)this);
  
  // Initialise le buffer de sortie et stocke le résultat
  *(_OWORD *)&result = 0u;
  *(_BYTE *)(a2 + 32) = 0;
  *(_OWORD *)a2 = 0u;
  *(_OWORD *)(a2 + 16) = 0u;
  *(_BYTE *)(a2 + 33) = isWidevineUUID;  // Résultat de la vérification
  
  return result;
}

La fonction ne fait que marquer un flag dans un buffer (offset 33) et retourne toujours 0. Ce n’est pas une validation stricte, plutôt une détection informative pour la couche supérieure.

D’où ma question : Pourquoi est-ce que cet UUID 29701fe4-3cc7-4a34-8c5b-ae90c7439a47, associé historiquement à un FairPlay non officiel, se retrouve-t-il dans une librairie Android liée à Widevine ?