Open Source im professionellen Einsatz

Apache Santuario: Zu lange Schlüssel führen zu Overflow

Digitale Signaturen spielen heute insbesondere bei Web-Applikationen eine große Rolle. Darum hat das W3C (World Wide Web Consortium) schon vor längerer Zeit eine Arbeitsgruppe zu XML-Signaturen eingesetzt. Die dort spezifizierten Standards werden vom Projekt Apache Santuario als Java- und C++ Bibliotheken implementiert. Für die C++-Schnittstelle wurde nun ein Sicherheitsupdate herausgegeben, das eine vor kurzem entdeckte Schwachstelle korrigiert.

Ursache der Sicherheitslücke ist ein Stack Buffer Overflow, der erstmals Mitte/Ende Mai diskutiert wurde. Das Problem tritt beim Verarbeiten sehr langer Schlüssel auf. Die Originalmeldung demonstrierte dies mit einem 8192 Bit langem RSA-Schlüssel, der zum Signieren eingesetzt wurde. Der dabei ausgenutzte Programmierfehler befindet sich in der Funktion "DSIGAlgorithmHandlerDefault::signToSafeBuffer()":

unsigned int DSIGAlgorithmHandlerDefault::signToSafeBuffer(
  TXFMChain * inputBytes,
  const XMLCh * URI,
  XSECCryptoKey * key,
  unsigned int outputLength,
  safeBuffer & result) { 

...

char b64Buf[1024]; 
unsigned int b64Len;

...

case (XSECCryptoKey::KEY_RSA_PRIVATE):
case (XSECCryptoKey::KEY_RSA_PAIR):
   
if (sm != SIGNATURE_RSA) {
  
  throw XSECException(XSECException::AlgorithmMapperError,
        "Key type does not match <SignedInfo> signature type");
  
 }

b64Len = ((XSECCryptoKeyRSA *) key)->signSHA1PKCS1Base64Signature(hash, hashLen, (char *) b64Buf, 1024, hm);

if (b64Len <= 0) {
  throw XSECException(XSECException::AlgorithmMapperError,
        "Unknown error occured during a RSA Signing operation");
  
 }

// Clean up some "funnies" and make sure the string is NULL terminated

if (b64Buf[b64Len-1] == '\n')
  b64Buf[b64Len-1] = '\0';
 else
   b64Buf[b64Len] = '\0';

break; 

...

default :
  
throw XSECException(XSECException::AlgorithmMapperError,
"Key found, but don't know how to sign the document using it");
  
}
  
result = b64Buf; 

return (unsigned int) strlen(b64Buf);
 
} 

Dieser Code deklariert zunächst einen Array ("b64Buf") fester Größe, der 1024 Zeichen speichern kann. Im Falle eines RSA-Schlüssels wird anschließend der Case "(XSECCryptoKey::KEY_RSA_PRIVATE)"/"(XSECCryptoKey::KEY_RSA_PAIR)" ausgeführt. Ist der Schlüssel nun sehr lang, so wird die Funktion "signSHA1PKCS1Base64Signature()" die übergeben Maximallänge von 1024 als "b64Len" zurückliefern, weil die gesamten 1024 Zeichen bei einem langen Schlüssel benötigt werden.

Der kritische Punkt kommt am Ende des Codes, wo das Kontrollzeichen "\n" durch "\0" ersetzt wird, um die Zeichenkette zu terminieren. Im Allgemeinen wird bei einem sehr langen beliebigen Schlüssel "b64Buf[b64Len-1] != '\n'" zutreffen, das heißt, der Else-Fall tritt ein. Und hier findet sich nun ein typischer Off-by-One-Overflow: "b64Buf[b64Len]" schreibt ein Byte über das Ende des Puffers hinaus, und der String wird nicht korrekt terminiert.

Der Overflow lässt sich mit folgendem Exploit auslösen:

// This program is used to reproduce a bug in Apache Santuario.
// Compile it and run with valgrind to see the error messages.

// Warning: this program exploits a library bug and might cause any kind of
// behavior, including data loss, exploding your computer or the appearance of
// Santa Claus.

#include <iostream>

#ifdef unix
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#endif

#include <xercesc/parsers/XercesDOMParser.hpp>
#include <xercesc/util/PlatformUtils.hpp>

#include <xsec/dsig/DSIGReference.hpp>
#include <xsec/enc/OpenSSL/OpenSSLCryptoKeyRSA.hpp>
#include <xsec/framework/XSECException.hpp>
#include <xsec/framework/XSECProvider.hpp>

#include <openssl/rand.h>
#include <openssl/pem.h>

XERCES_CPP_NAMESPACE_USE

