BankID-Cava

Från FriBID
Hoppa till navigeringHoppa till sök

Översikt[redigera]

I nya BankID (från version 5 och uppåt) samt i Mobilt BankID används inte en plugin, utan BankID-programmen/appen startas genom en speciell "bankid:"-URL, och programmet/appen kommunicerar sedan mot BankIDs servrar som i sin tur kommunicerar med förlitande part (m.a.o. tjänsen som använder BankID för inloggning).

Namnet "Cava" förekommer i URL:er m.m. som används i nya versionen, och är tydligen ett kodnamn för nya versionen.

Version 6 av BISP fungerar inte på Windows XP. Kör man Windows XP kan man ladda ner version 5.1.4 istället (och från den versionen kan man uppgradera till 6.0.3). Vidare så går version 5.0.2 att starta i Wine (men jag har inte testat att använda den till något). Här finns en lista med BankID-versioner och nerladdningslänkar

BankID-urler[redigera]

BankID-urler får BankID Säkerhetsprogram att starta och infon i URL:en skickas med till programmet. De ser ut så här:

bankid:///?autostarttoken=11111111-1111-1111-1111-111111111111&redirect=https%3a%2f%2fexample.com%2f

där autostarttoken är ett pseudo-UUID (version 1, inte enligt spec) som genereras av BankIDs servrar, och redirect anger en URL som ska öppnas när inloggningen/signeringen är slutförd.

Kommunikation mot BankIDs servrar[redigera]

BankID Säkerhetsprogram kommunicerar via HTTPS mot cavainternal.bankid.com. Klienten validerar certifikatkedjan ner till rotcertifikatet (dock skickas HTTP request headers redan innan valideringen, vilket kan vara aningen förvirrande). Men man kan patcha bort certvalideringen, vilket möjliggör att man kan styra om trafiken till en lokal server (och därifrån kan man sedan logga trafiken).

Kommunikationen sker genom att binära requestar skickas med POST och svar (också binära) skickas tillbaka som request body. Exempel på request (binärdata visas hexadecimalt):

POST /cava/ HTTP/1.1
User-Agent: BankIDSecurityProgram
Host: cavainternal.bankid.com
Content-Length: 908
Cache-Control: no-cache

00 00 03 88  00 00 00 01  00 00 00 00  < till synes slumpmässig data, 896 bytes >

De första 4 byten verkar vara längden på resten meddelandet (inklusive de följande 8 byten innan "slumpdatan"). "Slumpdatan" är rimligvis någon form av krypterad data. Eftersom längden är är ett jämt antal 16-bytes block så skulle det kunna vara något blockchiffer, t.ex. AES-128/2561, och eftersom hela meddelandet ändras2 vid varje request så används antagligen någon form av IV + något block cipher mode (inte ECB alltså). Se även nästa stycke.

Svaret man får tillbaka är:

HTTP/1.1 200 OK
Connection: Keep-Alive
Content-Length: 1020

00 00 03 f8  00 00 00 01  00 00 00 00  < till synes slumpmässig data, 1008 bytes >

Dvs. samma struktur på meddelandet som vid requesten, men med lite mer data.

Några dumpar av trafiken finns att ladda ner här:

Analys av "slumpdatan"[redigera]

Som sagt skulle "slumpdatan" kunna vara någon form av data som är krypterad med ett blockchiffer som t.ex. AES. Jag har använt en hemmabyggd s.k. hit tracer (LoopyTrace nedan) för att få fram vilka delar av programmets maskinkod som körs när programmet skickar en request. Med denna metod får man givetvis med helt irrelevant kod också, t.ex. för användargränsnittet, och man får inte med polymorfisk (=självmodifierande) kod.

Därefter har jag analyserat maskinkoden som körts med "Krypto Analyser" i programmet PEiD, och fått fram att hash-algoritmerna FORK256/SHA1/SHA256 förekommer i koden (antagligen så används dock bara en av dem, men att de är implementerade i samma funktion -- FORK256 verkar vara en helt utdöd algorithm nämligen, men den är baserad på SHA1). Detta skulle bl.a. kunna betyda:

  • "Slumpdatan" är integritetsskyddad med SHA1/SHA256 eller en HMAC med någon av dessa funktioner.
  • "Slumpdatan" är rent av krypterad med ett stream cipher som bygger på SHA1/SHA256 (ganska osannolikt IMO).
  • Hashningen har inget alls med slumpdatan att göra, utan används t.ex. för kontrollsummor av programmets filer.

