BankID-Cava
Ö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:
- Trafikdumpar med innehållet HTTP-requestarna/responserna
- Minnesdump från KVM (torrent). 82 MB gzippat. Här kan man leta krypteringsnycklar för "slumpdatan" och/eller ledtrådar om hur den är skapad.
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]
- NyaDemoBanken - BankID demo sida
- RPG - BankID Relying Party Guidelines (Engelska), specifikation.
- Hur man styr om trafiken till en egen server med iptables.
- Hur man patchar bort certvalideringen i BankID
- AES
- Block Cipher Modes
- Initialization Vector (IV)
- HMAC
- Analys av krypterad uppdaterings-blob från Intel - liknande problem, men ej relaterat till BankID. Beskriver några användbara black box-metoder för att analysera krypterad data.
- Hur man dumpar minne från KVM
- aeskeyfind - söker efter "initialiserade" AES-nycklar i minnesdumpar.
- aesscan - söker efter AES-nycklar för viss krypterad data i minnesdumpar och binärer (väldigt nytt och otestat program).
- PEiD - program för enklare analys av PE(=.exe)-filer. Kan bl.a. söka efter packers och krypteringsalgoritmer.
- LoopyTrace - en hit tracer som halvt fungerar med BankID, men kraschar när vissa funktioner körs.
- DebugDetector - kan användas för att testa om ett program upptäcks av olika anti-debug-tekniker
- Frida - verktyg för reverse engineering som använder s.k. dynamic recompilation. Ska vara gjort för att fungera även med program som använder olika anti-debug-tekniker.