void init_libs() {
    try {
        XMLPlatformUtils::Initialize();
        XSECPlatformUtils::Initialise();
    } catch (...) {
        std::cerr << "Error initializing Xerces/XSec\n";
        exit(EXIT_FAILURE);
    }
}

void finish_libs() {
    try {
        XSECPlatformUtils::Terminate();
        XMLPlatformUtils::Terminate();
    } catch (...) {
        std::cerr << "Error finishing Xerces/XSec\n";
        exit(EXIT_FAILURE);
    }
}

bool fileExists(const char *file) {
#ifdef unix
    struct stat statBuffer;
    return (stat(file, &statBuffer) == 0);
#else
#error fileExists() not implemented for this system
#endif
}

void signXmlFile(DOMDocument *doc) {
    std::cout << "Signing XML file\n";

    XSECProvider prov;
    DSIGSignature *sig = prov.newSignature();
    sig->setDSIGNSPrefix(MAKE_UNICODE_STRING("ds"));

    DOMElement *sigNode = sig->createBlankSignature(doc, CANON_C14N_COM,
                                                    SIGNATURE_RSA, HASH_SHA1);

    DOMElement *rootElem = doc->getDocumentElement();
    rootElem->appendChild(sigNode);

    DSIGReference *ref = sig->createReference(MAKE_UNICODE_STRING(""));
    ref->appendEnvelopedSignatureTransform();

    BIO *bio = BIO_new_file("rsa-private.pem", "r");
    assert(bio);
    EVP_PKEY *private_key = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL);
    assert(private_key);
    OpenSSLCryptoKeyRSA *rsakey = new OpenSSLCryptoKeyRSA(private_key);

    sig->setSigningKey(rsakey);
    sig->sign();

    EVP_PKEY_free(private_key);
    BIO_free_all(bio);
}

void verifyXmlFile(DOMDocument *doc)
{
    std::cout << "Verifying XML file\n";

    XSECProvider prov;
    DSIGSignature *sig = prov.newSignatureFromDOM(doc);

    sig->load();

    BIO *bio = BIO_new_file("rsa-public.pem", "r");
    assert(bio);
    EVP_PKEY *public_key = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL);
    assert(public_key);
    OpenSSLCryptoKeyRSA *rsakey = new OpenSSLCryptoKeyRSA(public_key);

    sig->setSigningKey(rsakey);

    std::cout << "sig->verify() == " << sig->verify() << "\n";

    EVP_PKEY_free(public_key);
    BIO_free_all(bio);
}

void saveXmlFile(DOMDocument *doc, const char *file)
{
    XMLCh *xmlstr_file = XMLString::transcode(file);

    DOMImplementation *impl = DOMImplementationRegistry::getDOMImplementation(
                                MAKE_UNICODE_STRING("LS"));
    DOMLSSerializer *serializer =
        ((DOMImplementationLS*)impl)->createLSSerializer();

    serializer->writeToURI(doc, xmlstr_file);

    serializer->release();
    XMLString::release(&xmlstr_file);
}

int main() {
    init_libs();

    assert(fileExists("example.xml"));
    assert(fileExists("rsa-private.pem"));
    assert(fileExists("rsa-public.pem"));

    {
        try {
            XercesDOMParser parser;
            parser.setValidationScheme(XercesDOMParser::Val_Auto);
            parser.setDoNamespaces(true);

            parser.parse("example.xml");
            DOMDocument *doc = parser.getDocument();

            signXmlFile(doc);
            saveXmlFile(doc, "result.xml");
            verifyXmlFile(doc);

        } catch (XMLException& e) {
            char *msg = XMLString::transcode(e.getMessage());
            std::cerr << "XMLException: " << msg << "\n";
            XMLString::release(&msg);
        } catch (DOMException& e) {
            char *msg = XMLString::transcode(e.getMessage());
            std::cerr << "DOMException: " << msg << "\n";
            XMLString::release(&msg);
        } catch (XSECException& e) {
            char *msg = XMLString::transcode(e.getMsg());
            std::cerr << "XSECException: " << msg << "\n";
            XMLString::release(&msg);
        }
    }

    finish_libs();
    return 0;
}

Wer den benötigten RSA-Schlüssel ("rsa-private.pem") und die Beispiel XML-Datei ("example.xml") nicht selbst aufsetzen möchte, kann das ganze Paket einfach herunterladen.

Wird dieser Code mit dem Analyseprogramm Valgrind ausgeführt, so ist zu erkennen was genau geschieht:

