Obejeście weryfikacji podpisu providera kryptograficznego na HotSpot JRE

Aby dodać do JCA provider kryptograficzny Javy zawierający implementację związaną z szyfrowaniem (dokładniej Cipher, KeyAgreement, KeyGenerator, MAC albo SecretKey) trzeba go wcześniej podpisać certyfikatem uzyskanym od firmy Oracle (lub IBM). Cytując za dokumentacją, w ten sposób JCA uwierzytelnia dostawcę żeby tylko providery podpisane przez zaufane podmioty mogły być włączone do JCA. Znalazłem lukę, która pozwala to wymaganie obejść.

Certyfikaty CA wystawiających certyfikaty do podpisu providerów zaszyte są głęboko w obfuskowanym kodzie JRE (javax.crypto.SunJCE_b). Przed modyfikacją tego kodu chroni licencja, choć polskie prawo w niektórych przypadkach taką ochronę uchyla na mocy art. 75 ust. 2 ustawy o prawach autorskich i prawach pokrewnych. Niemniej jednak nawet to nie jest konieczne.

Okazuje się, że Java HotSpot od wielu lat ma lukę w weryfikacji podpisów na providerach (a także na jurisdiction policy files). Kod weryfikujący podpisy  tworzy (w javax.crypto.SunJCE_b) certyfikaty CA ze strumienia bajtów przy pomocy CertificateFactory. Tą pobiera jednak z pierwszego providera na liście przy uzyciu metody CertificateFactory#getInstance(String).

Daje to nam możliwość dodania na początek listy providerów takiego, który dostarczy implementację CertificateFactory podmieniającą jeden z certyfikatów CA (można te certyfikaty znaleźć nawet w Google). Provider zawierający tylko implementację CertificateFactory.X.509 nie musi być podpisany.

Poniżej przykładowa implementacja CertificateFactorySpi zwracająca zamiast jednego z certyfikatów JCE code signing certyfikat użyty do podpisu naszego providera z implementacją szyfru:

package eu.zacheusz.hacksignprovider;
 
import java.io.InputStream;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import sun.security.x509.X509CertImpl;
 
/**
 * @author Zacheusz
 */
public class ReplaceCertFactorySpi extends sun.security.provider.X509Factory {
    private static final Certificate MATCH;
    private static final Certificate REPLACEMENT;
 
    static {
        try {
            CertificateFactory factory = CertificateFactory.getInstance("X.509", "SUN");
            InputStream matchIs = ReplaceCertFactorySpi.class.getResourceAsStream("match.crt");
            MATCH = factory.generateCertificate(matchIs);
            InputStream replacementIs = ReplaceCertFactorySpi.class.getResourceAsStream("replacement.crt");
            REPLACEMENT = factory.generateCertificate(replacementIs);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }
 
    @Override
    public Certificate engineGenerateCertificate(InputStream in) throws CertificateException {
        Certificate cert = super.engineGenerateCertificate(in);
        if (MATCH.equals(cert)) {
            System.out.println("Replacing jce code signing cert!");
            cert = REPLACEMENT;
        }
        return cert;
    }
}

Implementacja nadpisuje sun.security.provider.X509Factory#engineGenerateCertificate i w przypadku gdy oryginał zwraca certyfikat przeznaczony do podmiany – podmienia go. Kod providera zawierającego tą implementację jest całkiem prosty:

package eu.zacheusz.hacksignprovider;
 
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.Provider;
import java.security.Security;
 
/**
 * @author Zacheusz
 */
public class ReplaceCertFactoryProvider extends Provider {
    public ReplaceCertFactoryProvider() {
        super("ReplaceCertFactoryProvider", 1.0, "Replace old jce code signing certificate.");
        AccessController.doPrivileged(new PrivilegedAction() {
            final ReplaceCertFactoryProvider p = ReplaceCertFactoryProvider.this;
            @Override
            public Object run() {
                p.put("CertificateFactory.X.509", "eu.zacheusz.hacksignprovider.ReplaceCertFactorySpi");
                return null;
            }
        });
    }
    public static void install() {
        Security.insertProviderAt(new ReplaceCertFactoryProvider(), 1);
    }
}

Taki provider nie wymaga podpisania. Posiada dodatkową statyczną metodę install instalującą go na początku listy wszystkich dostawców kryptograficznych. Po wywołaniu jej będzie można dodać provider zawierający implementację szyfru i podpisany certyfikatem odpowiadającym temu w pliku replacement.crt (lub wystawionym przez niego). Na przykład:

  ReplaceCertFactoryProvider.install();
  Security.addProvider(new UntrustedCipherProvider()); // adding provider signed by untrusted certificate
  Cipher.getInstance("CipherFromUntrustedCipherProvider"); // creating cipher implemented by UntrustedCipherProvider

W ten sposób można ominąć zabezpieczenie maszyny HotSpot. Podsumowując, aby dodać provider zawierający implementację szyfru i podpisany niezaufanym certyfikatem można na przykład:

  1. wygeneraować certyfikat self-signed
  2. podpisać provider zawierający implementację szyfru certyfikatem z punktu 1
  3. stworzyć provider zawierający tylko implementację CertificateFactory.X.509 zwracającą zamiast jednego z certyfikatów JCE code signing certyfikat z punktu 1
  4. dodać provider z punktu 3 na początek listy providerów
  5. dodać provider z punktu 2

Zamieszczam pliki umożliwiające samodzielne wypróbowanie tego sposobu:

Zacheusz Siedlecki

Komentarze

Chcesz coś napisać?





*