Talos Vulnerability Report

TALOS-2019-0802

Nest Labs Openweave Weave ASN1Writer PutValue Code Execution Vulnerability

August 19, 2019
CVE Number

CVE-2019-5039

Summary

An exploitable command execution vulnerability exists in the ASN1 certificate writing functionality of Openweave-core version 4.0.2. A specially crafted weave certificate can trigger a heap-based buffer overflow, resulting in code execution. An attacker can craft a weave certificate to trigger this vulnerability.

Tested Versions

Nest Labs Openweave-core 4.0.2

Product URLs

https://openweave.io/

CVSSv3 Score

7.5 - CVSS:3.0/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H

CWE

CWE-122: Heap-based Buffer Overflow

Details

Openweave is the opensource implementation of the Weave protocol, which was initially developed by Nest Labs in order to provide a Session-Layer agnostic method of communication between their IoT devices. The weave binary is a utility tool that helps with various aspects of interacting with Weave, such as key conversion, cert conversion, and most relevantly, printing out weave TLV's. The weave binary can also be found on Nest devices such as the Nest Cam IQ Indoor.

The weave resign-command command takes an input certificate of either weave or X509 format and will resign it with another given certificate chain. To do this, the ReadCert function is used:

bool ReadCert(const char *fileName, X509 *& cert, CertFormat& origCertFmt)
{
    bool res = true;
    WEAVE_ERROR err;
    uint8_t *certBuf = NULL;
    const uint8_t *p;
    uint32_t certLen;
    CertFormat curCertFmt;

    cert = NULL;

    if (!ReadFileIntoMem(fileName, certBuf, certLen)) // reads/ftells/mallocs/freads.
    ExitNow(res = false);                        //  len returned into certLen, buf returned into certBuf

    curCertFmt = origCertFmt = DetectCertFormat(certBuf, certLen);

    [...]

    if (curCertFmt == kCertFormat_Weave_Raw)
    {
    uint32_t convertedCertLen = certLen * kMaxWeaveCertInflationFactor;      // [1] 
    uint8_t *convertedCertBuf = (uint8_t *)malloc((size_t)convertedCertLen); // [2] 

    err = ConvertWeaveCertToX509Cert(certBuf, certLen, convertedCertBuf, convertedCertLen, convertedCertLen); // [3]

Already there is a glaring issue in that convertedCertLen is a uint32_t and there's no restriction on the input certificate's size. Thus, when we multiply the certLen by the kMaxWeaveCertInflationFactor (which is 0x5), we can trigger an integer overflow if the size of the input certificate is greater than or equal to 0x3333334. This results in the convertedCertBuf allocation being less than the size of the actual certLen input [2]. Interestingly, this integer overflow doesn't really matter that much in the grand scheme of things, as it's not really exploitable aside from the vector that will be discussed in this advisory (which is also triggerable without the integer overflow).

The ConvertWeaveCertToX509Cert inside of WeaveToX509.cpp is now given:

NL_DLL_EXPORT WEAVE_ERROR ConvertWeaveCertToX509Cert(const uint8_t *weaveCert, uint32_t weaveCertLen, uint8_t *x509CertBuf, uint32_t x509CertBufSize, uint32_t& x509CertLen)
{
    WEAVE_ERROR err; 
    TLVReader reader;
    ASN1Writer writer;
    WeaveCertificateData certData;                

    reader.Init(weaveCert, weaveCertLen);        //[1]
    writer.Init(x509CertBuf, x509CertBufSize);   //[2]

    memset(&certData, 0, sizeof(certData));

    err = ConvertCert(reader, writer, certData); //[3]
    SuccessOrExit(err);
    err = writer.Finalize();
    SuccessOrExit(err);

    x509CertLen = writer.GetLengthWritten();

    exit:
    return err;
}

At [1], a WeaveTLVReader is appropriately initialized to the size of our input certificate, and likewise, we have an ASN1Writer object appropriately initialized to the size of the output buffer at [2]. As mentioned before it doesn't really matter if we trigger our integer overflow from before, but it is worth noting that this size is arbitrary and under user control. Moving on, let us examine the ConvertCert function:

static WEAVE_ERROR ConvertCert(TLVReader& reader, ASN1Writer& writer, WeaveCertificateData& certData){
    WEAVE_ERROR err;
    uint64_t tag;
    TLVType containerType;

    if (reader.GetType() == kTLVType_NotSpecified) {
        err = reader.Next();
        SuccessOrExit(err);
    }
    VerifyOrExit(reader.GetType() == kTLVType_Structure, err = WEAVE_ERROR_WRONG_TLV_TYPE);  
    tag = reader.GetTag();              
    VerifyOrExit(tag == ProfileTag(kWeaveProfile_Security, kTag_WeaveCertificate) || tag == AnonymousTag,  err = WEAVE_ERROR_UNEXPECTED_TLV_ELEMENT); 
    err = reader.EnterContainer(containerType); // [1]
    SuccessOrExit(err);

    // Certificate ::= SEQUENCE
    ASN1_START_SEQUENCE {

    // tbsCertificate TBSCertificate,
    err = ConvertTBSCertificate(reader, writer, certData); //[2]

To summarize, the above is just reading assorted TLV fields from our TLVReader for validation purposes and then the actual processing occurs in ConvertTBSCertificate. It's also worth noting specifically that in order to pass the check at [1], our TLVReader must have processed a minimum of three bytes. Continuing into ConvertTBSCertificate:

WEAVE_ERROR ConvertTBSCertificate(TLVReader& reader, ASN1Writer& writer, WeaveCertificateData& certData) // certificate data => 0xC0 Struct.
{
    WEAVE_ERROR err;
    uint64_t tag;
    uint32_t weaveSigAlgo;
    OID sigAlgoOID;

    // tbsCertificate TBSCertificate,
    // TBSCertificate ::= SEQUENCE
    ASN1_START_SEQUENCE {

    // version [0] EXPLICIT Version DEFAULT v1
    ASN1_START_CONSTRUCTED(kASN1TagClass_ContextSpecific, 0) {

    // Version ::= INTEGER { v1(0), v2(1), v3(2) }
    ASN1_ENCODE_INTEGER(2);

    } ASN1_END_CONSTRUCTED; 

    err = reader.Next(kTLVType_ByteString, ContextTag(kTag_SerialNumber));  // [1]
    SuccessOrExit(err); 

    // CertificateSerialNumber ::= INTEGER
    err = writer.PutValue(kASN1TagClass_Universal, kASN1UniversalTag_Integer, false, reader);  //[2] 
    SuccessOrExit(err);

At [1], we read a few more bytes to find a TLV ByteString inside of our buffer, and then at [2], we end up writing it into the output buffer, nothing out of the ordinary. Continuing on into ASN1Writer::PutValue:

ASN1_ERROR ASN1Writer::PutValue(uint8_t cls, uint32_t tag, bool isConstructed, nl::Weave::TLV::TLVReader& val)
{
    ASN1_ERROR err;
    uint32_t valLen = val.GetLength();  

    err = EncodeHead(cls, tag, isConstructed, valLen); //[1]
    SuccessOrExit(err);       

    val.GetBytes(mWritePoint, valLen);  //[2] 
    mWritePoint += valLen;             

    exit: 
        return err;
}

The EncodeHead function at [1] is entrusted with validating the input TLVElement and encoding the necessary ASN1 headers, after which, at [2], the bytes are actually written into the output buffer. Finally, we get to the crux of the matter:

ASN1_ERROR ASN1Writer::EncodeHead(uint8_t cls, uint32_t tag, bool isConstructed, int32_t len)
{
    // Only tags <= 31 supported. The implication of this is that encoded tags are exactly 1 byte long.
    if (tag >= 0x1F)
    return ASN1_ERROR_UNSUPPORTED_ENCODING;

    // Compute the number of bytes required to encode the length.
    uint8_t lenOfLen = GetLengthOfLength(len);
    // If the element length is unknown, allocate a new entry in the deferred-length list.
    //
    // The deferred-length list is a list of "pointers" (represented as offsets into mBuf)
    // to length fields for which the length of the element was unknown at the time the element
    // head was written. Examples include constructed types such as SEQUENCE and SET, as well
    // non-constructed types that encapsulate other ASN.1 types (e.g. OCTET STRINGS that contain
    // BER/DER encodings). The final lengths are filled in later, at the time the encoding is
    // complete (e.g. when EndConstructed() is called).
    //
    if (len == kUnkownLength) // What if length is 0? => total Len == 2
    mDeferredLengthList--;

    // Make sure there's enough space to encode the entire value without bumping into the deferred length
    // list at the end of the buffer. 
    uint16_t totalLen = 1 + lenOfLen + (len != kUnkownLength ? len : 0);  //[1]
    if ((mWritePoint + totalLen) > (uint8_t *)mDeferredLengthList)        //[2] 
        return ASN1_ERROR_OVERFLOW;   

At [1], we can see that the size of our TLV input is added to 1 and then the actual byte length of the size of our element (1-5). This is then stored in the uint16_t totalLen and validated against mWritePoint and the mDeferredLengthList. Unfortunately, since there's no bounds on our input ByteString TLV, we can easily overflow the uint16_t and bypass this check, which then allows us to get an arbitrary sized write on the heap after we return from Encode via val.GetBytes(mWritePoint, valLen);.

It should be noted that this ASN1Writer code path can be also hit from the CASE authentication functionality, more specifically the BeginCASESessionRequest. However, due to the limitation of the size of PacketBuffer input (~0x62F), and also the bytes read before getting to this overflow, we cannot have enough bytes left over in the TLVReader to say our bytestring is >0xFFFF in size, as the TLVReader will check the length of each element against the maximum length that it was initialized with, which for a BeginCASESessionRequest certInfo field would be normally limited to the size of the PacketBuffer.

Timeline

2019-04-18 - Vendor Disclosure
2019-05-20 - Vendor completed analysis
2019-06-18 - Follow up with vendor
2019-07-02 - 90 day notice; Vendor advised updates scheduled for release mid-July
2019-07-18 - Vendor advised fix will release end of July and be tested in the field
2019-07-26 - Extended disclosure date to 2019-08-15 2019-08-19 - Public Release

Credit

Discovered by Lilith (>_>) And Claudio Bozzato of Cisco Talos.