$ valgrind ./buff_overflow
==23009== Memcheck, a memory error detector
==23009== Copyright (C) 2002-2009, and GNU GPL'd, by Julian Seward et al.
==23009== Using Valgrind-3.5.0 and LibVEX; rerun with -h for copyright info
==23009== Command: ./buff_overflow
==23009==
Signing XML file
==23009== Invalid write of size 1
==23009== at 0x4007D3C: strcpy (mc_replace_strmem.c:303)
==23009== by 0x528F916: safeBuffer::safeBuffer(char const*, unsigned int) (in /usr/lib/libxml-security-c.so.16.0.0)
==23009== by 0x525D995: DSIGAlgorithmHandlerDefault::signToSafeBuffer(TXFMChain*, unsigned short const*, XSECCryptoKey*, unsigned int, safeBuffer&) (in /usr/lib/libxml-security-c.so.16.0.0)
==23009== by 0x5267060: DSIGSignature::sign() (in /usr/lib/libxml-security-c.so.16.0.0)
==23009== by 0x804A535: signXmlFile(xercesc_3_1::DOMDocument*) (buff_overflow.cpp:81)
==23009== by 0x804A795: main (buff_overflow.cpp:117)
==23009== Address 0x43adfe0 is 0 bytes after a block of size 1,024 alloc'd
==23009== at 0x4005D2D: operator new[](unsigned int) (vg_replace_malloc.c:258)
==23009== by 0x528F8EC: safeBuffer::safeBuffer(char const*, unsigned int) (in /usr/lib/libxml-security-c.so.16.0.0)
==23009== by 0x525D995: DSIGAlgorithmHandlerDefault::signToSafeBuffer(TXFMChain*, unsigned short const*, XSECCryptoKey*, unsigned int, safeBuffer&) (in /usr/lib/libxml-security-c.so.16.0.0)
==23009== by 0x5267060: DSIGSignature::sign() (in /usr/lib/libxml-security-c.so.16.0.0)
==23009== by 0x804A535: signXmlFile(xercesc_3_1::DOMDocument*) (buff_overflow.cpp:81)
==23009== by 0x804A795: main (buff_overflow.cpp:117)
==23009==
==23009== Invalid read of size 1
==23009== at 0x4006813: strlen (mc_replace_strmem.c:275)
==23009== by 0x59BE8CD: xercesc_3_1::ICULCPTranscoder::transcode(char const*, xercesc_3_1::MemoryManager*) (in /usr/lib/libxerces-c-3.1.so)
==23009== by 0x5836ACD: xercesc_3_1::XMLString::transcode(char const*, xercesc_3_1::MemoryManager*) (in /usr/lib/libxerces-c-3.1.so)
==23009== by 0x528F54A: safeBuffer::sbStrToXMLCh() const (in /usr/lib/libxml-security-c.so.16.0.0)
==23009== by 0x52671F9: DSIGSignature::sign() (in /usr/lib/libxml-security-c.so.16.0.0)
==23009== by 0x804A535: signXmlFile(xercesc_3_1::DOMDocument*) (buff_overflow.cpp:81)
==23009== by 0x804A795: main (buff_overflow.cpp:117)
==23009== Address 0x43695b0 is 0 bytes after a block of size 1,024 alloc'd
==23009== at 0x4005D2D: operator new[](unsigned int) (vg_replace_malloc.c:258)
==23009== by 0x528F9FC: safeBuffer::safeBuffer() (in /usr/lib/libxml-security-c.so.16.0.0)
==23009== by 0x5267000: DSIGSignature::sign() (in /usr/lib/libxml-security-c.so.16.0.0)
==23009== by 0x804A535: signXmlFile(xercesc_3_1::DOMDocument*) (buff_overflow.cpp:81)
==23009== by 0x804A795: main (buff_overflow.cpp:117)
==23009==
==23009==
==23009== HEAP SUMMARY:
==23009== in use at exit: 288 bytes in 8 blocks
==23009== total heap usage: 7,402 allocs, 7,394 frees, 1,494,903 bytes allocated
==23009==
==23009== LEAK SUMMARY:
==23009== definitely lost: 0 bytes in 0 blocks
==23009== indirectly lost: 0 bytes in 0 blocks
==23009== possibly lost: 0 bytes in 0 blocks
==23009== still reachable: 288 bytes in 8 blocks
==23009== suppressed: 0 bytes in 0 blocks
==23009== Rerun with --leak-check=full to see details of leaked memory
==23009==
==23009== For counts of detected and suppressed errors, rerun with: -v
==23009== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 45 from 8) 