Nästa steg skulle kunna vara t.ex. att dumpa minnet varje gång när SHA1/256-funktionen anropas för att få ut funktionsargumenten, eller att använda mer avancerade analysverktyg som t.ex. Frida som kan hantera polymorfisk kod.

BankID.exe innehåller ett par strängar (till synes name-manglade C++-klassnamn) som tyder på att protokollet är krypterat (utöver SSL) och att programmet använder ECDHE till någonting:

ICavaServerKeyHandler
EncryptorCavaServer
crypto::EllipticCurveDiffieHellman

Meddelandetyper[redigera]

Om man listar alla strängar i BankID.exe (över 200 000 st!) så hittar man några som ser ut att vara C++-klassnamn på meddelandetyper i Cava-protokollet. De ligger i namespacet "cava::protobuf", vilket tyder på att BankID använder Google Protobuf för att serialisera meddelanden. För att sålla fram dessa klassnamn kan man använda kommandot nedan:

strings BankID.exe | grep '@protobuf@cava'

Reverse engineering av Mobilt BankID[redigera]

Mobilt BankID för Android (hämta här) har många likheter med vanliga BankID-Cava, en indikation på att delar av protokollet kan vara gemensamt, men är betydligt enklare att reverse engineera. Till exempel kan man köra APK-filen både med eller utan egna modifieringar med hjälp av Chrome App Runtime (instruktioner). Vanliga verktyg för Android utveckling (så som APKTool) kan användas för att disassemblera hela APK filen till smali-kod, vilken man kan modifiera, och sedan assemblera ihop en APK-fil igen.

Den stora fördelen är att alla klasser, funktioner och indelning i Java paket fortfarande existerar, även om de flesta klassnamnen och funktionsnamnen tyvärr saknas (ersatt med a, b, c, osv..). Alla anrop till standard Java biblioteket har dock fulla och kompletta namn.

Mobilt BankID kommunicerar med businternal.bankid.com via TLS (BankID-Cava använder också TLS, men en annan server). TLS lagret kan tas bort helt om man så önskar genom att ersätta SSLSocket med Socket (och ta bort de SSL-specifika anropen på socketen så klart), och byta ut adressen till localhost istället. Bara en klass nämner SSLSocket i smali-koden, du hittar den. Om man hellre vill det kan man enkelt patcha InputStream'en och OutputStream'en för SSLSocket till att sända en plaintext kopia av all kommunikation till en egen server, men låta programmet kommunicera vanligt med businternal.bankid.com.

Innanför TLS sänds all data via ASN.1/DER-kodning istället, vilket skiljer ifrån BankID-Cava som använder HTTP POST. Dock är datan som sänds i likhet med BankID-Cava helt krypterat med någon krypteringsalgoritm.

