Summary

In this advisory we are disclosing a signature verification bypass vulnerability in the Huawei recovery mode. The vulnerability can be used not only to apply unauthentic firmware updates but also to achieve arbitrary code execution in the recovery mode. Combining this advisory with the vulnerability detailed in CVE-2021-40055, an attacker can achieve remote code execution without user interraction from the position of a network MITM.

The vulnerability was fixed in February 2022.

Vulnerability Details

Huawei devices - both those running Android and those running HarmonyOS - implement a proprietary update solution which can be applied in various ways. The methods are all public and differ in how the process is triggered (manually or automatically) and how the update media file to be applied is supplied (downloaded over Wi-Fi or supplied from a memory card). Every update method variant boils down to a common update media format, the ZIP archive, which carries the actual update material.

We have identified a logic vulnerability in the signature verification of update.zip files. By chaining this vulnerability with additional gaps of the update process authentication implementation, it is possible to achieve arbitrary code execution as root in recovery mode.

The Android Update.zip format

There are three different entities in a ZIP archive, which are identified by a 4-byte header.

Single file entry stored in ZIP format

The first one is the local file header, which contains information on the packed file data, such as compression method, data size, filename. The transformed (compressed) file data immediately follows the local file header. For every local file header, there is a matching central directory entry. The entries of the central directory are stored in succession, and each entry points to its own local file header. The pointers are implemented as offsets from the beginning of the file. Finally, the end of central directory (EOCD) entry provides information on the location and the size of the central directory. It also has a field to describe the length of the comment section, which immediately follows the EOCD. The ZIP archive has no dependency on the comment section at all, the archived files can be extracted with a corrupted comment section or even without it.

Java’s JAR specification, which also builds on ZIP, seems to have had some influence on the design of Android’s official update format that is used by Huawei as well.

Format of the update.zip as it is used by the recovery

As it can be seen on the illustration above the first part of the update.zip is just a regular ZIP file, with local file headers, central directories, the EOCD, and finally a ZIP comment section. The first noticable extension of Android’s update.zip over a traditional ZIP archive is the appended signature field.

Technically the signature is injected into the ZIP comment section, by increasing accordingly the comment size of EOCD and appending the signature data to the end of the ZIP file. This means that for a regular ZIP extractor the added signature is transparent, as it is considered to be part of the comment section.

The signature contains a SHA-256 hash which is signed by the hotakey_v2 key and its public part is used by the recovery binary to check the authenticity. The hash is generated over the whole update.zip file, up until the comment size field of EOCD, because obviously the comment size changes as the signature gets incorporated into the comment section. This make the update data tamper-proof, because to change the data one must know the private part of the signing key (or break RSA-PKCS7 or SHA-256).

Actually the signature isn’t just an encrypted hash value, but also at the very end of the update.zip there is a 6-byte long footer. This footer mirrors the comment size field of the EOCD, so it makes the range of the hashed data explicit. Furthermore the total size of the appended signature and the footer is also stored in the this footer.

Signature verification

The recovery binary, which can be found on the ramdisk images of recovery and erecovery at the /sbin/recovery location, is the one responsible for the update process. First of all, the update.zip is verified based on the appended signature. The traditional ZIP archive part of the update.zip is only parsed after successful signature verification, so the recovery doesn’t access the content of the ZIP unless it proves to be authentic.

The actual update.zip signature verification happens in the Verify::Verify function:

int Verify::Verify(archive_t *archive) {
  if ( Parse(&p7Data, archive, &signature) != 0)
    goto error;
  if ( Pkcs7Parse(&p7Data, &signature.signature_ptr) != 0)
    goto error;
  if ( Pkcs7Verify(&p7Data, &signature, archive->package) != 0)
    goto error;
  if ( HashCheck(&p7Data, &signature, archive->package) != 0)
    goto error;

  return 0;
}

The Parse function checks the basic structural integrity of the appended signature and returns the tagged location of the signature.