Valgrind meldet zwei Fehler, wobei der erste durch das oben diskutierte Problem ausgelöst wird. Beim Betrachten des Outputs fällt auf, dass beim Signieren ein ungültiger Schreibzugriff auftritt ("Invalid write of size 1"). Allerdings tritt dieser Fehler nicht direkt in "DSIGAlgorithmHandlerDefault::signToSafeBuffer()" auf, sondern erst in "safeBuffer::safeBuffer()". Bei "mc_replace_strmem.c" handelt es sich um eine Memorycheck-Datei von Valgrind selbst. Die "safeBuffer"-Klasse kommt hier vor, weil "DSIGAlgorithmHandlerDefault::signToSafeBuffer()" einen "safeBuffer" in "result" zurückliefert. Da der String "b64Buf" aber zuvor durch den fehlerhaften Aufruf von "b64Buf[b64Len] = '\0'" nicht ordentlich terminiert wurde, kommt es zu einem Speicherzugriffsfehler im "safeBuffer"-Konstruktor.

Bleibt die Frage, warum Valgrind nicht direkt den fehlerhaften Schreibzugriff auf "b64Buf[b64Len]" entdeckt. Grund hierfür ist, dass "b64Buf" auf dem Stack liegt, und Valgrind solche Stack-Zugriffe nicht erkennt.

Betroffen sind die Versionen vor 1.6.1.

Speicher mit Valgrind prüfen

Valgrind eignet sich sehr gut um verschiedenste Speicherprobleme in Programmen zu finden. Folgendes Programm greift beispielsweise auf nicht allozierten Heap-Speicher zu:

#include <stdlib.h>

int main(void)
{
    char *x = malloc(10);
    x[10] = 'a';
    return 0;
}

Das Kommando "valgrind ./a.out" liefert daraufhin:

==26142== Memcheck, a memory error detector
==26142== Copyright (C) 2002-2009, and GNU GPL'd, by Julian Seward et al.
==26142== Using Valgrind-3.6.0.SVN-Debian and LibVEX; rerun with -h for copyright info
==26142== Command: ./a.out
==26142== 
==26142== Invalid write of size 1
==26142==    at 0x400542: main (in /home/mark/work/Private/LinuxMagazin/Blog/codes/a.out)
==26142==  Address 0x51b004a is 0 bytes after a block of size 10 alloc'd
==26142==    at 0x4C274A8: malloc (vg_replace_malloc.c:236)
==26142==    by 0x400535: main (in /home/mark/work/Private/LinuxMagazin/Blog/codes/a.out)
==26142== 
==26142== 
==26142== HEAP SUMMARY:
==26142==     in use at exit: 10 bytes in 1 blocks
==26142==   total heap usage: 1 allocs, 0 frees, 10 bytes allocated
==26142== 
==26142== LEAK SUMMARY:
==26142==    definitely lost: 10 bytes in 1 blocks
==26142==    indirectly lost: 0 bytes in 0 blocks
==26142==      possibly lost: 0 bytes in 0 blocks
==26142==    still reachable: 0 bytes in 0 blocks
==26142==         suppressed: 0 bytes in 0 blocks
==26142== Rerun with --leak-check=full to see details of leaked memory
==26142== 
==26142== For counts of detected and suppressed errors, rerun with: -v
==26142== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 4 from 4)

Wie erwartet meldet Valgrind einen ungültigen Schreibzugriff, da "x[10]" über das Ende des allozierten Puffers hinausschießt.

Wird "x" aber auf dem Stack statt dem Heap abgelegt


int main(void)
{
    char *x = "0123456789";
    x[10] = 'a';
    return 0;
}

so liefert valgrind diesmal:

==26208== Memcheck, a memory error detector
==26208== Copyright (C) 2002-2009, and GNU GPL'd, by Julian Seward et al.
==26208== Using Valgrind-3.6.0.SVN-Debian and LibVEX; rerun with -h for copyright info
==26208== Command: ./a.out
==26208== 
==26208== 
==26208== Process terminating with default action of signal 11 (SIGSEGV)
==26208==  Bad permissions for mapped region at address 0x4005E6
==26208==    at 0x4004D8: main (in /home/mark/work/Private/LinuxMagazin/Blog/codes/a.out)
==26208== 
==26208== HEAP SUMMARY:
==26208==     in use at exit: 0 bytes in 0 blocks
==26208==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated
==26208== 
==26208== All heap blocks were freed -- no leaks are possible
==26208== 
==26208== For counts of detected and suppressed errors, rerun with: -v
==26208== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 4 from 4)
Segmentation fault

Obwohl offensichtlich ein Zugriff ausserhalb eines zugewiesenen Speichersegmentes im Programm auftritt (Segmentation Fault) findet Valgrind keinerlei Fehler. Solche Arrays lassen sich mit Valgrind nur auf Umwegen prüfen, beispielsweise indem man die Arrays zum Testen einfach auf den Heap schiebt.

comments powered by Disqus

Stellenmarkt

Artikelserien und interessante Workshops aus dem Magazin können Sie hier als Bundle erwerben.