Genom att iterativt modifiera programmet (till exempel printf-debugging för att se var datan skickas och kommer ifrån, fast skicka till egen server då det inte finns någon konsoll att printf'a till) hittar man snabbt klassen där datan krypteras. Det första paketet till exempel (bortsett från plaintext hello med svar) krypteras med vanlig RSA med padding (RSA/NONE/PKCS1Padding) mot en publik nyckel som ligger hårdkodad. Där är inte någon AES eller något blockchiffer alls involverat i det här steget. Inget har undersökts vidare än, även om så borde vara lika enkelt, men gissningsvis sker en ytterligare handskakning för något inre TLS-liknande men enklare protokoll som BankID folket har implementerat pga alla osäkerheterna i vanliga TLS. Detta innebär att en sessionsnyckel förmodligen negotieras och att ett blockchiffer tar vid (det är inte säkert, de kanske bara kör RSA fram och tillbaka, även om det är kostsamt). Hur och vad bör man kunna se i klassen ifråga. OBS! Klassen ifråga är också ett perfekt ställe att dumpa plaintexten från. Då ser man att innanför detta inre krypteringlager är det riktiga protokollet.

Var den inre krypteringen sker[redigera]

Det första paketet som krypteras med den inre krypteringen skickas in till Lcom/bankid/bus/f/b/c;->b([B)[B, vilket är ett interface för Lcom/bankid/bus/b/a;->b([B)[B. Där hämtas den publika nyckeln och Lcom/bankid/bus/a/a/a;->b([B[B)[B anropas för att kryptera datan. Lcom/bankid/bus/a/a/a;->b([B[B)[B är ett interface för Lcom/a/a/a/a/a;->b([B[B)[B (Detta är smali syntax för en funktion byte[] b(byte[] arg1, byte[] arg2). Det är i klassen Lcom/a/a/a/a/a; (alltså com.a.a.a.a.a i vanlig Java syntax) som all kryptering för det inre krypteringslagret verkar ske.

Där står alla algoritmers namn klart och tydligt i plaintext, och Java standard bibliotek används för själva krypteringen (BouncyCastle alltså, då det är Android). Om det är samma kryptering i BankID-Cava används förmodligen OpenSSL där.

ASN.1 / DER kodning[redigera]

com/bankid/bus/f/c/a = ASN1Boolean
com/bankid/bus/f/c/b = ASN1Integer
com/bankid/bus/f/c/c = CHOICE
com/bankid/bus/f/c/d =   <superklass för alla>
com/bankid/bus/f/c/g = IA5String
com/bankid/bus/f/c/h = PrintableString (rå data, all krypterad data skickas som detta)
com/bankid/bus/f/c/i = PrintableString (ASCII)
com/bankid/bus/f/c/j = SEQUENCE

Patchar[redigera]

Följande patch tar helt bort TLS lagret. Nu kommer du kunna se ett plaintext hello, som din egen server kan svara på, varpå all trafik sedan bara är den inre krypteringen.

Viktigt! Se till att ersätta businternal.bankid.com med localhost också!

--- a/apk_bankid_temp/smali/com/bankid/bus/d/b.smali
+++ b/apk_bankid_temp/smali/com/bankid/bus/d/b.smali
@@ -127,11 +127,9 @@
 
     move-result-object v0
 
-    invoke-virtual {v0}, Ljavax/net/ssl/SSLSocketFactory;->createSocket()Ljava/net/Socket;
+    new-instance v0, Ljavax/net/ssl/SSLSocket;
 
-    move-result-object v0
-
-    check-cast v0, Ljavax/net/ssl/SSLSocket;
+    invoke-direct {v0}, Ljavax/net/ssl/SSLSocket;-><init>()V
 
     iput-object v0, p0, Lcom/bankid/bus/d/b;->e:Ljavax/net/ssl/SSLSocket;
 
@@ -139,116 +137,6 @@
 
     invoke-virtual {v0, v1, p1}, Ljavax/net/ssl/SSLSocket;->connect(Ljava/net/SocketAddress;I)V
 
-    iget-object v0, p0, Lcom/bankid/bus/d/b;->d:Ljava/util/ArrayList;
-
-    invoke-virtual {v0}, Ljava/util/ArrayList;->clear()V
-
-    iget-object v0, p0, Lcom/bankid/bus/d/b;->e:Ljavax/net/ssl/SSLSocket;
-
-    invoke-virtual {v0}, Ljavax/net/ssl/SSLSocket;->getSupportedProtocols()[Ljava/lang/String;
-
-    move-result-object v0
-
-    invoke-static {v0}, Ljava/util/Arrays;->asList([Ljava/lang/Object;)Ljava/util/List;
-
-    move-result-object v0
-
-    const-string v1, "TLSv1.2"
-
-    invoke-interface {v0, v1}, Ljava/util/List;->contains(Ljava/lang/Object;)Z
-
-    move-result v0
-
-    if-eqz v0, :cond_4
-
-    iget-object v0, p0, Lcom/bankid/bus/d/b;->d:Ljava/util/ArrayList;
-
-    const-string v1, "TLSv1.2"
-
-    invoke-virtual {v0, v1}, Ljava/util/ArrayList;->add(Ljava/lang/Object;)Z
-
-    :cond_0
-    :goto_0
-    iget-object v0, p0, Lcom/bankid/bus/d/b;->d:Ljava/util/ArrayList;
-
-    iget-object v1, p0, Lcom/bankid/bus/d/b;->d:Ljava/util/ArrayList;
-
-    invoke-virtual {v1}, Ljava/util/ArrayList;->size()I
-
-    move-result v1
-
-    new-array v1, v1, [Ljava/lang/String;
-
-    invoke-virtual {v0, v1}, Ljava/util/ArrayList;->toArray([Ljava/lang/Object;)[Ljava/lang/Object;
-
-    move-result-object v0
-
-    check-cast v0, [Ljava/lang/String;
-
-    iget-object v1, p0, Lcom/bankid/bus/d/b;->e:Ljavax/net/ssl/SSLSocket;
-
-    array-length v2, v0
-
-    if-lez v2, :cond_1
-
-    invoke-virtual {v1, v0}, Ljavax/net/ssl/SSLSocket;->setEnabledProtocols([Ljava/lang/String;)V
-
-    :cond_1
-    iget-object v0, p0, Lcom/bankid/bus/d/b;->c:Ljava/util/ArrayList;
-
-    invoke-virtual {v0}, Ljava/util/ArrayList;->clear()V
-
-    iget-object v0, p0, Lcom/bankid/bus/d/b;->e:Ljavax/net/ssl/SSLSocket;
-
-    invoke-virtual {v0}, Ljavax/net/ssl/SSLSocket;->getSupportedCipherSuites()[Ljava/lang/String;
-
-    move-result-object v0
-
-    invoke-static {v0}, Ljava/util/Arrays;->asList([Ljava/lang/Object;)Ljava/util/List;
-
-    move-result-object v0
-
-    const-string v1, "TLS_RSA_WITH_AES_256_CBC_SHA"
-
-    invoke-interface {v0, v1}, Ljava/util/List;->contains(Ljava/lang/Object;)Z
-
-    move-result v0
-
-    if-eqz v0, :cond_6
-
-    iget-object v0, p0, Lcom/bankid/bus/d/b;->c:Ljava/util/ArrayList;
-
-    const-string v1, "TLS_RSA_WITH_AES_256_CBC_SHA"
-
-    invoke-virtual {v0, v1}, Ljava/util/ArrayList;->add(Ljava/lang/Object;)Z
-
-    :cond_2
-    :goto_1
-    iget-object v0, p0, Lcom/bankid/bus/d/b;->c:Ljava/util/ArrayList;
-
-    iget-object v1, p0, Lcom/bankid/bus/d/b;->c:Ljava/util/ArrayList;
-
-    invoke-virtual {v1}, Ljava/util/ArrayList;->size()I
-
-    move-result v1
-
-    new-array v1, v1, [Ljava/lang/String;
-
-    invoke-virtual {v0, v1}, Ljava/util/ArrayList;->toArray([Ljava/lang/Object;)[Ljava/lang/Object;
-
-    move-result-object v0
-
-    check-cast v0, [Ljava/lang/String;
-
-    iget-object v1, p0, Lcom/bankid/bus/d/b;->e:Ljavax/net/ssl/SSLSocket;
-
-    array-length v2, v0
-
-    if-lez v2, :cond_3
-
-    invoke-virtual {v1, v0}, Ljavax/net/ssl/SSLSocket;->setEnabledCipherSuites([Ljava/lang/String;)V
-
-    :cond_3
     invoke-static {}, Lcom/bankid/bus/f/b/e;->a()Lcom/bankid/bus/f/b/e;
 
     move-result-object v0
@@ -281,10 +169,6 @@
 
     invoke-direct {p0, v0}, Lcom/bankid/bus/d/b;->a(I)V
 
-    iget-object v0, p0, Lcom/bankid/bus/d/b;->e:Ljavax/net/ssl/SSLSocket;
-
-    invoke-virtual {v0}, Ljavax/net/ssl/SSLSocket;->startHandshake()V
-
     invoke-direct {p0, v6}, Lcom/bankid/bus/d/b;->a(I)V
 
     return-void
@@ -301,141 +185,6 @@
     invoke-direct {v1, v0}, Ljava/io/IOException;-><init>(Ljava/lang/String;)V
 
     throw v1
-
-    :cond_4
-    iget-object v0, p0, Lcom/bankid/bus/d/b;->e:Ljavax/net/ssl/SSLSocket;
-
-    invoke-virtual {v0}, Ljavax/net/ssl/SSLSocket;->getSupportedProtocols()[Ljava/lang/String;
-
-    move-result-object v0
-
-    invoke-static {v0}, Ljava/util/Arrays;->asList([Ljava/lang/Object;)Ljava/util/List;
-
-    move-result-object v0
-
-    const-string v1, "TLSv1.1"
-
-    invoke-interface {v0, v1}, Ljava/util/List;->contains(Ljava/lang/Object;)Z
-
-    move-result v0
-
-    if-eqz v0, :cond_5
-
-    iget-object v0, p0, Lcom/bankid/bus/d/b;->d:Ljava/util/ArrayList;
-
-    const-string v1, "TLSv1.1"
-
-    invoke-virtual {v0, v1}, Ljava/util/ArrayList;->add(Ljava/lang/Object;)Z
-
-    goto/16 :goto_0
-
-    :cond_5
-    iget-object v0, p0, Lcom/bankid/bus/d/b;->e:Ljavax/net/ssl/SSLSocket;
-
-    invoke-virtual {v0}, Ljavax/net/ssl/SSLSocket;->getSupportedProtocols()[Ljava/lang/String;
-
-    move-result-object v0
-
-    invoke-static {v0}, Ljava/util/Arrays;->asList([Ljava/lang/Object;)Ljava/util/List;
-
-    move-result-object v0
-
-    const-string v1, "TLSv1"
-
-    invoke-interface {v0, v1}, Ljava/util/List;->contains(Ljava/lang/Object;)Z
-
-    move-result v0
-
-    if-eqz v0, :cond_0
-
-    iget-object v0, p0, Lcom/bankid/bus/d/b;->d:Ljava/util/ArrayList;
-
-    const-string v1, "TLSv1"
-
-    invoke-virtual {v0, v1}, Ljava/util/ArrayList;->add(Ljava/lang/Object;)Z
-
-    goto/16 :goto_0
-
-    :cond_6
-    iget-object v0, p0, Lcom/bankid/bus/d/b;->e:Ljavax/net/ssl/SSLSocket;
-
-    invoke-virtual {v0}, Ljavax/net/ssl/SSLSocket;->getSupportedCipherSuites()[Ljava/lang/String;
-
-    move-result-object v0
-
-    invoke-static {v0}, Ljava/util/Arrays;->asList([Ljava/lang/Object;)Ljava/util/List;
-
-    move-result-object v0
-
-    const-string v1, "TLS_RSA_WITH_AES_128_CBC_SHA"
-
-    invoke-interface {v0, v1}, Ljava/util/List;->contains(Ljava/lang/Object;)Z
-
-    move-result v0
-
-    if-eqz v0, :cond_7
-
-    iget-object v0, p0, Lcom/bankid/bus/d/b;->c:Ljava/util/ArrayList;
-
-    const-string v1, "TLS_RSA_WITH_AES_128_CBC_SHA"
-
-    invoke-virtual {v0, v1}, Ljava/util/ArrayList;->add(Ljava/lang/Object;)Z
-
-    goto/16 :goto_1
-
-    :cond_7
-    iget-object v0, p0, Lcom/bankid/bus/d/b;->e:Ljavax/net/ssl/SSLSocket;
-
-    invoke-virtual {v0}, Ljavax/net/ssl/SSLSocket;->getSupportedCipherSuites()[Ljava/lang/String;
-
-    move-result-object v0
-
-    invoke-static {v0}, Ljava/util/Arrays;->asList([Ljava/lang/Object;)Ljava/util/List;
-
-    move-result-object v0
-
-    const-string v1, "SSL_RSA_WITH_3DES_EDE_CBC_SHA"
-
-    invoke-interface {v0, v1}, Ljava/util/List;->contains(Ljava/lang/Object;)Z
-
-    move-result v0
-
-    if-eqz v0, :cond_8
-
-    iget-object v0, p0, Lcom/bankid/bus/d/b;->c:Ljava/util/ArrayList;
-
-    const-string v1, "SSL_RSA_WITH_3DES_EDE_CBC_SHA"
-
-    invoke-virtual {v0, v1}, Ljava/util/ArrayList;->add(Ljava/lang/Object;)Z
-
-    goto/16 :goto_1
-
-    :cond_8
-    iget-object v0, p0, Lcom/bankid/bus/d/b;->e:Ljavax/net/ssl/SSLSocket;
-
-    invoke-virtual {v0}, Ljavax/net/ssl/SSLSocket;->getSupportedCipherSuites()[Ljava/lang/String;
-
-    move-result-object v0
-
-    invoke-static {v0}, Ljava/util/Arrays;->asList([Ljava/lang/Object;)Ljava/util/List;
-
-    move-result-object v0
-
-    const-string v1, "DES-CBC3-SHA"
-
-    invoke-interface {v0, v1}, Ljava/util/List;->contains(Ljava/lang/Object;)Z
-
-    move-result v0
-
-    if-eqz v0, :cond_2
-
-    iget-object v0, p0, Lcom/bankid/bus/d/b;->c:Ljava/util/ArrayList;
-
-    const-string v1, "DES-CBC3-SHA"
-
-    invoke-virtual {v0, v1}, Ljava/util/ArrayList;->add(Ljava/lang/Object;)Z
-
-    goto/16 :goto_1
 .end method
 
 .method public final b()Z

Fotnoter[redigera]

  • ^1: Både AES-128 och AES-256 använder 16-bytes block (och AES-192, men den används väldigt sällan). Numret anger nyckellängden, inte blockstorleken.
  • ^2: Det finns i alla fall inga bitar som alltid är 1 eller alltid 0 i request-datan (jag har dock ej kollat response-datan). Detta kan verifieras med t.ex. detta skript med parametrarna --and och --or med 10-15 eller fler dumpar: https://github.com/samuellb/scripts/blob/master/bit_op.py

Se även[redigera]