int Verify::Parse(
  void *unused, archive_t *archive, signature_t *signature
) {
  archive_ptr = archive->archive_ptr;
  archive_len = archive->archive_len;
  archive_end = archive_ptr + archive_len;

  if ((archive_ptr == 0) || (archive_len < 0x1c))
    goto error;
  if ((archive_end[-3] != 0xff) || (archive_end[-4] != 0xff))
    goto error;

  comment_size = *(ushort *)(archive_end - 2);
  signature_size = *(ushort *)(archive_end - 6);
  if (
    (signature_size > comment_size) ||
    (5 >= signature_size) ||
    (5 >= comment_size)
  )
    goto error;

  eocd_size = comment_size + 22;
  if (archive_len < eocd_size)
    goto error;

  ret = EocdCheck(unused, archive_end - eocd_size, eocd_size, comment_size);
  if (ret != 0)
    goto error;
  
  signature->archive_ptr = archive_ptr;
  signature->signed_length = archive_len - (2 + comment_size);
  signature->signature_ptr = archive_ptr + (archive_len - signature_size);
  signature->signature_len = signature_size - 6;

  return 0;
}

int Verify::EocdCheck(
  void *unused, byte *eocd, ulong eocd_size, short comment_size
) {
  if (eocd_size < 28)
    goto error;

  if ((eocd[0] != 'P') || (eocd[1] != 'K') || (eocd[2] != 5) || (eocd[3] != 6))
    goto error;
  
  if (*(short *)(eocd + 20) != comment_size) {
    goto error;

  return 0;
}

So besides the basic integrity checks (meaningful size of the archive, existence of the ffff magic value), it also makes sure that the comment size in the original EOCD and the appended footer are the same.

The implementation also defines the location of the PKCS7 signature based on the signature_size value. This value is directly read from the footer, which is not signed, so it is open for modification.

Of course, when the pointed signature is not a valid RSA-PKCS7 signature or the signature itself is invalid, the Pkcs7Verify and HashCheck functions throw an error and the update process terminates.

But what if everything after the original ZIP comment section of the update.zip is moved by filling in data between the end of the ZIP comment and the beginning of the PKCS7 signature? The signature_size would remain the same, as that is counted from the end of the file. The comment_size fields should be adjusted accordingly, because the distance between the end of the archive and the end of the EOCD is increased. That adjustment is feasible, as the data hashing stops right before the EOCD comment size field, so both comment size fields are modifiable.

This means there is a possibility of smuggling data between the original ZIP archive and the unchanged signature footer appended by Android, and the resulting update.zip still passes the cryptographic verification. Because the comment size is interpreted as an unsigned 16-bit integer, and the legitimate signature takes up less than 2~kB, there is 62~kB of space for injection.

ZIP extraction

After a successful verification, the update.zip is processed as a ZIP file. Huawei seems to use a slightly modified version of the minzip from AOSP. With minzip the update.zip never gets extracted in the traditional sense, rather it builds an in-memory map of the files and provides the extracted files via memory streams.

The unzip method is implemented in the ZipArchive function, which handles the opening of a ZIP file, first seeks to the end of the archive and works backwards in search of the EOCD magic value. When the PK\5\6 value is found, the algorithm reads the central directory offset field of the EOCD, and uses it as a file offset.

int mzOpenZipArchive(byte *addr, ulong length, ZipArchive *pArchive)
{
  if (length < 22)
    goto error;
  ...

  // must start with local file header
  if (*(uint *) addr != 0x4034b50)
    goto error;

  for (ptr = addr + (length - 22); addr <= ptr; ptr -= 1) {
    if ((ptr[0] != 'P') || (ptr[1] != 'K') || (ptr[2] != 5) || (ptr[3] != 6))
      continue;

    centdir_entry = *(ushort *)(ptr + 8);
    centdir_ofs = *(uint *)(ptr + 16);
    ...
    centdir_ptr = addr + centdir_ofs;
    ...
    break;
  }
  ...

  return 0;
}

The end result is that if there are multiple EOCD entries, minzip always finds the latest one, thanks to the backward search, while Verify::Parse selects the one which is pointed by the Android specific footer, and those two methods can yield different results.

For example, when the smuggled data contains an EOCD header, minzip would find that, as it comes after the EOCD of the original content (closer to the end), but the verification function still uses the original EOCD entry.

But the smuggled data is not limited to only containing a bare EOCD header, a whole ZIP file can easily fit there as well. That means the minzip would parse and extract the injected ZIP file, instead of the original archive, and at the same time the verification would still succeed, for the reasons detailed above. So it is possible to provide the recovery with an arbitrary update content in an update.zip which will still pass signature verification.

Injecting a new ZIP file between the EOCD and the update signature

Update verification

In Huawei’s case, the zip signature verification, extraction, authentication of the extracted content, and finally firmware flashing is all done in recovery mode. Recovery is a special mode of Huawei devices, in which low-level maintenance (e.g. update flashing, user data wipe) can be performed. It is similar to the normal Android mode, because both use the very same kernel image, but the recovery utilizes an in-memory root filesystem from ramdisk. Based on its init.rc scripts it starts the recovery binary /sbin/recovery immediately after boot.

First, the recovery binary fetches the update.zip according to the update method selected at initialization. Next, the cryptographic signature verification of the update.zip happens. Once this is passed, the update.zip container is unpacked and the recovery performs an extensive verification of the update.

This includes cryptographic and semantic analysis of the update archive, but also makes sure the update is compatible with the current device and its partition layout and it performs authorization verification as well (e.g. “ShipDevice” vs. “R&D” device).

The common huawei_ota_update function wraps the whole update process from the update.zip verification to the actual update install.

The update verification part is implemented in the huawei_update_pre_check or huawei_erecovery_pre_check functions.

As we can see from the tree below, the majority of the semantic checks are trivially bypassed by including or omitting certain files. The hw_update_auth_verify routine would mean an additional cryptographic authentication, however the implementation accepts a “skip authentication” tag instead. All of these file inclusions/ommissions are made freely possible by the EOCD confusion that allows the attacker to control the ZIP contents.

huawei_update_pre_check
├── DoSecVerifyFromZip
│   · bypass with a missing sec_xloader_header file
├── huawei_signature_and_auth_verify
│   ├── hw_map_and_verify_package
│   │   └── hw_signature_verify_package
│   │       └── verify_file
│   │           └── verifyInstance.Verify
│   │               └── Verify::Verify
│   │                   · refer to the previous section on EOCD confusion
│   │       · size check of 'META-INF/CERT.RSA'
│   ├── IsSdRootPackage
│   │   · bypass with missing OTA_update.tag
│   └── hw_update_auth_verify
│       └── hw_update_auth_verify
│           ├── IsNeedUpdateAuth
│           └── is_unauth_pkg
│               · bypass by creating the 'skipauth_pkg.tag' file
├── IsAllowShipDeviceUpdate
│   · bypass with no 'sec_xloader_header'
├── CheckBoardIdInfo
│   · bypass with no 'CHECKBOARDID.mbn'
├── UpdatePreCheck_wrapper
│   · bypass with no 'packageinfo.mbn'
└── USBUpdateVersionCheck
    └── CheckBoardIdInCompressPkg
        · in case of SD update 'SOFTWARE_VER_LIST.mbn' must be valid

The final step is achieving code execution. The actual update (i.e. writing new data to the flash chip) is not directly handled by the recovery itself, but it happens via the update-binary. As it was seen above, this file is part of the update.zip, it is stored at META-INF/com/ google/android/update-binary and gets called in the install_package function as the last step of do_ota_update.

This immediately translates to arbitrary code execution as a root process within recovery mode.

In order to reach this vulnerability, one of the methods for applying updates must be used. With memory card based updates this is trivial - if the device is rebooted into recovery and a memory card device (SD/NM card, USB Flash Drive) is enumerated over USB with the right file contents, the update.zip signature verification procedure auto-starts. In CVE-2021-40055 we disclose an additional vulnerability that allows triggering CVE-2021-40045 in the case of OTA updates as well.

For a detailed description of the vulnerability impact, see our presentation.

Affected Devices (Verified)

  • Kirin 990

    • Huawei Mate 30 Pro (LIO)
    • Huawei P40 Pro (ELS)
    • Huawei P40 (ANA)
  • Kirin 9000/9000E

    • Huawei Mate40 Pro (NOH) EMUI
    • Huawei MatePad Pro 12 (WGH) HarmonyOS
  • Qualcomm SM6115 Snapdragon 662

    • Huawei nova 8i (NEN) EMUI

Fix

Huawei OTA images, released after February 2022, contain the fix for the vulnerability.

Timeline

  • 2020.11.15. Bug reported to Huawei PSIRT
  • 2022.02.02. Update requested
  • 2022.02.03. Huawei promises a response
  • 2021.02.25. Huawei confirms vulnerability, assigns CVE, confirms severity
  • 2021.03.01. Huawei releases security bulletin