W skrócie. Niezależna, kontradyktoryjna analiza statyczna produkcyjnego pakietu APK. Zamiast orzekać o bezpieczeństwie aplikacji bądź o jej rzekomej wadliwości, przedstawiam wyłącznie to, co wynika z samego artefaktu — popierając każde twierdzenie ścieżką pliku i numerem linii oraz jawnie oddzielając fakt statyczny od wniosku i od hipotezy wymagającej testu na urządzeniu. Wyłania się z tego obraz kodu dojrzałego i utwardzonego warstwowo, obciążonego wszakże jedną spójną słabością na granicy zaufania: podpisane dokumenty bywają przyjmowane bez weryfikacji wystawcy, a poufność bywa mylona z uwierzytelnieniem. Działającego exploita nie prezentuję; wskazuję natomiast zestaw testów P0, które tę wątpliwość ostatecznie rozstrzygną.
Stan dokumentu: raport z analizy statycznej, rewizja 1 z 25 sierpnia 2026 r.
Autor: Marek Wesołowski (WESMAR), 2026
Badany pakiet: pl.nask.mobywatel, 4.87.2 (6426), versionCode 56416
Cel: niezależna weryfikacja techniczna artefaktu
Zasada oceny: ani domniemanie bezpieczeństwa, ani domniemanie winy; wnioski zależą od dowodów i jawnie określonego modelu zagrożeń.
Publicznie udostępniony materiał, który można przypisać projektowi Android, zawiera 192 pliki Kotlin. Przegląd drzewa wykazał, że jest to przede wszystkim warstwa interfejsu i system komponentów wizualnych. W tym materiale nie znaleziono:
settings.gradle;AndroidManifest.xml;Oznacza to, że publiczny materiał nie wystarczał do przeprowadzenia audytu zachowania aplikacji produkcyjnej. Pozostałą część trzeba było odtworzyć z kodu DEX i zasobów podpisanego APK. Jest to uczciwe i użyteczne źródło informacji o wykonywanej binarce, ale nie jest odpowiednikiem pełnego kodu źródłowego: dekompilator traci nazwy, typy i część struktury sterowania, a niektóre funkcje Kotlin/coroutines rekonstruuje błędnie.
W konsekwencji:
Publikacja części interfejsu użytkownika może wspierać przejrzystość wzornictwa, lecz nie umożliwia niezależnej kontroli właściwości bezpieczeństwa produktu. Jest to ograniczenie audytowalności, a nie samo w sobie dowód podatności.
Zamknięcie pozostałej części implementacji nie jest kontrolą bezpieczeństwa. Uniemożliwia ono zewnętrznym badaczom sprawdzenie założeń, odtworzenie kompilacji i szybkie wykrywanie regresji. Równocześnie sama niejawność kodu nie stanowi dowodu, że znajduje się w nim konkretna podatność — taki zarzut nadal wymaga śladu technicznego. W niniejszej pracy brak przejrzystości traktowany jest więc jako obniżenie poziomu niezależnego zapewnienia, a nie jako zamiennik analizy.
Wstępny przegląd tego artefaktu wskazał kilka realnie niebezpiecznych konstrukcji obecnych w APK, przypisał im jednak wpływ i dostępność, których kod nie potwierdza. Najpoważniejsze błędy to:
gstatic.com z możliwością podmiany fontów, JavaScriptu i dowolnych zasobów;ksRandomizer jako losowania aliasu klucza;android:autoVerify dla własnego schematu URI, mimo że weryfikacja Android App Links dotyczy adresów HTTP/HTTPS;Jednocześnie pierwotny raport pominął rzeczywistą słabość mechanizmu przejrzystości certyfikatów (CT):
Lista dzienników CT jest pobierana po HTTP i wprawdzie poprawnie weryfikowana podpisem Google, ale aplikacja nie egzekwuje jej świeżości, wersji, stanu dziennika ani przedziału czasowego. Aktywny napastnik sieciowy może więc ponownie podawać starszą, nadal poprawnie podpisaną listę.
Wpływ jest warunkowy. Samo ponowne podanie starszej listy nie przełamuje TLS i nie pozwala wstrzyknąć dowolnej treści. Może jednak osłabić dodatkową kontrolę CT w scenariuszu, w którym napastnik dysponuje już certyfikatem akceptowanym przez system oraz SCT pochodzącym z dziennika, któremu aplikacja nie powinna już ufać. Oceniono to jako ryzyko średnie dotyczące obrony warstwowej; nie jest to samodzielne przejęcie sesji.
Poza tym potwierdzono:
base.apk, zgodny z odciskiem opublikowanym przez domenę mobywatel.gov.pl;FLAG_SECURE w wariancie produkcyjnym;TrustAll, permisywny HostnameVerifier oraz SslErrorHandler.proceed() pozostawione w produkcyjnym APK, lecz bez wykazanego przełącznika dostępnego zwykłej aplikacji trzeciej, użytkownikowi lub napastnikowi sieciowemu;state i wymienia kod z code_verifier=null; równolegle obecna nowsza implementacja stosuje losowy state oraz PKCE S256, co stanowi zarówno dobry wzorzec, jak i dowód realnej luki regresyjnej między wariantami;Content-Disposition może zostać włączona bezpośrednio do ścieżki w pamięci współdzielonej, bez normalizacji i kontroli pozostania w katalogu Downloads; daje to warunkowy zapis nowego pliku poza katalogiem docelowym, ale kod nie nadpisuje pliku już istniejącego, a rzeczywistą kontrolę nazwy przez innego użytkownika należy potwierdzić testem;FileProvider i uprawnienia systemowe na eksportowanych komponentach bibliotek;FLAG_SECURE, istotne w kontekście wcześniejszego CVE dotyczącego podglądu danych w przełączniku aplikacji systemu iOS; test dynamiczny regresji Androida nadal jest konieczny.Inwentaryzacja łańcucha dostaw ujawniła 324 zadeklarowane moduły Maven, w tym 28 artefaktów oznaczonych alpha lub beta. Trzy spakowane moduły Bouncy Castle 1.83 są objęte znanymi poprawkami bezpieczeństwa; automatyczne zapytanie OSV wskazało trzy komunikaty, a ręczna kontrola not projektu źródłowego ujawniła dalsze pozycje pominięte przez to źródło danych. Poprawiona wersja 1.84 była dostępna od 28 kwietnia 2026 r., czyli około trzy miesiące przed badanym wydaniem. Aplikacja rzeczywiście rejestruje dostawcę kryptograficznego BC i używa CMS, PKCS#12 oraz ASN.1, więc nie jest to martwa zależność.
Nie znaleziono wywołania konkretnych mechanizmów GOST, LDAP, Composite Signature ani FrodoKEM poza kodem biblioteki. Jest to potwierdzony dług aktualizacyjny i potencjalna powierzchnia podatności, ale nie dowód osiągalnej eskalacji tymi czterema CVE. Znaleziono natomiast odrębny wzorzec aplikacyjny: klient tworzy i zapisuje obiekty CMSSignedData, a następnie odczytuje getSignedContent() bez znalezionej lokalnej weryfikacji podpisujących. Brak obejmuje więc zidentyfikowane punkty przyjęcia i wykorzystania danych, a nie tylko ekran prezentacji. Wpływ zależy od tego, czy napastnik może dostarczyć lub zmienić kontener, ale hipoteza wymaga testu P0, ponieważ treść zasila modele dokumentów.
Ocena procesu inżynierskiego jest krytyczna. W produkcie przetwarzającym państwową tożsamość nie powinno być konieczne tłumaczenie, dlaczego kod TrustAll nie może trafić do wydania produkcyjnego, dlaczego aktywnie używany dostawca kryptograficzny z opublikowaną od kwietnia aktualizacją nie powinien pozostać w lipcowym wydaniu ani dlaczego SignedData musi być zweryfikowane przed użyciem treści. Każdy z tych problemów osobno może mieć warunkowy wpływ; razem świadczą o braku wystarczająco twardych bram jakości artefaktu. Publiczne udostępnienie wyłącznie warstwy interfejsu użytkownika dodatkowo uniemożliwia społeczną weryfikację, czy bramy takie istnieją i działają.
Nie ma podstaw do oceny, ile środków finansowych zostało zmarnowanych ani kto personalnie odpowiada za decyzje — badany materiał nie zawiera budżetów, umów, historii przeglądów ani wyjątków ryzyka. Można natomiast uczciwie stwierdzić, że rezultat techniczny i zakres publikacji nie odpowiadają poziomowi audytowalności oczekiwanemu od krytycznej aplikacji publicznej.
Nie ma podstaw do ogólnego komunikatu „aplikacja jest bezpieczna”. Wynik jest migawką analizy statycznej wersji 4.87.2. Najważniejsze rozstrzygnięcia wymagające testu urządzeniowego opisano w sekcji 12.
| Właściwość | Wynik |
|---|---|
| Archiwum | mObywatel.apkm |
| SHA-256 archiwum | 534df84d382f05a28a0f1fb6ff48950a1e182e3f126644e495ce945f873b1078 |
| Badana binarka | decompiling/base.apk |
SHA-256 base.apk |
1ede2b031ffb7e5036308dda8ad08c98ccd0d30316c3336cbf7b93caf86ecb72 |
| Pakiet | pl.nask.mobywatel |
| Wersja | 4.87.2 (6426) |
| VersionCode | 56416 |
| Min / target / compile API | 26 / 36 / 36 |
| Data wpisana w metadanych APKM | 29 lipca 2026 r. |
Weryfikator Android apksig 9.3.2 potwierdził integralność base.apk:
verified = true
v1 = false
v2 = true
v3 = true
v4 = false
Certyfikat podpisujący:
Subject/Issuer: CN=Android, OU=Android, O=Google Inc.,
L=Mountain View, ST=California, C=US
SHA-256: CB:4F:1E:A4:F0:BE:4A:91:EA:80:34:97:DD:A6:9F:31:
84:5C:9A:98:F4:39:03:C5:60:17:CF:4A:B8:77:D8:9F
Sam ogólny podmiot O=Google Inc. nie dowodzi autentyczności aplikacji. Rozstrzygający jest fakt, że identyczny odcisk dla pakietu pl.nask.mobywatel publikuje oficjalny plik Digital Asset Links. Potwierdza to użycie klucza autoryzowanego przez domenę mObywatel. Nie dowodzi natomiast, że plik o podanym hashu jest obecnie identyczny z egzemplarzem pobieranym z każdego kanału dystrybucji; do takiego twierdzenia potrzebne byłoby bezpośrednie porównanie artefaktów.
fallback);| Oznaczenie | Znaczenie |
|---|---|
| Potwierdzone | Zachowanie wynika bezpośrednio z artefaktu lub zostało odtworzone eksperymentalnie. |
| Częściowo potwierdzone | Mechanizm istnieje, ale pełny wpływ wymaga warunku lub testu dynamicznego. |
| Nieudowodnione | Hipoteza jest możliwa, ale obecny materiał nie wykazuje ścieżki wykorzystania. |
| Obalone | Kod lub eksperyment przeczy twierdzeniu raportu pierwotnego. |
Ocena zakłada przede wszystkim zwykłego napastnika sieciowego lub złośliwą aplikację bez uprawnień administratora systemu (root). Uzyskanie takich uprawnień, instrumentacja Frida, modyfikacja prywatnego katalogu aplikacji i przepakowanie APK są omawiane osobno, ponieważ napastnik o takich możliwościach może bezpośrednio przechwytywać dane procesu i omijać kontrolę TLS bez używania flag aplikacji.
| ID | Ustalenie | Ocena | Pewność |
|---|---|---|---|
| MOB-A-01 | Możliwość podstawienia starszej, prawidłowo podpisanej listy dzienników CT pobieranej po HTTP | Średnie | Potwierdzone zachowanie; wpływ warunkowy |
| MOB-A-02 | Ścieżki akceptacji połączenia mimo błędu TLS lub WebView pozostawione w binarce produkcyjnej | Niskie | Potwierdzone istnienie; brak zwykłej ścieżki aktywacji |
| MOB-A-03 | Własne schematy URI przenoszące tokeny: kolizja procedur obsługi i możliwość dostarczenia jawnego Intentu | Niskie / do testu | Częściowo potwierdzone |
| MOB-A-04 | Aktywnie używany Bouncy Castle 1.83 mimo wcześniejszej dostępności 1.84; źródło danych OSV nie obejmuje pełnej listy projektu źródłowego | Średnie operacyjne / P0 | Obecność i użycie potwierdzone; wpływ poszczególnych CVE wymaga potwierdzenia osiągalności kodu |
| MOB-A-05 | Przyjęcie, zapis i odczyt CMSSignedData bez znalezionej lokalnej weryfikacji podpisujących |
Średnie / P0 do testu | Zachowanie potwierdzone; wektor dostarczenia warunkowy |
| MOB-A-06 | Globalna, stała ośmiobajtowa sól PBKDF2 dla pakietów aktywacyjnych legitymacji studenckiej | Niskie–Średnie / P0 do charakterystyki | Stała sól i parametry KDF potwierdzone; entropia kodu i wykonalność ataku poza aplikacją wymagają testu |
| MOB-A-08 | Eksportowalny klucz prywatny certyfikatu UNIVERSITY i hasło PKCS#12 współlokowane w pakiecie aktywacyjnym; zapis nieopakowanego PKCS#8 w SQLCipher | Średnie, warunkowo Wysokie po P0 | Cykl życia klucza potwierdzony; odzyskanie poza aplikacją i skutki użycia klucza wymagają testu |
| MOB-A-09 | Starszy wariant OAuth e-Doręczeń bez state i PKCE, pozostawiony jako ścieżka wybierana flagą serwerową |
Średnie / warunkowe | Brak obu zabezpieczeń potwierdzony; aktywny wariant i praktyczne wstrzyknięcie kodu wymagają P0 |
| MOB-A-10 | Nazwa załącznika z Content-Disposition używana bez kanonikalizacji w bezpośrednio budowanej ścieżce zapisu na API 26–28 |
Niskie–Średnie / P1 do testu | Łańcuch klienta potwierdzony; kontrola nazwy przez atakującego i wpływ serwera warunkowe |
| MOB-O-01 | Zdalne logowanie pełnego URL dla części profili HTTP może ujawnić parametry zapytania | Obserwacja | Nieudowodniony wrażliwy przepływ |
| MOB-O-02 | FileProvider obejmuje całą pamięć zewnętrzną, choć nie jest eksportowany |
Utwardzenie | Potwierdzone, bez wykazanego wycieku |
| MOB-O-03 | Rozbudowany zakres tagów urządzenia i zdarzeń bezpieczeństwa wysyłanych do Sentry | Prywatność / do testu | Zakres konfiguracji potwierdzony |
| MOB-O-04 | Brak znalezionej ochrony aplikacyjnej przed pełnym i częściowym zasłonięciem ekranów operacji wrażliwych | Utwardzenie / do testu | Brak mechanizmów potwierdzony statycznie; skuteczna akcja przez nakładkę nieudowodniona |
Nie nadano osobnych identyfikatorów dla rzekomego wstrzykiwania czcionek/JavaScriptu, zdalnej zmiany flagi TLS, „losowania aliasu” przez ksRandomizer ani programowego przejścia na słabszy wariant bez StrongBox jako CWE-326, ponieważ te tezy są odpowiednio obalone lub nieudowodnione.
Identyfikator MOB-A-07 pozostaje zarezerwowany dla potencjalnego rozwinięcia MOB-A-03 dotyczącego mobywatel://eqsig. Analiza statyczna potwierdza zewnętrzny token i nieweryfikowalny własny schemat URI, ale bez testu wiązania tokenu z sesją, walidacji po stronie serwera i ekranu zgody nie wykazano jeszcze wpływu niezależnego od już opisanego MOB-A-03.
Potencjalne ustalenie nazwane w materiale porównawczym „WebView akceptuje połączenie mimo błędu TLS i zezwala na dostęp do plików” zostało scalone z wcześniejszym MOB-A-02, ponieważ opisuje tę samą klasę granicy WebView i nie powinno sztucznie zwiększać liczby wyników. Identyfikator MOB-A-09 w niniejszym raporcie oznacza niezależną wadę transakcyjnego wiązania OAuth opisaną w sekcji 8.2.
Ocena: średnia, mechanizm obrony w głąb
Atakujący: aktywny napastnik na ścieżce sieciowej
Warunki wpływu: dodatkowo certyfikat akceptowany przez system oraz SCT z dziennika, który po wydaniu starszej listy utracił zaufanie albo zmienił dopuszczalny przedział
Pewność: potwierdzona akceptacja starszej listy; wpływ eksploatacyjny zależny od PKI i historii dzienników
Konfiguracja zezwala na HTTP do gstatic.com:
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">gstatic.com</domain>
</domain-config>
Dowód: network_security_config.xml.
Jedyny aktywny literał HTTP do tej domeny znaleziony w zasobach aplikacji to:
http://www.gstatic.com/ct/log_list/v3/log_list.zip
Dowód: strings.xml.
Kod pobiera ZIP profilem NonCTRaw, wyodrębnia log_list.json i log_list.sig, ograniczając ich wielkość odpowiednio do 1 MiB i 512 B. Następnie weryfikuje podpis kluczem publicznym osadzonym w APK. Dowody: iy3/c.java, iy3/c.java, iy3/c.java.
Pierwsza standardowa dekompilacja funkcji weryfikującej była błędna i sugerowała akceptację nieważnego podpisu. Dekompilacja tej samej klasy w trybie zapasowym (fallback) wykazała przeciwnie:
Bieżący ZIP pobrany podczas badania miał SHA-256:
164e3b2368fcb84763bbb6fb4c3839d53d69c2ca469515365bd0e762613b50ce
Weryfikacja log_list.sig kluczem z APK dała Verified OK. To obala hipotezę, że napastnik może podmienić listę dowolną treścią.
Problem dotyczy świeżości. DTO zawiera pola version i log_list_timestamp, ale w badanej implementacji nie znaleziono ich użycia do kontroli monotoniczności ani maksymalnego wieku. Dowód: LogListDto.java. Model pojedynczego dziennika pomija ponadto pola bieżącego formatu, takie jak stan dziennika i przedział czasowy; przechowuje głównie identyfikator i klucz. Dowód: LogDto.java.
Po poprawnej weryfikacji aplikacja zapisuje lokalny czas pobrania jako cacheTime. W efekcie historyczny, lecz prawidłowo podpisany dokument dostarczony ponownie po HTTP wygląda lokalnie jak świeżo pobrany. Dowód: iy3/c.java.
gstatic.com.log_list_timestamp oraz monotoniczny wzrost version.temporal_interval względem czasu SCT/certyfikatu.Ocena: niska jako higiena i odporność produktu
Pewność: istnienie kodu potwierdzone; dostępność dla zwykłego napastnika niepotwierdzona i w standardowej konfiguracji zaprzeczona
Konfiguracja klienta HTTP pobiera podczas działania flagę HTTP_CLIENT_SSL. Jeżeli jest fałszywa, fabryka ustawia permisywny HostnameVerifier i TrustAllX509TrustManager. Dowód: pl/gov/coi/common/network/o.java.
Wspólny WebViewClient wywołuje SslErrorHandler.proceed(), gdy odpowiadająca mu konfiguracja SSL jest wyłączona. Dowód: t80/h.java.
Ten klient jest wspólny dla wielu ekranów. Ustawia parametry dokładnie z obiektu konfiguracji: dostęp do plików, JavaScript, pamięć DOM, treść mieszaną i pamięć podręczną: t80/l.java. Domyślna klasa bazowa ma restrykcyjne wartości: JavaScript false, pamięć DOM false, allowFileAccess=false oraz mixedContentMode=1, czyli MIXED_CONTENT_NEVER_ALLOW: t80/p.java. Konkretne ekrany świadomie rozszerzają te uprawnienia.
| Rodzina WebView | SSL | JavaScript | Pamięć DOM | Dostęp do plików | Treść mieszana | Ocena statyczna |
|---|---|---|---|---|---|---|
płatności i zapis karty (e32, mt3, s32) |
na stałe true |
true |
domyślnie false |
false |
NEVER_ALLOW |
poprawne odrzucenie przy błędzie |
podglądy i proste treści (qu3) |
na stałe true |
true |
true |
false |
NEVER_ALLOW |
rozsądne, bez mostu Java |
OAuth e-Doręczeń / Keycloak (a22, ys3) |
lokalna flaga WEB_VIEW_SSL |
true |
true |
false |
NEVER_ALLOW |
bezpieczne przy produkcyjnym true; krytyczny skutek błędnego wariantu |
usługi online (f43) |
lokalna flaga WEB_VIEW_SSL |
true |
true |
true |
NEVER_ALLOW |
zbędne rozszerzenie powierzchni; nie wykazano samodzielnego odczytu pliku |
Ścieżka usług internetowych pobiera zdalny URL, obsługuje POST, pobieranie plików oraz systemowy wybór pliku, a konfiguracja ustawia allowFileAccess=true: f43/b.java, e43/u.java. Samo to ustawienie nie przełamuje zasady wspólnego pochodzenia i w tym materiale nie wykazuje odczytu lokalnego pliku przez zdalną stronę. Jest jednak niepotrzebnym uprawnieniem dla komponentu przetwarzającego treść z sieci. Należy je wyłączyć i testować nawigacje file://, content://, przekierowania oraz systemowy wybór pliku.
Wspólny klient buduje komunikaty diagnostyczne z pełnego URL i po onPageFinished pobiera całe document.body.outerHTML: t80/h.java. W badanym wariancie produkcyjnym nie jest to potwierdzony wyciek: wywołania trafiają na poziom diagnostyczny do q00.a, którego metody debug/info odrzucają treść, a dopiero błąd jest przekazywany do Sentry: q00/a.java. To pozytywny wynik dla badanego odbiornika logów, ale kod powinien przestać konstruować pełny DOM i URL, ponieważ podmiana implementacji mechanizmu logowania w innym wariancie natychmiast zmieni tę właściwość bezpieczeństwa.
Przypinanie certyfikatów jest także zależne od flag, a profil NonCTRaw ma dodatkową ścieżkę akceptacji mimo błędu: jeśli nie uda się utworzyć oczekiwanego menedżera zaufania, używa wariantu permisywnego. Dotyczy to danych podpisanych na poziomie aplikacji, m.in. listy CT; nie należy automatycznie przenosić wpływu na główne API.
Wartości produkcyjne są bezpieczne:
HTTP_CLIENT_SSL = true
CERTIFICATE_TRANSPARENCY = true
HTTP_CLIENT_CERT_PINNING = true
WEB_VIEW_SSL = true
WEB_VIEW_DEBUGGING = false
Dowód wartości domyślnych: a04/c.java.
Lokalne nadpisania trafiają do prywatnych SharedPreferences. Manifest wyłącza backup, a reguły ekstrakcji wykluczają dane. Zwykła aplikacja trzecia i napastnik sieciowy nie mogą zapisać tego pliku.
Repozytorium flag składa wartości domyślne z lokalnymi wpisami i nie pobiera WEB_VIEW_SSL z listy flag serwerowych: e04/b.java. Rozgałęzienie f04.e ma osobne wyjątki dla NURSE_CARD, ale dla WEB_VIEW_SSL po sprawdzeniu profilu audytowego/testowego odczytuje właśnie lokalne repozytorium. Co istotne, profile isSecurityAudit i isAutomaticTest wymuszają dla tej flagi false; jest to celowe ułatwienie testów MITM, a nie zdalny przełącznik produkcji. Taki wyjątek nadal powinien istnieć wyłącznie w osobnym artefakcie testowym.
Kod ekranu deweloperskiego istotnie jest skompilowany, ale produkcyjny graf zależności wstrzykuje implementację modułu zarządzającego, której isAvailable() zwraca false i która nie wykonuje nawigacji. Główna aktywność nie eksportuje osobnego ekranu deweloperskiego. Sama obecność oq1.m nie dowodzi osiągalności interfejsu.
Stąd klasyfikacja Wysokie/CWE-295 jako zdalnie sterowalnego obejścia zabezpieczeń jest nieuzasadniona. Konstrukcja nadal jest zła z perspektywy obrony warstwowej: błąd wariantu, błędne wstrzyknięcie zależności lub przyszła regresja może aktywować kod akceptujący połączenie mimo błędu.
TrustAll, permisywny HostnameVerifier i SslErrorHandler.proceed() z wariantu produkcyjnego przez rozdzielenie zestawów źródeł;allowFileAccess=false we wszystkich WebView sieciowych oraz dodać jawne listy dozwolonych schematów i hostów dla nawigacji uprzywilejowanych;SslErrorHandler.proceed();Manifest eksportuje MainActivity dla:
mobywatel://app/institutions;mobywatel://eqsig;https://api.mobywatel.gov.pl/application/diia_confirmed.Dowód: AndroidManifest.xml.
Procedura obsługi institutions odczytuje parametr qrCode. Wewnątrz aplikacji sprawdza akcję VIEW, obecność URI i wyłącznie to, czy ostatni segment ścieżki jest równy institutions; nie porównuje ponownie schematu ani hosta: bk3/e.java. Filtr manifestu ogranicza zwykłe rozpoznanie niejawnego Intentu do mobywatel://app/institutions, lecz dowolna aplikacja może skierować jawny Intent do eksportowanej MainActivity z innym schematem lub hostem i pasującym ostatnim segmentem. To nie dowodzi przejęcia procesu: qrCode uruchamia przebieg weryfikacji/logowania i nie znaleziono lokalnego pominięcia autoryzacji. Jest jednak konkretną luką walidacji granicy wejściowej, którą trzeba zamknąć przed analizą jednorazowości tokenu po stronie serwera.
Procedura obsługi podpisu kwalifikowanego jest pod tym względem lepsza: sprawdza oczekiwany schemat i host, odczytuje token i rozpoczyna proces potwierdzenia tożsamości. Nie wykazano, aby samo dostarczenie wartości omijało logowanie, zgodę użytkownika lub walidację po stronie serwera.
Ryzyka własnego schematu są dwa:
MainActivity z przygotowanym URI.android:autoVerify nie rozwiązuje tego dla mobywatel://, ponieważ mechanizm zweryfikowanych linków aplikacyjnych Android App Links obejmuje domeny HTTP/HTTPS. Raport pierwotny zalecał więc środek technicznie niedopasowany.
Zalecenie: przenieść przebiegi zawierające tokeny do zweryfikowanych linków HTTPS, stosować token jednorazowy z krótkim czasem ważności (TTL), wiązaniem z odbiorcą, stanem i sesją (audience/state/session) oraz walidacją po stronie serwera. Do czasu testu z aplikacją kolidującą nie należy przedstawiać tego jako potwierdzonego przejęcia konta lub podpisu.
state i PKCEOcena: średnia, warunkowa
Pewność: brak mechanizmów po stronie klienta potwierdzony; aktywność wariantu i skuteczność ataku wymagają testu P0
Klasa skutku: CSRF podczas logowania, wstrzyknięcie kodu autoryzacyjnego lub pomylenie sesji, bez statycznie wykazanego przejęcia konta
APK zawiera starszą implementację autoryzacji e-Doręczeń oraz nowsze mechanizmy autoryzacyjne. Starsza ścieżka y12.t uruchamia URL edorUrl bez dodania transakcyjnego state ani code_challenge: y12/t.java. Wywołanie zwrotne akceptuje każdy URL o schemacie mobywatel, którego ścieżka zaczyna się od /edelivery, pobiera sam parametr code i przekazuje go do wymiany tokenów; host, state i powiązanie z rozpoczętą transakcją nie są sprawdzane: y12/t.java.
Łańcuch nie urywa się na warstwie interfejsu użytkownika. Stara ścieżka tworzy sn0.x.Params(tokenUrl, code, null), więc codeVerifier jest jawnie pusty: y12/t.java. Repozytorium przekazuje tę wartość do pola formularza code_verifier punktu końcowego tokenów: zn0/y.java, tn0/h.java. Nie znaleziono alternatywnej wartości jednorazowej (nonce) ani lokalnego sekretu transakcyjnego w tym wariancie.
Nowszy moduł Keycloak obecny w tej samej aplikacji pokazuje wzorzec, który powinien obowiązywać wszędzie: generuje sekrety transakcji, dopisuje state, code_challenge i code_challenge_method=S256, porównuje zwrócony state, a do punktu końcowego tokenów przekazuje codeVerifier: vs3/p.java, vs3/p.java, vs3/p.java. Jest to dobra implementacja oraz silny dowód, że brak w y12.t nie wynika z ograniczenia platformy. Analiza statyczna nie pozwala utożsamić każdego wywołania vs3.p z następcą konkretnego ekranu y12.t, dlatego porównanie dotyczy własności zabezpieczeń, a nie deklaracji pełnej równoważności tras.
Wybór między starszym WebView a nowym modułem natywnym odbywa się przez serwerową flagę NATIVE_LOGIN_EDOR_MODULE_ANDROID: przypadek użycia zwraca false, jeżeli flagi brakuje albo jest nieaktywna, a interfejs użytkownika wybiera wtedy właśnie y12.t: xy1/i0.java, i64/c6.java. Analiza statyczna nie ujawnia bieżącej wartości flagi dla konkretnego użytkownika, regionu lub grupy wdrożeniowej. Dlatego nie wolno pisać, że wszyscy użytkownicy produkcyjni są obecnie podatni; równie nieuczciwe byłoby uznanie starego wariantu za martwy tylko dlatego, że istnieje nowszy.
Aktualne dobre praktyki OAuth (BCP) wymagają ochrony klienta przed CSRF i wstrzyknięciem kodu. Klient publiczny powinien używać PKCE, a gdy ochrona CSRF nie wynika z poprawnie związanego PKCE/OIDC nonce, powinien stosować jednorazowy state związany z przeglądarką lub innym programem pośredniczącym użytkownika. Źródło normatywne: RFC 9700, sekcja 2.1 i 2.1.1.
Warunek podniesienia oceny: w teście P0 należy wymusić starszy wariant flagą serwerową, rozpocząć dwie równoległe transakcje na kontrolowanych kontach i sprawdzić, czy kod z transakcji A można wstrzyknąć do wywołania zwrotnego B lub dostarczyć przez kontrolowaną nawigację WebView. Należy także potwierdzić dokładne dopasowanie zwrotnego URI po stronie serwera, jednorazowość kodu, wiązanie kodu z klientem oraz zachowanie przy braku lub powtórzeniu state. Do tego czasu raport stwierdza brak wymaganych zabezpieczeń klienta, a nie udane przejęcie sesji.
Korekta: usunąć starszy wariant albo nadać mu ten sam generator sekretów i walidator co nowej ścieżce; wymusić PKCE S256 bez możliwości obniżenia zabezpieczeń, jednorazowy state, dokładną kontrolę schematu, hosta i ścieżki, jedną aktywną transakcję na przebieg oraz atomowe zużycie wywołania zwrotnego.
Przechwytywacz dla części profili zewnętrznych przekazuje do zdalnego mechanizmu logowania pełny URL, potencjalnie wraz z parametrami zapytania. Konfiguracja produkcyjna włącza zdalne logowanie HTTP. Nie znaleziono jednak jeszcze potwierdzonego wywołania, w którym sekret lub dane osobowe przechodzą tą konkretną ścieżką. Jest to hipoteza do testu przepływowego, a nie gotowa podatność.
Zalecenie: globalnie usuwać parametry zapytania, fragment adresu, nagłówki autoryzacyjne i identyfikatory z telemetrii; utrzymywać listę dozwolonych pól diagnostycznych.
FileProviderDostawca treści nie jest eksportowany i wymaga jawnego nadania uprawnienia do URI, co jest poprawną granicą Androida: AndroidManifest.xml. Jego konfiguracja deklaruje jednak external-path path=".", czyli korzeń współdzielonej pamięci zewnętrznej, zamiast wydzielonego podkatalogu: provider_paths.xml. Sam wpis nie udostępnia plików bez przyznania uprawnienia i nie jest dowodem wycieku. Powoduje natomiast, że błąd w miejscu budującym URI lub nadającym czasowe uprawnienie może mieć znacznie większy zasięg niż potrzebny.
Zalecenie: zawęzić ścieżki dostawcy do wydzielonych katalogów i typów plików oraz zinwentaryzować wszystkie miejsca wywołujące getUriForFile, grantUriPermission i FLAG_GRANT_*_URI_PERMISSION. Test negatywny powinien potwierdzić, że nie da się utworzyć ani skutecznie nadać URI dla pliku spoza katalogu eksportu.
Produkcja inicjalizuje Sentry własnym kodem; automatyczna inicjalizacja manifestowa jest wyłączona. Konfiguracja nadaje zdarzeniom między innymi tagi:
X_SESSION_ID, poziom StrongBox, obciążenie CPU i wykryte zagrożenia.Dowód: q00/d.java. Mechanizm logowania przyjmuje także komunikaty, wyjątki i ślady zdarzeń. Zestaw programistyczny (SDK) zawiera integracje pomiarowe dla uruchomienia aplikacji, Compose, baz danych, plików, Logcat i OkHttp. Sama obecność integracji w metadanych nie dowodzi, że każda jest włączona ani że przesyła treść danych. Biblioteka odtwarzania sesji (Session Replay) również znajduje się w pakiecie, lecz nie znaleziono konfiguracji aplikacyjnej potwierdzającej aktywne rejestrowanie sesji; nie przypisano go więc aplikacji.
Moduł wykrywania zagrożeń przekazuje do mechanizmu logowania zdarzenia dotyczące uprawnień root, emulatora i podatności BlueBorne oraz nazwę i identyfikator pakietu wykrytego złośliwego oprogramowania. Dowód: h83/c.java. Dane te mogą być uzasadnione ochroną przed nadużyciami, ale stanowią osobną granicę prywatności i powinny być objęte minimalizacją, retencją, kontrolą dostępu oraz jawną dokumentacją celu.
Nie znaleziono dowodu, że wrażliwa treść dokumentów jest wysyłana do Sentry. Jednocześnie przechwytywacz opisany w MOB-O-01 zapisuje pełny URL, dlatego brak wycieku musi zostać potwierdzony testem ruchu i zdarzeń, a nie założony na podstawie intencji implementacji.
Pełne przeszukanie źródeł i manifestu nie znalazło filterTouchesWhenObscured, setFilterTouchesWhenObscured, obsługi FLAG_WINDOW_IS_PARTIALLY_OBSCURED, Window.setHideOverlayWindows() ani deklaracji android.permission.HIDE_OVERLAY_WINDOWS. Produkcyjne FLAG_SECURE ogranicza przechwytywanie obrazu, ale nie jest kontrolą przyjmowania dotyku przez zasłonięte okno. Oficjalne zalecenie Androida rozróżnia pełne i częściowe zasłonięcie: nowszy system ogranicza część pełnych nakładek, lecz dla częściowego zasłonięcia nie ma ochrony domyślnej. Android Developers — ataki przez zasłanianie interfejsu (Tapjacking).
W aplikacji zawierającej potwierdzenia związane z tożsamością i podpisem jest to brak obrony warstwowej. Analiza statyczna nie dowodzi jednak, że nakładka potrafi uruchomić konkretną operację bez czytelnej zgody, biometrii, PIN-u lub walidacji serwera. Dlatego MOB-O-04 pozostaje obserwacją do testu, a nie stwierdzoną podatnością.
Zalecenie: dla całego okna czynności wrażliwych na Androidzie 12+ zadeklarować HIDE_OVERLAY_WINDOWS i użyć setHideOverlayWindows(true); niezależnie od wersji odrzucać dotknięcia oznaczone pełnym lub częściowym zasłonięciem na przyciskach potwierdzenia. Testy muszą objąć pełną nakładkę, przezroczystą nakładkę poniżej progu systemowego, częściowe zasłonięcie i umieszczenie aktywności między oknami napastnika (activity sandwich), z kontrolą dostępności dla prawidłowych narzędzi asystujących.
Downloads na API 26–28Ocena: Niskie–Średnie, warunkowe
Atakujący: nadawca lub inny podmiot mogący wpłynąć na nazwę pobieranego załącznika; alternatywnie przejęte zaplecze serwerowe
Warunki: Android 8–9 (API 26–28), zgoda na zapis w pamięci zewnętrznej oraz zaakceptowanie przez usługę nazwy zawierającej segmenty ścieżki
Pewność: potwierdzony brak walidacji po stronie klienta; zdalna sterowalność nazwy i skuteczność wobec produkcyjnej usługi wymagają testu
Łańcuch zaczyna się w odpowiedzi HTTP. Parser Content-Disposition wybiera filename= albo dekoduje filename*=, lecz nie usuwa separatorów katalogów, .. ani znaków sterujących: q10/a.java. Repozytorium e-Doręczeń przekazuje wynik bez zmiany do FileDto, a następnie DomainFile; w razie braku nagłówka używa bezpiecznej nazwy domyślnej: wn0/d.java, un0/a.java. Wspólny przypadek użycia rozdziela nazwę wyłącznie wokół ostatniej kropki i nie przeprowadza normalizacji ścieżki: ww3/l.java.
Standardowy przebieg JADX pominął główną metodę zapisu w ww3/m.java; ponowna dekompilacja tej samej klasy z --show-bad-code odtworzyła jawny warunek Build.VERSION.SDK_INT >= 29. Na API 29+ aplikacja używa MediaStore z relative_path=Downloads: r20/e.java. Na API 26–28 — które nadal mieszczą się w deklarowanym minSdkVersion=26 — buduje natomiast ścieżkę tekstową i otwiera ją przez FileOutputStream: ww3/m.java, AndroidManifest.xml, r20/e.java. Bazą jest publiczny katalog Downloads: r20/b.java. Funkcja składa go operatorem tekstowym jako Downloads + "/" + fileName + "." + extension, bez getCanonicalFile() i sprawdzenia prefiksu: ww3/c.java.
Przykładowa nazwa ../Pictures/probe.pdf prowadzi więc w starszej gałęzi do ścieżki równoważnej Download/../Pictures/probe.pdf. Manifest żąda WRITE_EXTERNAL_STORAGE, a przypadek użycia poprzedza zapis prośbą o to uprawnienie: AndroidManifest.xml, ww3/l.java. Kod sprawdza istnienie celu i dodaje przyrostek (n), zatem w przeanalizowanym przebiegu nie potwierdzono nadpisywania istniejącego pliku. Potwierdzona własność ogranicza się do możliwości utworzenia nowego pliku w innym istniejącym katalogu pamięci współdzielonej, w granicach uprawnień starego Androida. Nie oznacza zapisu do prywatnego obszaru innej aplikacji ani wykonania kodu.
Test P1: na emulatorach API 26 i 28 wysłać w kontrolowanym środowisku załączniki o nazwach zwykłych oraz ../Pictures/probe.pdf, ..%2FPictures%2Fprobe.pdf w filename*=, z wieloma segmentami ../, separatorem odwrotnym i nazwą już istniejącą. Należy zapisać zarówno odpowiedź serwera, jak i rzeczywistą ścieżkę pliku. Test rozstrzyga, czy serwer zachowuje nazwę nadawcy, normalizuje ją lub odrzuca przed zwróceniem klientowi. Na API 29+ trzeba osobno potwierdzić zachowanie _display_name; nie należy automatycznie przenosić wyniku ze starej gałęzi.
Naprawa: potraktować nazwę z sieci wyłącznie jako etykietę. Odrzucać /, \\, segmenty ./.., NUL, znaki sterujące i nieakceptowane rozszerzenia; ograniczyć długość i zastosować listę dozwolonych znaków. Dla gałęzi plikowej obliczyć kanoniczny katalog bazowy i cel, a następnie wymagać, by cel pozostawał jego potomkiem. Najlepiej usunąć starszą gałąź bezpośredniego budowania ścieżki wraz z końcem wsparcia API 26–28 albo użyć bezpiecznego API systemowego również tam. Walidację trzeba wykonać niezależnie po stronie serwera i klienta.
CERT Polska opisał CVE-2025-11598 jako błąd wersji iOS starszych niż 4.71.0, w których dane mogły pozostać widoczne w systemowym przełączniku aplikacji po zakończeniu sesji. NVD klasyfikuje problem jako CWE-359 i niski poziom CVSS 4. Nie jest to CVE Androida i nie wolno przedstawiać statycznego sprawdzenia Androida jako potwierdzenia naprawy błędu iOS.
W Androidzie 4.87.2 znaleziono jednak właściwy wzorzec ochronny:
SECURE_WINDOW ma wartość produkcyjną true;8192, czyli WindowManager.LayoutParams.FLAG_SECURE;Dowód: a04/c.java, y10/x.java.
Jest to dobra implementacja statyczna. Nie zastępuje testu listy ostatnich aplikacji, zrzutów ekranu i nagrywania obrazu na urządzeniach oraz wersjach Androida, zwłaszcza przez cały cykl: aktywna sesja → tło → wylogowanie → wymuszone zamknięcie → ponowne uruchomienie.
Potwierdzone dobre praktyki:
AndroidKeyStore;KeyInfo.securityLevel i raportowanie poziomu.Korekty:
ksRandomizer jest przekazywane do setRandomizedEncryptionRequired(). Nie losuje aliasu klucza;StrongBox.PREFERRED celowo dopuszcza ponowienie generacji bez StrongBox po ProviderException;SOFTWARE;Zalecenie: formalnie określić, które klucze wymagają co najmniej TEE, które StrongBox, a które mogą działać programowo. Warunek powinien być egzekwowany w punkcie emisji/użycia klucza, jeżeli bezpieczeństwo protokołu faktycznie od tego zależy.
| Priorytet | Test | Kryterium pozytywne |
|---|---|---|
| P0 | Ponowne podanie historycznej, prawidłowo podpisanej listy CT przez kontrolowany punkt dostępowy lub serwer pośredniczący | aplikacja odrzuca cofnięcie wersji lub oznacza listę jako przeterminowaną |
| P0 | Lista CT z błędnym podpisem i uszkodzony ZIP | brak zastąpienia ostatniej znanej poprawnej wersji; brak pętli awarii; alert telemetryczny bez danych użytkownika |
| P0 | Zrzut ekranu, nagrywanie obrazu i ekran ostatnich aplikacji na Androidzie 8, 14 i 16 | brak obrazu dokumentu i danych na każdym etapie sesji oraz po wylogowaniu |
| P0 | Kompilacja produkcyjna przy błędzie certyfikatu API/WebView | połączenie zawsze odrzucone; brak przejścia po błędzie SSL |
| P0 | CMS: zły podpis, brak podpisujących, obcy certyfikat, zły OID/typ, obca koperta z nieufnym CMS oraz wstrzyknięcie na wejściu | odrzucenie przed zapisem i przed utworzeniem modelu interfejsu użytkownika |
| P0 | KDF pakietu studenckiego: dwa kontrolowane pakiety, polityka kodu i pomiar słownika poza aplikacją | unikalna losowa sól każdego pakietu; udokumentowana entropia, TTL i koszt ataku; brak praktycznej walidacji zgadywanego kodu poza aplikacją |
| P0 | Pełny cykl klucza UNIVERSITY: kontrolowany pakiet, odzyskanie poza aplikacją, import PKCS#12, zapis bazy, użycie i unieważnienie | brak transportu eksportowalnego klucza; klucz nieeksportowalny w magazynie kluczy; serwer odrzuca klucz po unieważnieniu; brak sekretów w stercie i logach |
| P0 | OAuth e-Doręczeń w starszym y12.t: dwie równoległe sesje, obcy kod, brak lub powtórzenie state, obniżenie ochrony PKCE i wywołanie zwrotne o obcym hoście lub ścieżce |
starszy wariant usunięty albo każde wywołanie zwrotne związane z właściwą transakcją przez PKCE S256 i jednorazowy state; obce wartości odrzucone przed punktem końcowym tokenów |
| P1 | Złośliwa aplikacja rejestrująca mobywatel:// |
brak ujawnienia użytecznego tokenu; przebieg odporny na przechwycenie i spreparowanie danych |
| P1 | Jawny Intent do MainActivity z błędnym hostem, ścieżką lub tokenem |
odrzucenie przed zmianą stanu; brak awarii i wycieku diagnostycznego |
| P1 | WebView usług internetowych: file://, content://, przekierowania, systemowy wybór pliku i złośliwy HTML |
allowFileAccess=false; nawigacja spoza listy dozwolonej zablokowana; brak odczytu plików i nadmiernych uprawnień do URI |
| P1 | Nazwy załączników e-Doręczeń z ../, separatorami i zakodowanym filename*= na API 26, 28 i 29 |
nazwa odrzucona lub sprowadzona do bezpiecznej etykiety; cel kanoniczny zawsze pozostaje w Downloads |
| P1 | Inspekcja Sentry przez kontrolowane punkty końcowe | URL i zdarzenia nie zawierają tokenów, numeru PESEL, danych dokumentu ani parametrów zapytania |
| P1 | Macierz urządzeń bez TEE / TEE / StrongBox | poziom klucza zgodny z wymaganiami i czytelna polityka przejścia na wariant zastępczy |
| P1 | Atak przez zasłanianie ekranów logowania, zgody i podpisu: pełna lub częściowa nakładka oraz aktywność między oknami napastnika | dotyk w zasłoniętym oknie odrzucony; brak wykonania operacji; poprawne narzędzie dostępności nadal działa zgodnie z polityką |
| P2 | Nadawanie URI przez FileProvider |
uprawnienie obejmuje wyłącznie oczekiwany plik i wygasa po użyciu |
Testy powinny być wykonane na dokładnie tym samym certyfikowanym artefakcie wydania, dla którego zostaną opublikowane skrót kryptograficzny i versionCode. Wyniki negatywne także należy zachować w raporcie — brak powodzenia określonej hipotezy jest istotnym wynikiem audytu.
Nagłówek pierwotnego pliku przypisuje autorstwo nazwie „Antigravity Research”. Nie jest to autor ani podmiot niniejszego audytu. Prawidłowe autorstwo raportu rewizyjnego: Marek Wesołowski (WESMAR), 2026.
| Teza pierwotna | Werdykt | Uzasadnienie |
|---|---|---|
| APK jest podpisany oficjalnym kluczem | Potwierdzone po uzupełnieniu dowodu | Odcisk certyfikatu odpowiada oficjalnemu assetlinks.json; sam DN Google był niewystarczający. |
| HTTP dla gstatic pozwala wstrzykiwać czcionki/JS/zasoby | Obalone | Jedyny aktywny odbiorca to podpisana lista CT; błędny podpis jest odrzucany. |
| HTTP dla gstatic jest poprawne i bez konsekwencji | Obalone | Możliwe jest podstawienie starszej, poprawnie podpisanej listy; brak kontroli świeżości i stanu. |
sslEnabled można przełączyć przez produkcyjny ekran deweloperski |
Obalone dla standardowej produkcji | Kod interfejsu istnieje, ale produkcyjny moduł zarządzający jest niedostępny i brak eksportowanego wejścia. |
Modyfikacja prywatnego XML przez root wyłącza TLS |
Technicznie prawdziwe, ale mylący model zagrożeń | Uprawnienia root lub Frida oznaczają już kontrolę nad procesem; nie jest to zdalne ryzyko wysokie. |
| Obecność TrustAll w produkcji jest obojętna | Obalone | To niepotrzebna ścieżka akceptacji mimo błędu i ryzyko regresji, choć bez wykazanego zwykłego sposobu jej uruchomienia. |
| Przypinanie certyfikatów stanowi osobną średnią podatność | Nieudowodnione / duplikat | Wyłącza się w tej samej uprzywilejowanej ścieżce flagi co TLS. |
ksRandomizer losuje alias |
Obalone | Steruje setRandomizedEncryptionRequired(). |
| Aplikacja blokuje krytyczne funkcje bez StrongBox/TEE | Nieudowodnione | Znaleziono zamierzone przejście PREFERRED na wariant zastępczy i raportowanie poziomu, a nie ogólną blokadę. |
| AndroidX/Tink chroni krytyczną bazę dokumentów | Nieudowodnione | Obecność biblioteki nie dowodzi konkretnego przepływu danych. |
autoVerify należy dodać do mobywatel:// |
Technicznie błędne zalecenie | Android App Links weryfikuje domenowe linki HTTP/HTTPS. |
| Odnośniki kierujące do aplikacji nie wymagają dalszych testów | Obalone | Własny schemat URI z tokenem wymaga testu kolizji, spreparowania danych i walidacji po stronie serwera. |
| Analiza była „pełna” | Obalone opisem zakresu | Nie wykonano testów dynamicznych, zaplecza serwerowego ani pełnej analizy kodu natywnego; źródła publiczne są niekompletne. |
Uczciwy audyt obejmuje również kontrole działające poprawnie:
android:allowBackup="false" i reguły tworzenia kopii oraz ekstrakcji wykluczające dane aplikacji;android:usesCleartextTraffic="false", z jednym jawnie opisanym wyjątkiem;FLAG_SECURE domyślnie aktywne w produkcji;addJavascriptInterface w przejrzanym kodzie;allowFileAccess=false, zablokowana treść mieszana) oraz twarde sslEnabled=true w ekranach płatności;debug/info nie utrwala treści pełnego URL ani DOM budowanego przez wspólny WebView; nie znaleziono zdalnej wysyłki tych komunikatów;state oraz PKCE S256 i odrzuca wywołanie zwrotne z niezgodnym state;PendingIntent we własnych powiadomieniach i sprawdzonych przebiegach interfejsu; odziedziczona funkcja pomocnicza rejestracji Google tworzy osobny mutowalny token do fikcyjnego pakietu, dlatego nie zaliczono całej powierzchni bezwarunkowo do dobrych wzorców;FileProvider nieeksportowany;Te elementy zmniejszają ryzyko, ale nie unieważniają ustaleń MOB-A-01, MOB-A-02 ani rozbieżności między wariantami opisanej jako MOB-A-09.
Przykładowe polecenia:
sha256sum mObywatel.apkm decompiling/base.apk
apksigner verify --verbose --print-certs decompiling/base.apk
curl --fail --silent --show-error \
https://api.mobywatel.gov.pl/.well-known/assetlinks.json
rg -n 'http://[^" ]+' src/resources src/sources
rg -n 'getVersion\(|getLogListTimestamp\(' src/sources
rg -n 'filterTouchesWhenObscured|setFilterTouchesWhenObscured|\
FLAG_WINDOW_IS_PARTIALLY_OBSCURED|setHideOverlayWindows|HIDE_OVERLAY_WINDOWS' \
src/sources src/resources/AndroidManifest.xml
jadx --single-class iy3.c \
--single-class-output /tmp/iy3_c_fallback.java \
--decompilation-mode fallback decompiling/base.apk
unzip -p log_list.zip log_list.json > log_list.json
unzip -p log_list.zip log_list.sig > log_list.sig
openssl dgst -sha256 -verify gstatic_public_key.pem \
-signature log_list.sig log_list.json
Należy zapisać wersje narzędzi, pełne dane wyjściowe, czas UTC pobrania zasobu zdalnego oraz skróty kryptograficzne wszystkich plików dowodowych. Plik z kluczem publicznym w ostatnim poleceniu powinien być zbudowany bezpośrednio z wartości gstatic_public_key badanego APK.
Metadane Sentry w APK zawierają listę 324 dokładnych współrzędnych Maven. Jest to użyteczny substytut wykazu składników oprogramowania (SBOM), lecz nie pełny dokument CycloneDX/SPDX: nie podaje relacji bezpośrednia/przechodnia, zakresu użycia, skrótów plików JAR ani modułów własnych. Dowód: sentry-external-modules.txt.
Wśród składników odnotowano między innymi:
| Składnik | Wersja w APK | Znaczenie audytowe |
|---|---|---|
Bouncy Castle bcprov / bcpkix / bcutil-jdk15to18 |
1.83 | aktywnie używany dostawca kryptograficzny, CMS, PKCS#12 i ASN.1; znane CVE |
| Tink Android | 1.8.0 | stara wersja nie dowodzi użycia w krytycznym przepływie |
| Conscrypt | 2.5.3 | Java/JNI TLS; wymaga osobnej kontroli kodu natywnego |
| SQLCipher Android | 4.10.0 | natywna baza; obecność nie dowodzi zakresu szyfrowanych danych |
| OkHttp | 4.12.0 oraz 2.7.2 | dwa pokolenia API zwiększają koszt utrzymania |
| Sentry Android | 8.22.0 | telemetria kodu Java i natywnego |
| RootBeer | 0.1.2 | wykrywanie uprawnień root/JNI; mechanizm ten nie stanowi granicy zaufania |
| Protobuf | 4.29.3 | parser danych binarnych |
Lista zawiera 28 artefaktów alpha/beta, głównie Compose 1.11.0-beta02, Material3 1.5.0-alpha18, Navigation3 1.1.0-alpha01 i ODML Image beta. Wersja przedprodukcyjna nie jest podatnością sama w sobie. W aplikacji państwowej powinna jednak wymagać jawnego wyjątku ryzyka, testów regresji i terminu przejścia na wydanie stabilne.
Zapytania do OSV dla wszystkich 324 współrzędnych, wykonane 25 sierpnia 2026 r., zwróciły trzy wpisy dotyczące dwóch z trzech spakowanych modułów Bouncy Castle:
| Moduł | Komunikat / CVE | Mechanizm | Wersja naprawiona | Osiągalność kodu w aplikacji |
|---|---|---|---|---|
bcpkix-jdk15to18:1.83 |
GHSA-wg6q-6289-32hp / CVE-2026-5588 | roboczy CompositeVerifier akceptuje pustą sekwencję podpisów |
1.84 | klasa obecna; brak wywołania spoza BC |
bcprov-jdk15to18:1.83 |
GHSA-574f-3g2m-x479 / CVE-2025-14813 | licznik GOST 28147/G3413 CTR zawija się i powtarza strumień klucza | 1.84 | klasa obecna; brak użycia GOST spoza BC |
bcprov-jdk15to18:1.83 |
GHSA-c3fc-8qff-9hwx / CVE-2026-0636 | wstrzyknięcie danych w LDAPStoreHelper |
1.84 | klasa obecna; brak konfiguracji/wywołania LDAP spoza BC |
Ręczna kontrola oficjalnej noty 1.84 wykazała, że pojedyncze źródło danych było niepełne. Projekt źródłowy wymienia także CVE-2026-3505 oraz CVE-2026-5598:
bcpg i pakiet org.bouncycastle.openpgp nie występują w badanym APK, więc ten wpis nie jest przypisywany aplikacji;bcprov, lecz nie znaleziono referencji aplikacyjnej do Frodo/FRODO/frodokem; osiągalność nie została wykazana.Przeszukanie 45 452 zdekompilowanych plików Java wykazało symbole GOST, LDAP, Composite Signature i Frodo wyłącznie pod org/bouncycastle/**. Jest to negatywny wynik osiągalności kodu na poziomie jawnych odwołań, a nie matematyczny dowód nieosiągalności: dostawca JCA może być wybierany nazwą algorytmu, refleksją albo przez dane wejściowe.
Nie wolno natomiast uznać całej zależności za martwą. Aplikacja tworzy i rejestruje BouncyCastleProvider, generuje oraz odczytuje CMS, używa magazynu PKCS12 dostawcy BC i bezpośrednio odczytuje ASN.1. Przykłady: y10/d0.java, kh0/m.java, dh2/a.java, ez/e0.java. Oznacza to, że dla błędów CMS/PKIX/PKCS#12/ASN.1 sama obecność aktywnego odbiorcy jest potwierdzona, a rozstrzygnięcie musi zejść do konkretnych metod i danych wejściowych.
Bazowa ocena CVE opisuje najgorsze użycie wadliwego komponentu; nie jest automatycznie oceną badanej aplikacji. W szczególności krytyczna ocena CVE-2025-14813 wymaga rzeczywistego użycia trybu GOST CTR z warunkami ponownego użycia strumienia; takiego przebiegu nie wykazano. Z drugiej strony pozostawienie znanego podatnego dostawcy kryptograficznego w aplikacji tożsamościowej jest nieakceptowalnym długiem, ponieważ przyszłe lub słabo widoczne miejsce użycia może uczynić kod osiągalnym.
Wersja 1.84, która naprawiała powyższe problemy, została wydana 28 kwietnia 2026 r. Badana aplikacja trafiła do dystrybucji około 23–29 lipca 2026 r. Pozostawienie 1.83 nie może więc zostać wyjaśnione tym, że poprawka pojawiła się dopiero po zamknięciu kompilacji. To co najmniej niewystarczająca dyscyplina aktualizacji komponentu kryptograficznego albo brak wykazanego procesu akceptacji ryzyka.
Projekt źródłowy opublikował następnie 28 lipca 2026 r. Bouncy Castle 1.85, którego nota wymienia 32 dalsze poprawki CVE w całym produkcie, a 7 sierpnia poprawki dostawcy 1.85.2. Nie oznacza to, że wszystkie wpisy dotyczą użytych modułów albo przebiegów mObywatela. Ponieważ aplikacja używa CMS, PKCS#12 i ASN.1, priorytetowego rozpoznania wymagają między innymi klasy błędów CMS, walidacji RSA/certyfikatów, limitów ASN.1 i kosztu KDF. Nota wydania Bouncy Castle 1.85, bieżące pliki i noty wydania.
Zalecenie: przejść na najnowszy zgodny zestaw stabilnych artefaktów po testach współdziałania; usunąć nieużywane moduły i algorytmy przez minimalizację; generować podpisany SBOM dla każdego wydania; uruchamiać analizę składu oprogramowania (SCA) przy budowie i ponownie codziennie dla już wydanych wersji; łączyć co najmniej OSV/GHSA, NVD i komunikaty producenta; dla każdego CVE zapisywać decyzję „osiągalny / nieosiągalny / nierozstrzygnięty” wraz z dowodem. Samo podbicie numeru wersji bez testów nie zamyka ustalenia.
W wielu ścieżkach aplikacja tworzy CMSSignedData z danych binarnych i natychmiast pobiera getSignedContent().getContent(). Globalne wyszukanie nie znalazło poza kodem biblioteki wywołań getSignerInfos(), SignerInformation.verify(), verifySignatures() ani budowy SignerInformationVerifier. Najprostszy dekoder domenowy wykonuje wyłącznie:
return UTF_8.decode(ByteBuffer.wrap(
(byte[]) new CMSSignedData(byteArray).getSignedContent().getContent()
)).toString();
Dowód: y10/f0.java. Ten dekoder zasila repozytoria dokumentów. Analogiczny wzorzec występuje w ih2/a.java oraz starszym magazynie storage/g.java. Wprost odczytywane w ten sposób są między innymi zakresy prawa jazdy i legitymacji studenckiej: d64/l.java, d64/l.java. Inna ścieżka najpierw odszyfrowuje CMSEnvelopedData kluczem użytkownika, a następnie tylko odczytuje wewnętrzne obiekty CMSSignedData: d64/l.java.
Brak nie ogranicza się do ścieżki odczytu. Publiczna metoda repozytorium b0(...) przekazuje otrzymany ciąg do K1(...); zależnie od trybu B0(...) albo A0(...) odszyfrowuje CMS EnvelopedData, tworzy wewnętrzne CMSSignedData, a następnie zapisuje ich getEncoded() do kontenera. Sprawdzana jest liczba elementów albo dozwolona nazwa zakresu (scope), lecz nie podmiot podpisujący. Dowody: d64/l.java, d64/l.java, d64/l.java. Analiza statyczna oznacza tu brak weryfikacji zarówno na zidentyfikowanej granicy przyjęcia danych, jak i później przy budowaniu modeli. Nie przesądza natomiast, kto może wywołać tę granicę z kontrolowaną treścią.
Osobny, istotny wariant dotyczy aktywacji legitymacji studenckiej. Pakiet pochodzi z punktu końcowego FrontSrv/rs/post/getPackage: uo0/b.java, yo0/b.java. Flaga MOB_DB_CONTAINERS, której wartości domyślne w artefakcie wynoszą false/false, przełącza całą implementację aktywacji pomiędzy ey3.a i wy3.a: a04/c.java, u54/y5.java. W starszej ścieżce packageData jest dekodowane i odszyfrowywane hasłem, po czym A1(...) wyciąga fullStudentData jako CMSSignedData i porównuje PESEL bez weryfikacji podpisu: d64/l.java, d64/l.java. Alternatywna implementacja również tworzy obiekty CMSSignedData z mapy pakietu bez znalezionej kontroli podpisujących: ey3/b.java. Flaga zmienia więc zaplecze magazynu i wykonanie aktywacji, ale w zbadanych wariantach nie usuwa omawianego braku.
CMSSignedData jest parserem i kontenerem; konstruktor ani getSignedContent() nie ustanawiają zaufania do podpisu. Dokumentacja projektu źródłowego pokazuje osobne pobranie getSignerInfos() i wywołanie signer.verify(...); zaznacza też, że nawet taki prosty przykład nie sprawdza jeszcze ważności certyfikatu. Bouncy Castle API — CMSSignedData. Prawidłowa walidacja wymaga co najmniej obecności oczekiwanej liczby podpisujących, weryfikacji kryptograficznej, budowy i polityki łańcucha certyfikatów, EKU/OID polityki, czasu, unieważnienia oraz związania podpisującego z typem dokumentu i środowiskiem.
Potwierdzone: klient przyjmuje i zapisuje w zidentyfikowanych przebiegach oraz odczytuje treść oznaczoną jako CMS SignedData bez znalezionej lokalnej weryfikacji podpisujących. Globalny wynik negatywny obejmuje również brak getSignerInfos(), SignerInformation.verify(), verifySignatures() i budowy SignerInformationVerifier poza biblioteką.
Niepotwierdzone: zwykły napastnik może samodzielnie dostarczyć taki kontener do repozytorium. Dane są chronione przez izolację aplikacji, TLS i w części przebiegów CMS EnvelopedData. Ta koperta kryptograficzna używa AES256_CBC i certyfikatu odbiorcy: bh2/a.java, dh2/a.java. Zapewnia poufność, lecz nie uwierzytelnia nadawcy ani nie jest trybem AEAD. Podmiot, który pozyska publiczny certyfikat odbiorcy, może technicznie zbudować skierowaną do niego kopertę; APK nie dowodzi jednak, że podmiot nieufny może pozyskać właściwy certyfikat i dostarczyć wynik do b0(...).
Nie należy mieszać tego z zewnętrzną warstwą pakietu aktywacyjnego legitymacji: w starszej implementacji C0/G0 używa hasła i AES/CBC/PKCS5Padding, a kod dopina IV na końcu szyfrogramu i nie pokazuje znacznika uwierzytelniającego: d64/l.java, d64/l.java, ch2/a.java, ch2/c.java. Niezależny ślad w nowej implementacji prowadzi od packageData i hasła przez generator klucza PBE do odszyfrowania wariantem z IvParameterSpec, a następnie do utworzenia CMSSignedData: ey3/b.java, ey3/b.java, y10/k.java, ey3/b.java. Jest to drugi rodzaj mechanizmu poufności bez potwierdzonej integralności kryptograficznej, obecny w obu implementacjach aktywacji; praktyczna modyfikacja nadal wymaga spełnienia formatu, dopełnienia, znajomości hasła lub kontrolowanej zmiany szyfrogramu oraz warunków dostarczenia pakietu. Dlatego błąd może stać się istotny przy przejęciu kanału, błędzie serwera, podatnym imporcie lub zapisie lokalnym, lecz nie wykazano samodzielnego zdalnego wektora.
Parametry KDF zewnętrznego pakietu aktywacyjnego, w tym potwierdzoną stałą sól PBKDF2, wydzielono jako niezależne ustalenie MOB-A-06. Nie są one dowodem braku weryfikacji CMS i nie należy sztucznie sumować obu wad, choć w jednym przepływie mogą wpływać na ten sam pakiet danych.
Aplikacja ma ogólny weryfikator Signature.initVerify()/verify() w y10/c.java, ale znaleziony odbiorca używa go do podpisu listy dzienników CT: iy3/c.java. Nie jest to gotowa implementacja polityki CMS: nie buduje łańcucha ani nie sprawdza EKU/OID, czasu i unieważnienia. Pokazuje jednak niespójność architektoniczną: podpis zewnętrznego artefaktu CT jest jawnie weryfikowany, podczas gdy podpisane dane dokumentów są jedynie odczytywane.
Test P0: w kontrolowanym środowisku sprawdzić na granicy przyjęcia danych i ponownie przy odczycie: (a) SignedData z uszkodzonym podpisem, (b) SignedData bez podpisujących, (c) podpis poprawny, ale wykonany nieufnym certyfikatem, (d) poprawny podpis niewłaściwego typu dokumentu/OID, (e) kopertę zbudowaną poza oczekiwanym producentem do publicznego certyfikatu testowego użytkownika i zawierającą jeden z nieufnych wariantów a–d oraz (f) bezpośrednie wstrzyknięcie wariantów a–e do b0(...) i pakietu aktywacyjnego. Należy osobno zmierzyć zachowanie obu stanów MOB_DB_CONTAINERS. Warianty a–f muszą zostać odrzucone przed zapisem i przed zbudowaniem modelu interfejsu użytkownika, a log ma wskazać konkretną przyczynę bez ujawnienia danych dokumentu. Koperta utworzona przez inny podmiot, ale przenosząca autentyczny, zaufany i świeży CMS, jest odrębnym przypadkiem ponownego podania danych i ustalania ich pochodzenia — sama koperta nie zawiera dowodu nadawcy.
Naprawa: jedna, centralna funkcja verifyThenDecode, wywoływana na granicy przyjęcia danych przed getEncoded() i zapisem; ponowna walidacja przy odczycie jako obrona warstwowa; brak publicznego API decode odczytującego niezweryfikowaną treść; testy negatywne dla braku podpisujących, złej sygnatury, obcego certyfikatu, złego OID i czasu; oddzielne typy UntrustedCmsBytes oraz VerifiedDocumentPayload. Dla nowych formatów należy zastąpić nieuwierzytelnione AES-CBC trybem AEAD lub poprawną konstrukcją „najpierw szyfrowanie, potem MAC” (encrypt-then-MAC). Nie należy jednak traktować tego jako zamiennika podpisu wystawcy dokumentu.
Pakiet aktywacyjny legitymacji studenckiej jest szyfrowany kluczem wyprowadzanym z kodu aktywacyjnego. Śledzenie danych rozdziela dwa elementy przekazywane użytkownikowi: State.getActivationCode() jest argumentem hasłowym operacji deszyfrowania packageData, natomiast setupData.getQrCode() trafia osobno do operacji oznaczenia pakietu jako pobranego: z63/n.java, z63/n.java. Zasoby interfejsu również mówią osobno o kodzie QR i kodzie aktywacyjnym oraz deklarują ich jednorazowe użycie: strings.xml. Nie ma więc podstaw, aby nazywać hasło „kodem QR”; z APK nie odtworzono natomiast alfabetu, długości ani procesu losowania kodu aktywacyjnego.
Parametr 0 widoczny przy tworzeniu PBEKeySpec jest fałszywym tropem dekompilacyjnym. Wywołanie korzysta z maski argumentu domyślnego Kotlina, a konstruktor syntetyczny podstawia PKIFailureInfo.notAuthorized, czyli 65 536: lz/d.java, PKIFailureInfo.java. Generator przekazuje tę wartość do javax.crypto.spec.PBEKeySpec; algorytm to PBKDF2-HMAC-SHA256, a długość klucza wynosi 256 bitów: e20/g.java. Zarzut „zero iteracji” byłby nieprawdziwy.
Potwierdzonym problemem jest sól. saltStudentID to wpisana w kod, ośmiobajtowa stała {-73, 116, 33, -84, 127, -116, -18, -103} obecna w obu implementacjach: d64/p.java, ey3/b.java. Funkcja przedstawiona przez dekompilator jako łączenie trzech wartości wykonuje cykliczny XOR, a nie konkatenację ani KDF: hh2/a.java. Wszystkie trzy argumenty są identyczne, więc wynik algebraicznie pozostaje tą samą ośmiobajtową stałą: salt XOR salt XOR salt = salt. Nowa ścieżka przekazuje je jawnie w ey3/b.java; starsza ścieżka robi to przez funkcję pomocniczą w bh2/a.java, wywołaną z tym samym stałym argumentem trzykrotnie w d64/l.java. Nie należy zatem przedstawiać tej funkcji pomocniczej jako dowodu użycia losowej soli dla legitymacji studenckiej.
Skutek kryptograficzny jest precyzyjny: sól nie zapewnia rozdzielenia użytkowników ani pakietów, a identyczny kod aktywacyjny i te same parametry PBKDF2 prowadzą do identycznego klucza AES. Napastnik posiadający wiele szyfrogramów może amortyzować przygotowanie słownika między celami. Jednorazowość kodu ogranicza jego ponowne użycie w legalnym protokole, lecz sama nie usuwa ataku słownikowego poza aplikacją po pozyskaniu pakietu. AES-CBC z dopełnieniem i przewidywalnie ustrukturyzowanym wynikiem może dostarczyć sposobu rozpoznania poprawnego odgadnięcia, ale jego praktyczność — podobnie jak kanał pozyskania packageData — wymaga potwierdzenia laboratoryjnego. Nie wykazano odzyskania kodu ani danych użytkownika.
Liczba 65 536 iteracji jest znacznie niższa od współczesnego punktu odniesienia OWASP wynoszącego 600 000 dla PBKDF2-HMAC-SHA256. Jest to porównanie z zaleceniem dotyczącym przechowywania haseł; nie stanowi automatycznego dowodu naruszenia normy tego protokołu ani samodzielnej podatności. Właściwy koszt należy dobrać do entropii sekretu, wydajności urządzeń i modelu zagrożeń. OWASP Password Storage Cheat Sheet.
Ocena: Niskie–Średnie, warunkowe. Stała globalna sól i parametry KDF są potwierdzone statycznie. Podniesienie oceny wymaga wykazania, że kod ma małą przestrzeń, zaszyfrowany pakiet jest dostępny napastnikowi i istnieje wiarygodny sposób sprawdzania zgadywanego kodu poza aplikacją. Brak uwierzytelnienia AES-CBC z MOB-A-05 zwiększa wagę całego przebiegu, ale pozostaje odrębną własnością kryptograficzną i nie powinien być liczony drugi raz jako skutek stałej soli.
Test P0: pozyskać w autoryzowanym środowisku co najmniej dwa pakiety testowe dla różnych kont i kontrolowanego tego samego kodu, potwierdzić powtarzalność soli oraz wyprowadzonego klucza, udokumentować alfabet, długość, rozkład, sposób generowania, TTL, jednorazowość i kanał przekazania kodu, a następnie zmierzyć koszt kontrolowanego słownika poza aplikacją dla rzeczywistego formatu. Osobno sprawdzić, czy dopełnienie, parser JSON/CMS lub komunikaty błędów tworzą niezawodny sposób potwierdzenia odgadnięcia. Wyniki należy zachować także wtedy, gdy hipoteza praktycznego ataku zostanie obalona.
Naprawa: generować kryptograficznie losową, unikalną sól co najmniej 16-bajtową dla każdego pakietu i przechowywać ją jawnie w uwierzytelnionym nagłówku; przejść na AEAD; wersjonować parametry KDF oraz format i jawnie odrzucać przejście do słabszego wariantu ze stałą solą. Koszt KDF należy dobrać pomiarem na wspieranych urządzeniach i zapisać jako politykę aktualizowalną bez zmiany kodu. Jeżeli kod aktywacyjny ma małą entropię, sama większa liczba iteracji nie zastąpi projektu protokołu odpornego na zgadywanie poza aplikacją.
MOB-A-06 określa koszt zaatakowania zewnętrznej warstwy pakietu. Analiza tekstu jawnego po jej odszyfrowaniu rozstrzyga, co stanowi chroniony zasób. StudentPackageData zawiera trzy pola typu String: pkcs12Pass, pkcs12Data oraz signedDataList: StudentPackageData.java. Implementacja aktywacji dekoduje Base64 obu pierwszych pól, zamienia hasło na char[] i wywołuje moduł zarządzający PKCS#12 z aliasem mlWork: ey3/b.java. Moduł ten ładuje magazyn Bouncy Castle PKCS#12, pobiera certyfikat X.509 i odpowiadający mu PrivateKey, po czym zwraca CertKeyPair: y10/b.java.
Hasło PKCS#12 i chroniony nim magazyn są współlokowane w tym samym zaszyfrowanym obiekcie. Hasło może wykrywać błędne odszyfrowanie i chronić sam plik PKCS#12 po jego przypadkowym oddzieleniu, ale nie stanowi niezależnej granicy poufności wobec podmiotu, który odszyfruje cały pakiet. Taki podmiot otrzymuje jednocześnie pkcs12Data i pkcs12Pass. Ochroną sieciową pozostają TLS i przypinanie certyfikatów; raport nie twierdzi, że szyfrogram jest dostępny pasywnemu napastnikowi internetowemu. Na poziomie formatu aplikacyjnego zewnętrzną granicą jest jednak PBKDF2 ze stałą solą oraz AES-CBC bez widocznego znacznika AEAD/MAC. Wewnętrzna integralność PKCS#12 nie obejmuje osobnego pola signedDataList, którego obiekty CMS są następnie jedynie odczytywane, jak opisano w MOB-A-05.
Po imporcie klucz nie jest zachowywany jako nieeksportowalny uchwyt AndroidKeyStore. saveUserCertificateUC otrzymuje typ UNIVERSITY, pustą starą frazę oraz CertKeyPair: ey3/b.java. Repozytorium serializuje getPrivateKey().getEncoded() bezpośrednio do pola privateKey encji certyfikatu: xx3/a.java, CertificateEntity.java. Jest to eksportowalna reprezentacja PKCS#8, a nie alias nieeksportowalnego klucza sprzętowego.
Baza nie jest przy tym jawnym SQLite. Jest otwierana przez SQLCipher z kluczem przekazanym do SupportOpenHelperFactory: o20/c.java. Produkcyjny dostawca pobiera klucz z masterKeyProvider: p20/d.java, u54/j2.java. Materiał klucza głównego jest utrzymywany w pamięci procesu, a szersza ścieżka zarządzania kluczami obsługuje klucze AndroidKeyStore: v14/a.java, co1/z.java. Oznacza to sensowną warstwę ochrony przechowywanych danych przed prostym skopiowaniem pliku bazy. Nie zmienia jednak podstawowej właściwości: po otwarciu bazy aplikacja odtwarza pełne bajty prywatnego klucza, a ich poufność zależy od klucza SQLCipher i bezpieczeństwa procesu, zamiast od sprzętowo egzekwowanej nieeksportowalności samego klucza.
Klucz UNIVERSITY nie jest martwym artefaktem. Warstwa tożsamości pobiera właśnie ten CertKeyPair dla legitymacji studenckiej: u54/h2.java. Wspólna infrastruktura używa prywatnych kluczy CertKeyPair do podpisywania JWS oraz CMS i do odszyfrowywania kluczy danych: j01/a.java, m0.java, nw0/h.java. Analiza statyczna pozwala więc nazwać go aktywnym prywatnym kluczem certyfikatu tożsamości aplikacyjnej. Nie dowodzi natomiast, że jest to klucz kwalifikowanego podpisu elektronicznego ani że samodzielnie umożliwia wykonanie czynności prawnej; takiego sformułowania raport celowo nie używa.
W ey3/b.java nie znaleziono Arrays.fill ani równoważnego czyszczenia. Odszyfrowany JSON, pkcs12Data, pkcs12Pass, char[], klucz AES, bufory PKCS#12 i obiekt PrivateKey powstają jako kolejne kopie oraz są utrzymywane w polach stanu korutyny. Sekrety zapisane jako niemutowalne String nie mogą zostać deterministycznie wyzerowane przed pracą mechanizmu odśmiecania pamięci (GC). Nie jest to samodzielny zdalny wektor, ale zwiększa skutki zrzutu pamięci, narzędzia diagnostycznego, podatności procesu albo analizy urządzenia po aktywacji.
Dwa automatycznie wygenerowane toString() są szczególnie niebezpiecznym wzorcem: StudentPackageData.toString() wypisuje całe pkcs12Pass, pkcs12Data i signedDataList, a CertificateEntity.toString() wypisuje tablice certyfikatu i prywatnego klucza bajt po bajcie: StudentPackageData.java, CertificateEntity.java. Nie znaleziono bezpośredniego miejsca wywołania, które zapisywałoby te obiekty w logach w analizowanej ścieżce, dlatego raport klasyfikuje to jako potencjalny wyciek diagnostyczny, a nie potwierdzoną eksfiltrację do Sentry.
Jeżeli napastnik uzyska szyfrogram packageData i rzeczywista polityka kodów umożliwi atak słownikowy poza aplikacją, poprawne odgadnięcie odsłoni nie tylko dane dokumentu, lecz również plik PKCS#12, jego hasło i eksportowalny klucz prywatny. Możliwe następstwa zależą od rozszerzeń certyfikatu, akceptowanych operacji, ważności, unieważnienia oraz kontroli serwera. Mogą obejmować odtwarzanie aplikacyjnych podpisów lub uwierzytelnień albo odszyfrowywanie danych przeznaczonych dla tego certyfikatu, ale nie zostały jeszcze wykazane na produkcyjnym zapleczu serwerowym.
Atak na kopię bazy ma inną granicę: sam plik SQLCipher nie wystarcza bez klucza bazy. Po zdobyciu klucza SQLCipher pełny PKCS#8 jest jednak odzyskiwalny, ponieważ aplikacja nie przechowuje wyłącznie aliasu sprzętowego. Uprawnienia root lub pełna kontrola procesu dają już szerokie możliwości i nie powinny sztucznie podnosić CVSS; właściwym pytaniem jest odporność na kradzież kopii, lukę w procesie, zrzut po odblokowaniu oraz zakres skutków po utracie jednego urządzenia.
Ocena: Średnie, warunkowo Wysokie po P0. Potwierdzone są współlokacja klucza i hasła, eksportowalna postać PKCS#8, brak deterministycznego czyszczenia oraz brak izolacji klucza w AndroidKeyStore. Ocena Wysoka wymaga jednoczesnego wykazania praktycznego odzyskania kodu poza aplikacją z dostępnego szyfrogramu i skutecznego użycia odzyskanego klucza wobec nadal akceptującego go systemu. Bez tych dowodów określenie „przejęcie podpisu” byłoby nadinterpretacją.
Test P0: w wydzielonym środowisku i na własnych danych testowych: (a) zachować wyłącznie szyfrogram pakietu i przeprowadzić pomiar rzeczywistej przestrzeni kodów; (b) po kontrolowanym odzyskaniu kodu wyodrębnić PKCS#12, zweryfikować hasło i zgodność klucza z certyfikatem; (c) ustalić KeyUsage, ExtendedKeyUsage, wystawcę, okres ważności i wszystkie protokoły akceptujące certyfikat; (d) wykonać nieszkodliwy test podpisu i odszyfrowania w środowisku testowym; (e) unieważnić certyfikat i potwierdzić odrzucenie starego klucza w każdym punkcie końcowym; (f) po aktywacji wykonać kontrolowany zrzut sterty oraz inspekcję logów/Sentry pod kątem obu DTO i buforów; (g) skopiować bazę przed i po odblokowaniu, wykazać granicę SQLCipher oraz potwierdzić odzyskiwalność PKCS#8 dopiero po zdobyciu klucza bazy. Wyniki muszą rozdzielać przejęcie zawartości pakietu, procesu i bazy.
Naprawa: preferowany projekt generuje parę kluczy na urządzeniu wewnątrz AndroidKeyStore, wysyła wyłącznie CSR lub klucz publiczny i przechowuje alias zamiast PKCS#8. Polityka powinna wymagać TEE albo StrongBox tam, gdzie model zagrożeń tego wymaga, z jawnym zachowaniem na urządzeniu niespełniającym wymagań. Jeżeli import klucza wystawionego zewnętrznie jest nieunikniony, należy natychmiast importować go do AndroidKeyStore z restrykcyjnym KeyProtection, zweryfikować rzeczywisty poziom zabezpieczenia i usuwać wszystkie kopie importowe; sam import nie gwarantuje sprzętowej ochrony na każdym urządzeniu. Pakiet trzeba chronić AEAD, losową solą dla każdego pakietu, wysoką entropią sekretu oraz związaniem z użytkownikiem, urządzeniem, sesją i wersją protokołu. Hasło PKCS#12 przesłane obok magazynu nie może być przedstawiane jako dodatkowa bariera. Typy zawierające sekrety nie mogą generować toString(), equals() ani hashCode() operujących na pełnym materiale; bufory należy ograniczyć do byte[]/char[], czyścić w finally, a rotację i natychmiastowe unieważnienie klucza traktować jako element procedury incydentowej.
Skan nie obejmował pełnej korelacji CVE dla bibliotek .so, usług Google dostarczanych poza APK, kodu zaplecza serwerowego ani komponentów własnych bez współrzędnych Maven. Bazy podatności są opóźnione i niekompletne. Wynik „zero trafień” nigdy nie byłby dowodem braku błędów; wynik „trafienie” nie jest jeszcze dowodem osiągalności.
APK zawiera rozbudowany model raportu SDK BotSense, który teoretycznie przewiduje dane urządzenia, sieci, lokalizacji, integralności i klienta. Analiza samego modelu prowadziłaby do fałszywego wniosku o szerokim zbieraniu danych. Konfiguracja aplikacyjna jest znacznie węższa:
testUserID i nie wskazuje w tej ścieżce zdalnego URL konfiguracji;THREAT_DETECTED.Dowody: h83/c.java, h83/c.java, h83/c.java.
Odrębnie od heurystyk RootBeer/BotSense w APK znajduje się rzeczywista ścieżka Play Integrity. Konfiguracja niesie projectNumber, klient generuje 16 losowych bajtów przez SecureRandom, koduje je jako wartość jednorazową (nonce) i wywołuje requestIntegrityToken; wynik oraz ta wartość trafiają do raportu: c74/i.java, w64/l4.java, w64/g4.java. Generator kluczy AndroidKeyStore obsługuje także setAttestationChallenge, ale wyłącznie dla ścieżki EC i tylko wtedy, gdy wyzwanie jest niepuste: e20/m.java. Są to poprawne mechanizmy obronne i należy je odnotować. Analiza statyczna klienta nie pokazuje jednak, jak serwer weryfikuje token, wartość jednorazową, wiek werdyktu, licencję i rozpoznanie urządzenia ani jakie decyzje od nich zależą. Obecność API nie może więc zostać przekształcona w ogólne zapewnienie integralności urządzenia.
RootBeer i BotSense zwiększają koszt prostych modyfikacji, ale nie powinny stanowić jedynej przesłanki autoryzacji. Lokalny klient jest kontrolowany przez właściciela urządzenia; decyzje o wydaniu dokumentu, podpisie lub zmianie stanu muszą opierać się na kryptograficznie związanej sesji, świeżości, poświadczeniu integralności urządzenia o jasno opisanej polityce i walidacji serwera. Wykrycie uprawnień root nie naprawia błędu autoryzacji, a ich niewykrycie nie może oznaczać zaufania absolutnego.
Wzorem analiz SKCI i EFITool zachowano nie tylko wynik końcowy, ale również ścieżki, które wyglądały obiecująco i zostały odrzucone. Pozwala to odtworzyć rozumowanie i zapobiega ponownemu przedstawianiu obalonych tez jako odkryć.
| Hipoteza początkowa | Sprawdzenie | Wynik | Znaczenie |
|---|---|---|---|
HTTP do gstatic daje dowolne wstrzyknięcie zasobu |
wyszukanie odbiorców, format ZIP, test podpisu | fałsz w tej postaci | treść jest podpisana; pozostała możliwość podstawienia starszej listy |
| Dekompilacja pokazuje akceptację złego podpisu CT | drugi tryb JADX + eksperyment: klucz prawidłowy, nieprawidłowy i brak klucza | artefakt dekompilatora | błędny podpis jest odrzucany |
sslEnabled jest zdalnym przełącznikiem |
manifest, eksporty, produkcyjne wstrzykiwanie zależności, SharedPreferences | brak ścieżki zwykłego napastnika | kod akceptujący połączenie mimo błędu nadal powinien zniknąć z wydania produkcyjnego |
Uprawnienia root pozwalają zmienić prywatną flagę i „przełamać TLS” |
porównanie uprawnień atakującego | mylący model zagrożeń | uprawnienia root lub Frida oznaczają już kontrolę nad procesem |
ksRandomizer losuje alias klucza |
śledzenie argumentu do Android Keystore | fałsz | to setRandomizedEncryptionRequired() |
| StrongBox jest bezwzględnie wymagany | ścieżka PREFERRED i obsługa ProviderException |
fałsz dla polityki ogólnej | istnieje świadome przejście na wariant zastępczy i pomiar poziomu |
| Tink/SQLCipher dowodzi ochrony konkretnej bazy | referencje i miejsca użycia | nieudowodnione | komponent nie jest dowodem przepływu danych |
| Odtwarzanie sesji (Session Replay) jest aktywne, bo SDK jest w APK | wyszukanie konfiguracji aplikacyjnej | nieudowodnione | nie przypisano funkcji bez konfiguracji |
| BotSense zbiera cały szeroki model raportu | analiza konfiguratora klienta | fałsz dla badanej ścieżki | większość opcji wyłączona; aktywne są węższe kontrole |
| CVSS biblioteki jest oceną mObywatela | SCA, komunikaty projektu źródłowego i wyszukanie miejsc wywołań | fałsz metodologiczny | kod wadliwych mechanizmów istnieje, lecz ich osiągalność wymaga osobnego dowodu |
| PBKDF2 wykonuje zero iteracji | maska argumentu domyślnego i konstruktor syntetyczny lz/d |
artefakt dekompilacji | rzeczywista wartość to 65 536; problemem jest stała sól i nieznana entropia kodu |
Trzy składniki saltStudentID są konkatenowane lub zawierają losowość urządzenia |
implementacja hh2/a i oba miejsca wywołania |
fałsz | funkcja wykonuje XOR, a trzy identyczne stałe redukują się do tej samej stałej |
| PKCS#12 z legitymacji zawiera klucz podpisu kwalifikowanego | śledzenie typu UNIVERSITY, repozytorium certyfikatów i konsumentów CertKeyPair |
nieudowodnione | potwierdzono aktywny klucz prywatny tożsamości aplikacyjnej; nie potwierdzono kwalifikowanego zastosowania prawnego |
| SQLCipher oznacza, że klucz prywatny jest nieeksportowalny | format CertificateEntity i getPrivateKey().getEncoded() |
fałsz | baza jest szyfrowana, ale po jej otwarciu aplikacja odtwarza pełny PKCS#8 zamiast używać wyłącznie aliasu magazynu kluczy |
FLAG_SECURE rozwiązuje atak przez zasłanianie interfejsu |
porównanie semantyki flagi ze ścieżką obsługi zasłoniętych dotknięć | fałsz | flaga chroni obraz okna; nie zastępuje filterTouchesWhenObscured, obsługi częściowego zasłonięcia ani ukrycia nakładek |
| Sprawdzenie istnienia pliku przed zapisem tworzy możliwość nadpisania przez TOCTOU | śledzenie generatora nazw i FileOutputStream |
niepotwierdzone jako istotny wektor | potencjalny wyścig dotyczy pamięci współdzielonej i nie daje zapisu poza uprawnieniami; odrębnym błędem jest brak kanonikalizacji w MOB-A-10 |
Publiczny kod zawiera przykład isPies/isDog |
pełne rg w 192 plikach i dekompilacji |
nieodtworzone | anegdoty bez pliku i identyfikatora zmiany nie użyto jako ustalenia |
Ta tabela nie „broni” produktu. Oddziela to, co można zaatakować dowodem, od tego, co jedynie źle wygląda. Pozwala dzięki temu ostrzej sformułować potwierdzone zarzuty bez rozcieńczania ich błędami.
Poniższa macierz określa, co rzeczywiście sprawdzono w artefakcie. Wynik „brak potwierdzonego wektora” nie znaczy „niemożliwe”; oznacza, że przeszukanie miejsc wywołań nie wykazało przepływu z niezaufanego wejścia do niebezpiecznej operacji w dostępnej analizie statycznej.
| Klasa zagrożenia | Zakres kontroli | Wynik w APK 4.87.2 | Klasyfikacja / dalszy krok |
|---|---|---|---|
| Integralność CMS / pochodzenie dokumentu | konstrukcja, przyjęcie, zapis i odczyt CMSSignedData; miejsca wywołań API weryfikacji |
brak znalezionej weryfikacji podpisujących w cyklu dokumentu | MOB-A-05, P0 |
| Kryptografia hasłowa i materiał klucza | parametry PBKDF2, sól, AES, PKCS#12, zapis certyfikatu, higiena pamięci | stała sól; klucz i hasło współlokowane; eksportowalny PKCS#8 | MOB-A-06/A-08, P0 |
| OAuth / odnośniki do aplikacji / App Links | eksporty manifestu, schematy, walidacja wywołania zwrotnego, state, PKCE, warianty flag |
starszy wariant bez state/PKCE; własne schematy URI nie mają własności App Links |
MOB-A-03/A-09, P0 |
| TLS, przypinanie certyfikatów, CT i WebView | profile klientów, wartości produkcyjne, flagi, obsługa błędów, JS, dostęp do plików i treść mieszana | dobre wartości domyślne, ale kod akceptujący połączenie mimo błędu pozostaje; CT bez kontroli świeżości | MOB-A-01/A-02 |
| Wyjście poza katalog / eksport plików | Content-Disposition, nazwy DTO, MediaStore, bezpośrednie ścieżki, FileProvider i uprawnienia |
warunkowe wyjście poza katalog na API 26–28; dostawca prywatny, lecz zbyt szeroki | MOB-A-10 / MOB-O-02 |
| Niebezpieczna deserializacja Java | ObjectInputStream, readObject, readSerializable, źródła danych |
WorkManager czyta typy prymitywne; Serializable ograniczone do wewnętrznych typów Parcel; brak niezaufanego readObject |
brak potwierdzonego wektora; testowanie mutacyjne parserów nadal wymagane |
| Deserializacja polimorficzna JSON | adaptery Gson, metadane typów i miejsca wywołań | nie znaleziono aplikacyjnego polimorficznego tworzenia dowolnych klas z niezaufanego JSON | brak potwierdzonego wektora |
| Wstrzyknięcie SQL | zapytania raw query/execSQL, konkatenacje i wiązanie argumentów |
Room/generowany SQL i parametry wiązane; trafienia surowe biblioteczne/literalne | brak potwierdzonego wektora; serwer poza zakresem |
| Dynamiczne ładowanie kodu / wstrzyknięcie polecenia powłoki | DexClassLoader/PathClassLoader, refleksja, Runtime.exec/ProcessBuilder |
mechanizm ładowania modułów GMS i stałe polecenia RootBeer; brak danych aplikacyjnych sterujących kodem lub komendą | brak potwierdzonego wektora |
Przejęcie PendingIntent |
wszystkie bezpośrednie PendingIntent.get*, flagi i cele |
własne akcje niemutowalne; jedna mutowalna funkcja pomocnicza GMS do nieuprzywilejowanego fikcyjnego pakietu | dług biblioteczny, bez potwierdzonej podatności |
| Atak przez zasłanianie interfejsu | flagi okna, obsługa zasłoniętego dotyku, ukrywanie nakładek, manifest | FLAG_SECURE obecny, brak aplikacyjnej ochrony zasłoniętego dotyku |
MOB-O-04, test dynamiczny |
| Losowość bezpieczeństwa | Random, SecureRandom, miejsca użycia IV, soli i tokenów |
trafienia Random dotyczą interfejsu, mediów i bibliotek; ścieżki bezpieczeństwa używają SecureRandom, poza stałą solą MOB-A-06 |
brak dodatkowego ustalenia |
| Logowanie sekretów / schowek | zdalny mechanizm logowania, toString() DTO sekretów, miejsca użycia schowka |
brak potwierdzonego logowania wartości sekretów; ryzykowne toString() i pełne URL; schowek używany jawnie przez użytkownika |
MOB-O-01/O-03, test telemetrii |
| Kod natywny | inwentaryzacja ABI, ELF NX/RELRO/BIND_NOW/ochrona stosu, eksporty wysokiego poziomu | podstawowe utwardzenie ARM64 dobre; brak pełnej inżynierii wstecznej i testowania mutacyjnego | uzupełnić dla 4 ABI i JNI |
Uprawnienia root, instrumentacja i poświadczenie integralności |
RootBeer/BotSense, Play Integrity, poświadczenie klucza, punkty decyzji i model zaufania | token z losową wartością jednorazową i wyzwaniem klucza są obecne; polityki walidacji serwera nie widać; klient po przejęciu lub instrumentacji pozostaje środowiskiem kontrolowanym | granica modelu; serwer nie może ufać wyłącznie wynikowi klienta |
Pakiety dzielone dla arm64-v8a, armeabi-v7a, x86 i x86_64 zawierają po 11 bibliotek natywnych:
| Grupa | Biblioteki |
|---|---|
| TLS / kryptografia / dane | libconscrypt_jni.so, libsqlcipher.so |
| Telemetria | libsentry.so, libsentry-android.so |
| OCR / obraz / kody | libmlkit_google_ocr_pipeline.so, libbarhopper_v3.so, libimage_processing_util_jni.so, libsurface_util_jni.so |
| AndroidX / DataStore | libandroidx.graphics.path.so, libdatastore_shared_counter.so |
| Detekcja środowiska | libtoolChecker.so |
Kontrola nagłówków plików i nagłówków programu wszystkich 11 bibliotek w pakiecie ARM64 przyniosła wynik pozytywny:
| Kontrola ELF | Wynik ARM64 | Interpretacja |
|---|---|---|
typ ELF DYN |
11/11 | prawidłowy format współdzielonych obiektów pozycyjnie niezależnych; nie należy utożsamiać tego mechanicznie z testem PIE wykonywalnego pliku |
stos bez prawa wykonania (GNU_STACK = RW, bez E) |
11/11 | NX dla stosu skonfigurowane poprawnie |
GNU_RELRO oraz BIND_NOW |
11/11 | pełny RELRO we wszystkich badanych bibliotekach |
odwołanie do __stack_chk_fail |
10/11 | ochrona stosu widoczna w bibliotekach zawierających kwalifikujące funkcje |
Jedynym plikiem bez odwołania do __stack_chk_fail jest liczący około 10 KiB libdatastore_shared_counter.so. Eksportuje krótkie operacje JNI na liczniku, mmap i ftruncate; brak symbolu nie dowodzi globalnego wyłączenia ochrony, ponieważ kompilator wstawia ochronę stosu tylko do funkcji spełniających jego kryteria. Dlatego nie utworzono z tego pozornej podatności. Uczciwy wniosek brzmi: konfiguracja podstawowego utwardzenia ELF dla ARM64 jest dobra, ale sam zestaw flag nie dowodzi bezpieczeństwa logiki ani parserów.
Przegląd wszystkich bezpośrednich konstrukcji PendingIntent.get* również nie wykazał mutowalnej akcji należącej do aplikacji. Własne powiadomienia używają wartości 201326592, czyli FLAG_IMMUTABLE | FLAG_UPDATE_CURRENT: FirebaseService.java, x54/c.java. Funkcja pomocnicza Google Play Services wybiera flagę niemutowalności dla przebiegu interfejsu: s5/r.java. Wyjątkiem jest odziedziczona funkcja pomocnicza rejestracji Google, która od Androida 12 tworzy mutowalny PendingIntent do pakietu com.google.example.invalidpackage: fg/c.java, wg/a.java. Jest on przekazywany jako identyfikator pochodzenia, lecz nie wskazuje uprzywilejowanej akcji aplikacji i w badanym śladzie nie daje odbiorcy kontroli nad funkcją mObywatela. Pozostaje długiem bibliotecznym: należy potwierdzić aktualność zależności i usunąć mechanizm, jeżeli współczesny protokół rejestracji go nie wymaga.
Obecny etap nie objął kompletnej inżynierii wstecznej, symbolicznego śledzenia wejść ani testowania mutacyjnego JNI. Szczególnie istotne są parsery obrazu i kodów, SQLCipher, Conscrypt oraz granice Java↔JNI, ponieważ błędy pamięci w kodzie natywnym mogą zmienić obserwację biblioteczną w wykonanie kodu lub eskalację wewnątrz procesu aplikacji.
Niezbędne uzupełnienie: skróty kryptograficzne każdej biblioteki dla każdej ABI, identyfikacja wersji i build-id, powtórzenie kontroli utwardzenia dla pozostałych ABI, kontrola CFI/PAC/BTI, mapowanie eksportów JNI, testowanie mutacyjne danych wejściowych i porównanie z komunikatami projektu źródłowego. Brak pełnej analizy kodu natywnego jest jawną luką zakresu, a nie przemilczanym zapewnieniem bezpieczeństwa.
Sprawdzona kopia publicznego repozytorium wskazywała zmianę d39d2f947b1078332d758de086cb3cb7dd163940 z 4 stycznia 2026 r. i zawierała 192 pliki Kotlin. Zakres odpowiada systemowi wzornictwa i interfejsowi użytkownika, a nie pełnej aplikacji. Pliki COI są objęte dołączoną licencją MIT; licencję kopii repozytorium i prawa do innych składników należy rozpatrywać oddzielnie. Licencja na kod nie udziela automatycznie prawa do znaków, nazwy, urzędowych symboli ani sugerowania autoryzacji państwowej.
Oficjalny komunikat z 29 grudnia 2025 r. mówi o publikacji „kodu źródłowego aplikacji”. Technicznie dostępny zakres nie umożliwia zbudowania aplikacji ani audytu warstw bezpieczeństwa. To uzasadnia precyzyjną krytykę braku przejrzystości, ale nie uprawnia do twierdzenia, że każda brakująca część jest wadliwa. Komunikat Ministerstwa Cyfryzacji.
Poniższa część ma charakter techniczny. Ocenę prawną planowanego sposobu wykorzystania należy powierzyć prawnikowi właściwej specjalności.
Polska ustawa o prawie autorskim stanowi, że ochrona programu obejmuje formy wyrażenia, ale nie idee, zasady ani zasady będące podstawą interfejsów. Osoba uprawniona do korzystania z egzemplarza może obserwować, badać i testować jego działanie. Dekompilacja ma węższą podstawę: musi być konieczna do interoperacyjności niezależnie stworzonego programu i spełniać warunki art. 75 ust. 2 pkt 3. Uzyskanej informacji nie wolno używać do programu o istotnie podobnej formie wyrażenia ani innych naruszeń. Co istotne, ogólnego dozwolonego użytku osobistego z art. 23 nie stosuje się do programów komputerowych. Ustawa o prawie autorskim, art. 74–77.
Z tego wynika bezpieczniejszy profil prywatnego demonstratora:
DEMO — nie jest dokumentem ani aplikacją państwową;Niezależna aplikacja nie staje się przez podobną funkcjonalność aplikacją mObywatel ani dokumentem elektronicznym wywołującym skutki ustawowe. Przed dystrybucją, użyciem realnych protokołów lub danych i integracją z systemami państwowymi potrzebna jest pisemna autoryzacja oraz przegląd prawnika od prawa autorskiego, znaków i systemów teleinformatycznych.
WinAPI i niewielka liczba zależności mogą zmniejszyć ryzyko łańcucha dostaw, rozmiar SBOM oraz liczbę parserów i środowisk wykonawczych. „Zero zależności” nie jest jednak celem bezpieczeństwa samym w sobie: Windows, biblioteki systemowe, sterowniki i kompilator nadal są zależnościami, a własny parser, kryptografia lub TLS prawdopodobnie zwiększą ryzyko.
Zalecana konfiguracja bazowa: /std:c++23, ostrzeżenia jako błędy, /permissive-, /sdl, /guard:cf, ASLR/DEP, CET tam, gdzie są dostępne, Control Flow Guard, podpisywanie binarki, analiza MSVC/clang-tidy, testowanie mutacyjne parserów, testy Application Verifier i brak sekretów w logach oraz małych zrzutach pamięci. WebView2 należy dodać tylko wtedy, gdy wymaganie rzeczywiście wymusza HTML; wnosi oddzielne, szybko aktualizowane środowisko wykonawcze.
Ustalenia tego raportu nie mają charakteru wyłącznie krytycznego; wyznaczają zarazem wzorzec, według którego zamierzam zbudować niezależną aplikację referencyjną o zbliżonej funkcjonalności. Przepływ danych i model działania znam na tyle dokładnie, by odtworzyć go samodzielnie w języku C++23, w oparciu o natywne WinAPI, bez pośrednictwa rozbudowanego stosu kilkuset bibliotek. Rezygnacja z tej powłoki nie jest celem samym w sobie, lecz świadomą decyzją inżynierską: każda zbędna zależność to dodatkowy parser, kolejne środowisko uruchomieniowe i wydłużenie łańcucha dostaw, a zatem realny przyrost powierzchni potencjalnego ataku. Rdzeń mniejszy i przejrzysty łatwiej poddaje się higienie kodu, przeglądowi oraz utrzymaniu. Wybór natywnego WinAPI nie zamyka przy tym drogi na inne systemy: ta sama zasada — niewielki rdzeń, minimum zależności i kryptografia powierzona zaudytowanym mechanizmom platformy — przenosi się na Linuksa z nowoczesnym, natywnym zestawem narzędzi (GTK4/libadwaita), czego dowodzi mój edytor rejestru RegEdLin zbudowany dokładnie w tym duchu.
Nie piszę tego z pozycji teoretyka. Pracuję na co dzień poniżej warstw, które niniejszy raport traktuje jako gwarancje: wymuszania podpisu sterowników, HVCI, zabezpieczeń opartych na wirtualizacji i Bezpiecznego Jądra, aż po fazę UEFI — mechanizmów, w które Microsoft inwestuje ogromne środki, a które w moich narzędziach badawczych (zob. github.com/wesmar) potrafię obejść lub odtworzyć na każdym poziomie: od modyfikacji g_CiOptions i przekierowania SeCiCallbacks, przez uzyskanie uprawnień SYSTEM jeszcze przed ekranem logowania — bez zapisu na dysk i bez sterownika jądra — po własny sterownik systemu plików NTFS działający w środowisku UEFI. Kto rozumie te warstwy od tej strony, inaczej rozkłada zaufanie w aplikacji tożsamości: nie rozważa, czy wykrywanie roota jest dostatecznie pomysłowe, lecz przyjmuje, że urządzenie bywa w pełni kontrolowane, i przenosi rozstrzygnięcia tam, dokąd klient nie sięga. Po trzydziestu latach tej praktyki architektura takiej aplikacji powstałaby zupełnie inaczej — nie dlatego, że dysponuję większą liczbą bibliotek, lecz dlatego, że mam ich znacznie mniej i wiem, której warstwy trzeba naprawdę pilnować.
Prace prowadzę w reżimie clean-room i na licencji MIT: własny kod, własna architektura, własny interfejs, własna nazwa i grafika — bez przejmowania znaków, symboli urzędowych czy sugerowania autoryzacji państwowej. Bezpieczeństwo opieram nie na zaufaniu do klienta, lecz na dyscyplinie protokołu: pojedynczej granicy „zweryfikuj, zanim odczytasz" dla treści podpisanej, uwierzytelnionym szyfrowaniu w miejsce trybów pozbawionych kontroli integralności, kluczach nieeksportowalnych osadzonych w sprzęcie zamiast materiału przenoszonego do klienta oraz serwerze rozstrzygającym o każdej istotnej decyzji. Świadomie nie piszę przy tym własnej kryptografii ani własnego TLS — sięgam po zweryfikowane mechanizmy systemowe, ponieważ to one, a nie sama liczba zależności, przesądzają o rzeczywistym poziomie zapewnienia.
Nie jest to zapowiedź „aplikacji doskonałej", lecz zobowiązanie do warsztatu, w którym opisane w tym raporcie słabości granicy zaufania nie mają prawa się pojawić.
| ID | Właściciel | Działanie | Dowód zamknięcia | Termin zalecany |
|---|---|---|---|---|
| MOB-A-01 | sieć/PKI | HTTPS, monotoniczna wersja i znacznik czasu, stan oraz przedział czasowy dziennika CT | test starszej podpisanej listy kończy się odrzuceniem | przed następnym wydaniem |
| MOB-A-02 | platforma Android | usunięcie TrustAll, permisywnego weryfikatora i proceed() z wydania produkcyjnego |
przeszukanie końcowego APK + test błędu certyfikatu | przed następnym wydaniem |
| MOB-A-03 | aplikacja mobilna / serwer | migracja tokenów do App Links HTTPS, jednorazowość, TTL, stan i odbiorca | test aplikacji kolidującej i spreparowanego Intentu | 30 dni |
| MOB-A-04 | bezpieczeństwo aplikacji / budowanie | aktualizacja BC, pełny SBOM i osiągalność kodu dla wszystkich CVE 1.83→bieżące | wynik SCA bez nierozstrzygniętego ryzyka Wysokiego/Krytycznego + testy podpisów/PKI | natychmiast |
| MOB-A-05 | kryptografia / dokumenty | centralne verifyThenDecode przy przyjęciu i odczycie; typowanie danych niezaufanych i zweryfikowanych |
sześć wariantów negatywnych odrzuconych przed zapisem i interfejsem użytkownika w obu stanach flagi | natychmiast / P0 |
| MOB-A-06 | kryptografia / aktywacja | losowa sól dla każdego pakietu, AEAD, wersjonowanie parametrów KDF i ochrona przed przejściem do słabszego wariantu | dwa pakiety nie współdzielą soli ani klucza; pomiar i test rozpoznawania poprawnej próby poza aplikacją; test migracji starego formatu | natychmiast / P0 |
| MOB-A-08 | kryptografia / tożsamość / serwer | generowanie lub bezpieczny import klucza do AndroidKeyStore, nieeksportowalność, usunięcie sekretów z DTO, rotacja i unieważnienie | pełny test cyklu klucza; w bazie wyłącznie alias; brak PKCS#8 i hasła w stercie oraz logach; stary certyfikat odrzucony | natychmiast / P0 |
| MOB-A-09 | aplikacja mobilna / OAuth / serwer | wycofanie y12.t albo przeniesienie state + PKCE S256, ścisła walidacja zwrotnego URI i ochrona przed obniżeniem zabezpieczeń |
test dwóch sesji i wstrzyknięcia obcego kodu; każde niedopasowanie odrzucone przed wymianą tokenu | natychmiast / P0 |
| MOB-A-10 | aplikacja mobilna / serwer / pliki | oczyszczanie nazwy po obu stronach, kanoniczne związanie celu z katalogiem, usunięcie starszej ścieżki bezpośredniego zapisu | testy nazw z ../, separatorami i filename*= na API 26/28/29; plik nigdy nie powstaje poza Downloads |
następne wydanie / P1 |
| MOB-O-01/03 | prywatność / telemetria | centralne usuwanie wrażliwych części URL i danych, lista dozwolonych znaczników, retencja | zapis zdarzeń z testu nie zawiera parametrów zapytania, tokenów ani danych osobowych | przed zamknięciem audytu |
| MOB-O-02 | platforma Android | zawężenie FileProvider i audyt grantów |
test URI spoza katalogu kończy się odmową | następny cykl utwardzenia |
| MOB-O-04 | aplikacja mobilna / bezpieczeństwo interfejsu | ochrona okna i kontrola zasłoniętych dotknięć na ekranach wrażliwych | macierz nakładek nie wykonuje operacji i zachowuje wymaganą dostępność | następny cykl utwardzenia |
| NATYWNE | właściciele JNI | wersje, utwardzenie, CVE i testowanie mutacyjne 11 bibliotek dla 4 ABI | pakiet skrótów, build-id, wyniki utwardzenia i testów mutacyjnych |
przed zapewnieniem o bezpieczeństwie |
| ŹRÓDŁA | właściciel produktu | pełne źródła, plik blokady zależności, instrukcja odtworzenia, mapowanie zmiany źródłowej na APK | odtworzona kompilacja i porównanie artefaktów | po formalnym przekazaniu kodu |
Ustalenie nie jest zamknięte komunikatem prasowym, zmianą w repozytorium ani deklaracją zespołu. Zamknięcie dotyczy konkretnego podpisanego artefaktu i wymaga dowodu testowego możliwego do niezależnego powtórzenia.
Najistotniejszym potwierdzonym problemem wersji 4.87.2 nie jest dowolne wstrzyknięcie treści przez gstatic.com, lecz możliwość cofnięcia podpisanej listy zaufanych dzienników CT, ponieważ podpis chroni integralność i pochodzenie, ale implementacja nie chroni świeżości. Jest to precyzyjny, naprawialny błąd obrony warstwowej o warunkowym wpływie.
Niebezpieczne implementacje TrustAll, permisywnej weryfikacji nazwy hosta oraz SslErrorHandler.proceed() są obecne w artefakcie wydania i powinny z niego zniknąć. Dostępny materiał nie potwierdza jednak, aby zwykły napastnik mógł przełączyć produkcyjną aplikację na tę ścieżkę. Klasyfikowanie tego jako zdalnej podatności wysokiej wagi byłoby nierzetelne.
Przegląd WebView potwierdził jednocześnie, że nie wszystkie warianty są jednakowe. Płatności mają SSL wymuszone na stałe, bazowe ustawienia treści mieszanej i dostępu do plików są restrykcyjne, a nie znaleziono addJavascriptInterface; to należy pochwalić. Z drugiej strony usługi internetowe włączają allowFileAccess, a wspólny klient nadal zawiera proceed() oraz kod budujący pełny DOM i URL dla mechanizmu logowania. Badany produkcyjny odbiornik odrzuca poziomy debug/info, więc nie ogłoszono wycieku treści WebView. Jest to jednak konfiguracja krucha: bezpieczeństwo zależy od właściwego wariantu i konkretnej implementacji mechanizmu logowania, zamiast od niezmiennych ograniczeń komponentu.
Odrębny problem dotyczy starszej autoryzacji e-Doręczeń. y12.t rozpoczyna przebieg kodu autoryzacyjnego bez state i PKCE, akceptuje wywołanie zwrotne na podstawie schematu oraz początku ścieżki, a następnie wysyła code_verifier=null. Nowszy moduł obecny w APK implementuje state i PKCE S256, zatem luka wynika z niespójności między wariantami, a nie z braku możliwości technicznej. Ponieważ dostępność y12.t zależy od serwerowej flagi NATIVE_LOGIN_EDOR_MODULE_ANDROID, raport ocenia MOB-A-09 jako średnie i warunkowe oraz wymaga testu P0 zamiast bezpodstawnie deklarować aktywne przejęcie sesji.
W tej samej domenie funkcjonalnej znaleziono warunkowy błąd zapisu załączników na wspieranych nadal Androidach 8–9. Nazwa z Content-Disposition dociera bez normalizacji do ścieżki opartej na publicznym Downloads, a ../ może skierować nowy plik do innego katalogu pamięci współdzielonej. Kod nie nadpisuje istniejącego celu i gałąź Androida 10+ używa MediaStore, dlatego nie jest to uniwersalny dowolny zapis ani zdalne wykonanie kodu (RCE). Jest to jednak klasyczny brak egzekwowania granicy katalogu po stronie klienta, którego nie wolno usprawiedliwiać oczekiwanym oczyszczaniem danych na serwerze. MOB-A-10 wymaga testu P1 z nazwą kontrolowaną przez nadawcę.
Drugi obszar wymagający pilnego działania to łańcuch dostaw. Bouncy Castle 1.83 zawiera kod objęty znanymi CVE, a projekt źródłowy wydał już 1.84 i następnie linię 1.85.x z szerokim zestawem korekt. Nie znaleziono odwołań aplikacyjnych do GOST, LDAP, Composite Signature ani FrodoKEM, dlatego raport nie twierdzi, że tymi mechanizmami osiągnięto eskalację. Brak dowodu wykorzystania nie jest jednak argumentem za pozostawieniem podatnej wersji aktywnie używanego dostawcy kryptograficznego.
Jeszcze poważniejszego rozstrzygnięcia wymaga sposób wykorzystywania CMS. Analiza statyczna potwierdziła tworzenie i zapis CMSSignedData na wejściu oraz liczne odczyty getSignedContent() bez znalezionej lokalnej weryfikacji podpisujących; uzyskane dane zasilają modele dokumentów. Osobny pakiet aktywacyjny legitymacji przychodzi z FrontSrv, jest chroniony hasłowym AES-CBC bez widocznego znacznika uwierzytelniającego i w obu implementacjach prowadzi do konstrukcji CMSSignedData bez znalezionego verify. Nie wykazano jeszcze zwykłego zdalnego kanału dostarczenia podrobionego kontenera, dlatego nie ogłoszono przełamania integralności dokumentów. Jednak w produkcie tożsamościowym przyjmowanie obiektu nazwanego i zaprojektowanego jako SignedData bez jawnego verify wymaga natychmiastowego testu P0 i korekty, a nie kosmetycznego zalecenia.
W tym samym pakiecie aktywacyjnym potwierdzono niezależny błąd KDF: globalną, ośmiobajtową sól PBKDF2. Widoczne w dekompilacji zero iteracji jest artefaktem; rzeczywista wartość to 65 536, dlatego raport nie powiela efektownego, ale fałszywego zarzutu. Stała sól usuwa rozdzielenie kluczy między pakietami i umożliwia amortyzowanie słownika, lecz bez znajomości entropii kodu oraz kanału dostępu do szyfrogramu nie dowodzi jeszcze praktycznego odzyskania danych.
Analiza tekstu jawnego domknęła wpływ tego wątku: pakiet zawiera plik PKCS#12 z aktywnym prywatnym kluczem certyfikatu UNIVERSITY oraz hasło do tego pliku. Po aktywacji klucz jest zapisywany jako eksportowalny PKCS#8 w bazie SQLCipher, a nie jako nieeksportowalny uchwyt AndroidKeyStore. SQLCipher i magazyn kluczy zapewniają rzeczywistą ochronę przechowywanych danych i zostały w raporcie odnotowane; nie przywracają jednak nieeksportowalności samego klucza. Praktyczne odzyskanie poza aplikacją i użycie klucza pozostają testem P0, dlatego raport klasyfikuje MOB-A-08 jako Średnie, warunkowo Wysokie po potwierdzeniu, zamiast ogłaszać bez dowodu przejęcie podpisu kwalifikowanego.
Publiczne 192 pliki Kotlin nie obejmują warstw, które przesądzają o bezpieczeństwie. Dlatego obecny raport jest analizą binarną, a nie audytem źródłowym. Po otrzymaniu pełnego kodu należy powiązać wydanie z konkretną zmianą źródłową, odtworzyć kompilację, porównać artefakty i ponownie przejrzeć te same ustalenia na źródłach. Do czasu wykonania testów P0 nie należy wydawać zbiorczego zapewnienia o bezpieczeństwie ani alarmistycznego komunikatu o przełamaniu aplikacji.
CMSSignedData