ANALIZA STATYCZNA · DYNAMICZNA · INTEROPERACYJNOŚĆ eDO APP · AUDYT .NET 9 · ICU · Houdini · XALZ/LZ4 OD FAILFAST DO STABILNEGO STARTVIEW

Autor: Marek Wesołowski (WESMAR) · github.com/wesmar · kvc.pl

W skrócie. Artykuł obejmuje dwie części. Pierwsza to rozwiązanie problemu uruchomienia produkcyjnej wersji eDO App 1.7.1 (Xamarin/.NET 9, ARM) na WSA x86-64 przez translator Houdini: przyczyną awarii był splot biblioteki ICU, mostu native bridge, kodu wyciętego przez trimmer oraz optymalizacji wektorowych SIMD w bibliotece standardowej .NET. Druga część stanowi niezależny audyt bezpieczeństwa klienta: analiza statyczna assembly oraz weryfikacja dynamiczna na Xiaomi 13T Pro. Ustalenia potwierdzają brak lokalnego kontenera tożsamości, powiązanie sekretów z Android Keystore, implementację PACE i Chip Authentication na bibliotece Bouncy Castle oraz poprawną konfigurację certyfikatów. Główne uchybienie to brak wymuszonego podglądu dokumentu w kroku podpisu kwalifikowanego oraz brak filtracji wejścia z klawiatury sprzętowej.

Werdykt: ocena 7,3/10 — dobra (górny przedział)
eDO App 1.7.1 działa na WSA 2407 z aktywnym MagiskSU, a sam audyt pokazuje klienta o dojrzałej architekturze bezpieczeństwa, wyraźnie dojrzalszej niż mObywatel. Badanie objęło analizę statyczną trzech głównych assembly oraz częściową weryfikację dynamiczną na fizycznym Xiaomi 13T Pro. Mocne strony: brak lokalnego kontenera tożsamości, dane pobierane z karty, sekrety w Android Keystore wspieranym przez TEE, standardowe protokoły PACE i Chip Authentication na Bouncy Castle, aktywna flaga FLAG_SECURE, pinning certyfikatów oraz walidacja wejść QR i deep-link. Zidentyfikowane wady: brak wymuszonego podglądu dokumentu w kroku podpisu kwalifikowanego EDO-A-02 oraz brak filtrowania klawiatury sprzętowej w polach PIN i PUK EDO-Q-02. Ocena ważona architektury klienta wynosi ≈ 7,3/10 (ocena dobra, w górnym przedziale skali) zgodnie z rozbiciem w sekcji 22. Weryfikacja ruchu telemetrii, atestacji klucza oraz integralności po stronie serwera wymaga uprawnień root i stanowi zakres kolejnego etapu.
Status z 17–18 września 2026 r. Środowisko WSA: proces pl.pwpw.edohub stabilny po zimnym starcie, przejście RegulationsView → StartView bez błędów krytycznych. Weryfikacja dynamiczna na fizycznym Xiaomi 13T Pro z systemem Android 16: wersja produkcyjna 1.7.1 z Google Play bez debugowania; blokada FLAG_SECURE potwierdzona czarnym zrzutem ekranu; magazyn kluczy wspierany w TEE bez układu StrongBox; uprawnienia standardowe. Badanie ruchu telemetrii, ramek APDU oraz atestacji klucza wymaga uprawnień root.
Pobierz narzędzie — patch-eDO-App.zip   Zobacz nagranie na YouTube
Zawiera skrypty weryfikacyjne PowerShell, apktool.jar, definicje profili oraz payload-manifest.json. Skrypt patch-eDO-App-1.7.1-WSA.ps1 sprawdza obecność podkatalogu payload/ zdefiniowanego w manifeście; ze względów licencyjnych celowo go tam nie ma, a skrypt zatrzyma się z błędem braku plików wsadowych (zob. sekcja 21).

1. Zakres badania i granice wniosków

Badam własną instalację, własne WSA, własne konto i własny dowód. Celem jest interoperacyjność oraz niezależna ocena bezpieczeństwa klienta. Rozdzielam trzy poziomy pewności:

  1. Dowód statyczny: obecny w APK, bibliotekach ELF i IL.
  2. Dowód wykonaniowy: zaobserwowany w procesie, logcat, stosach, aktywnościach i ruchu TLS.
  3. Hipoteza audytowa: trop wymagający jeszcze kontrolowanego testu.

APK badawczy podpisałem oddzielnym certyfikatem. Opisuję każdą modyfikację, aby późniejsze wyniki nie mieszały zachowania producenta z zachowaniem laboratorium. Podczas akceptacji regulaminu aplikacja wykonała ruch do serwera. Nie eksportowałem danych dokumentu, PIN-u ani innych danych osobowych.

2. Materiał i powtarzalność

Element Wartość
Pakiet pl.pwpw.edohub
Wersja 1.7.1
Stos Xamarin / Mono / .NET 9
ABI arm64-v8a, armeabi-v7a; brak x86/x86-64
Oryginalny APK 89 869 095 B
SHA-256 oryginału 07768A1F0A07493D6A89A981E9603863787D04A750E04536FBB2E3CA8105B84B
Działający APK badawczy 112 204 867 B
SHA-256 APK badawczego B642FF8F5D8D3003F3B4585FCD7B73EA4BBA66AE1274B5F11EE2D072084D294A
Assembly store ARM64 11 915 712 B
SHA-256 końcowego store 1B546954849CE1943744242A9B108043F37F4653878AE3A955AC4D1ADD0CCBF3

Całość trzymam w C:\Temp\eDO_App, osobno od mObywatela. Nie odinstalowałem, nie nadpisałem ani nie podpisałem ponownie działającego mObywatela.

3. Stanowisko WSA

Warstwa Stan podczas testu
WSA 2407.40000.4.0, Android 13, host x86-64
Translacja ARM libhoudini.so
ADB 127.0.0.1:58526
Root MagiskSU: uid=0, u:r:magisk:s0
SELinux Enforcing
Zygisk aktywny; eDO App działa z nim po końcowej konfiguracji
mObywatel zainstalowany i nienaruszony

W jednej z pośrednich konfiguracji problem z native bridge zbiegł się z włączonym Zygiskiem i dał komunikat EM_AARCH64/EM_X86_64. Była to błędna korelacja, nie warunek działania eDO App. Po końcowej konfiguracji WSA aplikacja działa z aktywnym Zygiskiem i aktywnym MagiskSU. Stan ten traktuję jako potwierdzony funkcjonalnie; gdy WSA zostanie ponownie uruchomiony, uzupełnię go jeszcze o bieżący zrzut właściwości systemowych.

4. Xamarin kontra Kotlin: dlaczego eDO to trudny klient dla WSA

Zanim wejdę w szczegóły, muszę wyjaśnić różnicę, która przesądza o całej trudności, a której w audycie mObywatela w ogóle nie było.

mObywatel napisano w Kotlinie. Kod kompiluje się do bajtkodu Dalvik i wykonuje go natywnie wirtualna maszyna Androida (ART). Na WSA ART działa jako proces x86-64, więc logika aplikacji chodzi natywnie; przez Houdini przechodzi jedynie kilka drobnych bibliotek natywnych. Dlatego dla WSA mObywatel jest „lekki".

eDO App napisano w C# na .NET. To rodowód Xamarina, dziś kontynuowany jako .NET MAUI (co widać w APK po obecności Microsoft.Maui.Essentials). Logika aplikacji to kod pośredni IL, który wykonuje runtime Mono, a Mono to duża, natywna biblioteka ARM (libmonosgen-2.0.so). W efekcie translator Houdini na WSA wykonuje pełny silnik uruchomieniowy Mono wraz z biblioteką standardową .NET, warstwą ICU i kodem natywnym aplikacji, a nie jedynie pojedyncze wywołania JNI. Stanowi to przyczynę problemów opisanych poniżej.

Kontekst technologiczny: Microsoft wygasił „klasycznego" Xamarina 1 maja 2024 r. i scalił go z ujednoliconym .NET pod postacią .NET MAUI. Powodem był koszt utrzymywania osobnego stosu mobilnego obok głównego .NET oraz chęć jednego modelu dla wszystkich platform. eDO App wykorzystuje platformę .NET 9 oraz MAUI, zachowując tę samą architekturę: kod zarządzany wykonywany przez natywne środowisko Mono. Na fizycznym procesorze ARM działa to bez narzutu, natomiast w procesie translacji ARM→x86-64 pod Houdini stanowi przypadek o najwyższym stopniu złożoności.

Aplikacja zawiera własne środowisko Mono oraz biblioteki natywne ARM, co wymusza translację pełnego stosu .NET pod emulatorem Houdini.

flowchart LR APK[eDO App ARM64] --> MONO[Mono / .NET 9] MONO --> BCL[System.Private.CoreLib] MONO --> GLOB[libSystem.Globalization.Native.so] BCL --> H[Houdini ARM64 → x86-64] GLOB --> H H --> WSA[Android 13 / WSA]

Na tej granicy kolejno ujawniły się: ICU 70/72, host bridging, Scudo, bezwarunkowy LoadICU, reguły porządkowania tekstu, inicjalizacja MvvmCross, SearchValues i parser HTTP.

5. ICU 70 kontra ICU 72

WSA ma ICU 70, a eDO App oczekuje ICU 72. Houdini mostkuje wybrane biblioteki do hosta. Gdy ARM64 żądał symboli ICU 72, bridge szukał ich w hostowym ICU 70.

Próba dostarczenia czystych bibliotek gościa ARM64 ICU 72 zakończyła się niepowodzeniem: pliki danych były otwierane poprawnie, jednak funkcja IcuRegistration::registerIcu() powodowała błąd w alokatorze Scudo podczas zwalniania obiektów tymczasowych. Czterobajtowy skok w libicuuc.so omijał ten SIGABRT, ale wywołanie ucol_open_72+56 nadal kończyło się błędem SIGSEGV.

To był potrzebny eksperyment z negatywnym zakończeniem: dowiódł, że problem nie leży już w ścieżce, uprawnieniach ani braku danych. Ścianą były reguły sortowania ICU wykonywane pod Houdini. Finalna metoda nie modyfikuje WSA i nie podmontowuje ICU do systemu.

6. Trimmer usunął strażnik globalizacji

Aplikację zbudowano z InvariantGlobalization=false. ILLink usunął gałąź if (!Invariant). W GlobalizationMode.Settings..cctor wynik gettera był wyrzucany:

call Settings.get_Invariant
pop
ldloca.s 0
call TryGetAppLocalIcuSwitchValue
...
call LoadICU
...
call Environment.FailFast

Przestawienie gettera na true nie pomagało, bo nikt nie używał wyniku. Skuteczny patch zatrzymał konstruktor przed LoadICU: w CoreLib offset 0x82100 zmieniłem z 0x12 (ldloca.s) na 0x2A (ret). W LZ4 był to literał pod 0x9D4DEB.

Po tej zmianie zniknął Environment.FailFast: Unable to load required ICU Globalization data.

7. Globalizacja bez niezaładowanego ICU

Po pominięciu LoadICU część wskaźników była pusta. GlobalizationNative_GetSortHandle wykonywało blr przez ucol_open == 0, więc proces skakał pod pc=0.

Najpierw zastąpiłem trzy punkty wejścia: GetSortHandle, CompareString i GetSortKey. Następnie objąłem poprawką wszystkie 36 eksportowanych funkcji globalizacji używanych przez runtime. Wyszukiwanie i porównania otrzymały bezpieczne procedury ARM64, a funkcje lokalizacyjne deterministyczne wartości. Dotyczy to m.in. IndexOf, LastIndexOf, StartsWith, EndsWith, CompareString, GetLocaleName i IsPredefinedLocale.

Końcowa libSystem.Globalization.Native.so ma 71 960 B:

E14B0710A9BD310FA26F7B83426E71D4AC0AE06688FB574807B4A2EB800B99BF

To warstwa zgodności laboratorium, a nie pełny zamiennik językowy ICU.

8. Blokady w kodzie zarządzanym

Miejsce Objaw Zmiana laboratoryjna
_b3 / ISmartAppService przedwczesne subskrypcje MvvmCross pominięcie wadliwej części konstruktora
AppStart.<NavigateToFirstViewModel>d__10 Root detected IsRootedOrDangerous = false
RegulationsView.<ViewAppeared>d__17 brak bezpiecznego keyguarda WSA IsDeviceSecurityHighEnough = true

Zmiany weszły do Pwpw.Mobile.EdoHub.Core.dll. Wpis #67 ponownie skompresowałem do dokładnie 742 032 B.

Granica wniosku. Lokalna możliwość wyłączenia kontroli roota i blokady ekranu dowodzi, że te decyzje są podejmowane po stronie klienta. Nie dowodzi póki co obejścia NFC, PIN-u, uwierzytelnienia ani podpisu.

9. Obfuskacja, która nie ukryła protokołu

Nazwy prywatnych klas i metod skrócono do _bz, _b6, _b3, ac.g, s.e i b.d. Zwiększyło to koszt ręcznej analizy, ale nie usunęło informacji potrzebnych do odtworzenia przepływu.

Pozostały czytelne:

  • interfejsy, DTO, view-modele i część metod publicznych;
  • tokeny metod i typów;
  • maszyny stanów IAsyncStateMachine i MoveNext;
  • zależności MvvmCross i System.Net.Http;
  • literały endpointów, nagłówków i błędów;
  • stosy łączące kod obfuskowany z RegulationsViewModel.
Ślad Wniosek
_bz rejestrowany jako IEdoHubService rola klasy mimo zmienionej nazwy
_bz/ac::g(), token 0x060001F4 maszyna stanu żądania HTTP
_bz::j(...), token 0x060001B9 miejsce pochłaniające wyjątek
RegulationsViewModel.<AcceptRegulations>d__20.MoveNext wejście z przycisku regulaminu
edoservices_012/acceptTerms względny endpoint

Obfuskowany łańcuch _bz+s.e → _bz+ac.g → _bz+b.d prowadził wprost do maszyny stanu AcceptRegulations. Literały X-Device, text/plain oraz komunikat probably no internet connection nie były jawne: odzyskano je przez zdeszyfrowanie zaciemnionych łańcuchów (§9.2).

EDO-P-01: obfuskacja nie ukrywa przepływu protokołu. Zastosowano zmianę nazw, spłaszczenie przepływu sterowania oraz szyfrowanie literałów, jednak zachowane metadane .NET i odwracalny offline schemat szyfrowania łańcuchów pozwoliły na pełne odtworzenie logiki klienta, adresów endpointów oraz parametrów kryptograficznych. Fakt ten rzutuje na ocenę odporności aplikacji na inżynierię wsteczną.

XALZ/LZ4 nie jest obfuskacją. Po odczytaniu tabeli deskryptorów assembly można wyodrębnić, zanalizować po tokenach i wstawić ponownie.

9.1. Czym jest ta obfuskacja i na ile jest skuteczna

Obfuskatorem jest PreEmptive Dotfuscator (ślady w metadanych Core.dll). Zastosowano trzy techniki. Po pierwsze przemianowanie symboli: prywatne klasy i metody dostały krótkie nazwy _bz, _b6, ac.g, s.e, b.d. Po drugie zaciemnienie przepływu sterowania: właściwa maszyna stanów HTTP (_bz/ac::g(), token 0x060001F4) jest rozbita na przełączniki i fałszywe stany, które utrudniają odtworzenie kolejności wykonania. Po trzecie szyfrowanie literałów łańcuchowych: łańcuchy przechowywane są jako zaszyfrowane tablice bajtów i odszyfrowywane w czasie wykonania (§9.2).

Charakterystyczne dla tego profilu jest zachowanie metadanych .NET przy jednoczesnym szyfrowaniu literałów. Jawne pozostały wszystkie interfejsy publiczne, DTO i view-modele, tokeny metod i typów oraz nazwy maszyn stanu async (<AcceptRegulations>d__20), a także zależności MvvmCross i System.Net.Http, które bezpośrednio ujawniają architekturę rozwiązania. Literały protokołu (endpoint edoservices_012/acceptTerms, nagłówek X-Device, text/plain oraz komunikaty błędów) były natomiast zaszyfrowane i wymagały odwrócenia szyfru (§9.2). Odtworzenie logiki sprowadziło się zatem do mapowania tokenów oraz zdeszyfrowania łańcuchów.

Deobfuskacja sprowadziła się więc do prześledzenia stosów i tokenów. Odzyskana mapa jest jednoznaczna:

Zaciemniona nazwa Token Rzeczywista rola
_bz/b::d() 0x060001BE AcceptTermsOfService
_b6::r() 0x06000294 zwraca edoservices_012/acceptTerms
_bz::j(string, RequestBase, HttpMethod) 0x060001AC wspólny wrapper klienta HTTP
_bz/ac::g() 0x060001F4 maszyna stanów żądania HTTP
_bz::j(Exception, string, HttpResponseMessage) 0x060001B9 logger Sentry połykający szczegóły błędu

W aplikacji wykorzystano komercyjny pakiet Dotfuscator w konfiguracji łączącej zmianę nazw symboli, spłaszczenie przepływu sterowania oraz szyfrowanie literałów łańcuchowych, przy zachowaniu metadanych .NET. Jest to konfiguracja istotnie mocniejsza od samej zmiany nazw, jednak odwracalny offline schemat szyfrowania łańcuchów oraz nienaruszone metadane sprawiają, że nie chroni ona architektury protokołu, adresów endpointów ani operacji kryptograficznych. Stanowi to rozwinięcie ustalenia EDO-P-01: zastosowany poziom zaciemnienia kodu podnosi próg analizy, lecz pozostaje nieadekwatny do wartości chronionego zasobu tożsamościowego.

9.2. Szyfrowanie literałów i jego odwrócenie

Wbrew pierwotnemu założeniu Dotfuscator zaszyfrował literały łańcuchowe. W Core.dll na 734 łańcuchy jawne przypada 2230 zaszyfrowanych, w Droid.dll kolejne kilkaset. Zaszyfrowany łańcuch ma postać tablicy bajtów odszyfrowywanej w czasie wykonania przez wspólną metodę _a3::b(string, int32, string).

Sam deszyfrator jest podręcznikowym przykładem zaciemnienia przepływu. Jego konstruktor statyczny to festiwal śmieciowej arytmetyki: mnożenie przez jeden, dodawanie i natychmiastowe odejmowanie tej samej stałej (add 0x900000 / sub 0x900000), dzielenie przez jeden oraz switch na wartości zawsze prawdziwej. Instrukcje te nie zmieniają wyniku; służą wyłącznie zmyleniu dekompilatora i rozbiciu liniowego odczytu.

Sam algorytm szyfru jest natomiast słaby. Dla każdego znaku UTF-16 o bajtach (młodszy, starszy) tekst jawny odtwarza się jako jawny = starszy XOR ((młodszy + 1) mod 256). Młodszy bajt każdego znaku pełni rolę strumienia klucza, rosnącego o dwa na znak, przez co każdy szyfrogram niesie własny klucz. W praktyce oznacza to, że argument A_1 przekazywany do deszyfratora jest zbędny, a wszystkie łańcuchy da się odzyskać w pełni offline, bez uruchamiania aplikacji. Napisany na tej podstawie deszyfrator odtworzył komplet literałów.

Odzyskana zawartość pokazuje kluczowe parametry architektury, które mechanizm obfuskacji miał ukryć:

Kategoria Kluczowe zdeszyfrowane literały (Core.dll i Droid.dll)
Usługi i nagłówki HTTP https://www.edoapp.pl, https://edopost.pwpw.pl, sklep Sigillum, edoservices_012/acceptTerms, X-Device, text/plain, X-Token, X-CSRF-Token
Kryptografia i protokoły PACE, ChipAuthentication, PassiveAuthentication, ActiveAuthentication, ECDSA, SHA384WITHECDSA, NONEwithECDSA, AES/CBC/PKCS7Padding
Klucze magazynu danych EDOAPP_CAN, EDOAPP_MRZ, EDOAPP_SESSION_*, EDOAPP_AUTHENTICATOR_ID, EDOAPP_AUTHENTICATOR_PRIVATE_KEY_BASE64, CERT_ID
Wbudowany materiał krypto Osadzony klucz publiczny RSA (-----BEGIN PUBLIC KEY-----), blok licencyjny (appId=pl.pwpw.*, expiryDate=2023-06-10, podpis ECDSA)
Detekcja środowiska i root /sbin/su, /system/bin/su, /data/local/su, magisk, test-keys, keyguard, de.robv.android.xposed.installer, com.topjohnwu.magisk, RootCloak
Obsługa interfejsu NFC/APDU Extended Length APDU not supported, Error (IsoDep is null!), Tag not supported, nfc_dowod, nfc_passport, PinBlockedException
Przepływ podpisu i PDF Plik do podpisu, Plik podpisany e-dowodem, Podpis Sigillum, Podpis osobisty e-dowodem, application/pdf
Rozwiń pełny zrzut zdeszyfrowanych literałów Droid.dll (348 ciągów w responsywnym układzie wielokolumnowym)
  1. This device doesn't support Camera2 API.
  2. Camera2VideoFragment
  3. Couldn't find any suitable video size
  4. No suitable video size
  5. No suitable preview size
  6. No suitable preview size - even rotated!
  7. Couldn't find any suitable preview size
  8. CameraBackground
  9. Time out waiting to lock camera opening.
  10. Cannot access the camera.
  11. dialog
  12. Interrupted while trying to lock camera opening.
  13. camera
  14. Interrupted while trying to lock camera closing.
  15. CameraPreview
  16. video-
  17. yyMMdd-hhmmss
  18. .mp4
  19. layout_inflater
  20. /3
  21. android.resource://
  22. drawable
  23. string
  24. message
  25. id
  26. android
  27. _description
  28. pin_prompt
  29. sign_success
  30. info_dialog
  31. #9697A1
  32. #E42552
  33. Resource not found (id = 0)
  34. Resource name was empty or null
  35. Resource container type was empty or null
  36. Resource id cannot be null
  37. navigated_to_govpl_getedo
  38. card_data
  39. data_display_get_edo_text
  40. data_display_get_edo_linked_text
  41. dialog_open
  42. dialog_description
  43. title
  44. _display_name
  45. mime_type
  46. _size
  47. Plik do podpisu
  48. Plik podpisany e-dowodem
  49. download
  50. application/pdf
  51. android.intent.action.SEND
  52. android.intent.extra.STREAM
  53. android.intent.action.VIEW_DOWNLOADS
  54. android.resource
  55. .pdf
  56. - podpisano eDO App
  57. _display_name=?
  58. FACETEC INITIALIZATION PROBLEM!
  59. Invalid processor initialization
  60. keys are empty!
  61. null
  62. dDWqln4osRAo8vr1a8OzfPAldmI3Gwki
  63. -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv2M11lmIlLullW795/a8 d9dcXT7U/ia+qHVsQGvzSDKmctwtMrNPrwCqSkqx23tQqnjKL5TxZxDcl82uBUAw NtfF/R9JdTPOrnw45j314u0VsoWKJRI9IniRfOMu2rDHILdafbuOwylr7th21vz7 6MQVDd+CJC5uBGSxm5OxZNNwnXkKvfbLqGBXdVbpIiCkj/H0dm85JPnQtZGCFNYN TJKmHNuYddJnN32hPFd0Ec/HTik3D23gwhw6RWvcjhEcgDPjdI1X1Xg6nYzbe78g y5BLe+z6zzhBBIeAn10zLRjxYRP9I7FBHD9DkKoYAY52//YFc1kPnvubdTEDGUHy 2wIDAQAB -----END PUBLIC KEY-----
  64. appId = pl.pwpw.* expiryDate = 2023-06-10 key = 003044022038003c16420f54489095409a4a96bf303dc11a6cc17069427bd5d7fe3f637af302202d1df9521ecb7e556ebc7a0e1eab5e50b9d9f504b47d3aaab16ebc7128e47868
  65. authorizationMissingHelp_title
  66. authorizationMissingHelp_message
  67. inactiveCertificateHelp_title
  68. inactiveAuthenticationHelp_message
  69. inactiveAuthorizationHelp_message
  70. android.nfc.extra.ADAPTER_STATE
  71. NFCAdapterStateChanged:
  72. Tag not supported
  73. Error (IsoDep is null!)
  74. Not connected, cannot transmit/receive
  75. nfc
  76. NFCAdapter was null
  77. android.permission.NFC
  78. Android API below KitKAt is too low!
  79. NFC not available
  80. presence
  81. rate_dialog_opened
  82. rate_dialog_navigated_to_store
  83. market://details?id=
  84. com.android.vending
  85. android.intent.action.VIEW
  86. https://play.google.com/store/apps/details?id=com.pl.pwpw.edohub
  87. /system/app/Superuser.apk
  88. /sbin/su
  89. /system/bin/su
  90. /system/bin/.ext/su
  91. /system/xbin/su
  92. /data/local/xbin/su
  93. /data/local/bin/su
  94. /system/usr/we-need-root/su
  95. /system/sd/xbin/su
  96. /system/bin/failsafe/su
  97. /data/local/su
  98. /data/su
  99. /dev/su
  100. /cache/su
  101. /su/bin/su
  102. PATH
  103. supersu
  104. superuser
  105. root_detected_processname
  106. magisk_detected_fullpath
  107. su
  108. root_detected_fullpath
  109. root_detected_path
  110. magisk
  111. test-keys
  112. root_detected_emulator
  113. root_detected_debuggable
  114. root_detected_debugger
  115. root_detected_dangerous
  116. de.robv.android.xposed.installer
  117. com.saurik.substrate
  118. com.android.vending.billing.InAppBillingService.LUCK
  119. com.android.vending.billing.InAppBillingService.CLON
  120. com.android.vending.billing.InAppBillingService.COIN
  121. com.devadvance.rootcloak
  122. com.devadvance.rootcloakplus
  123. com.zachspong.temprootremovejb
  124. com.amphoras.hidemyroot
  125. com.amphoras.hidemyrootadfree
  126. com.formyhm.hiderootPremium
  127. com.formyhm.hideroot
  128. com.noshufou.android.su
  129. com.noshufou.android.su.elite
  130. eu.chainfire.supersu
  131. com.koushikdutta.superuser
  132. com.thirdparty.superuser
  133. com.yellowes.su
  134. com.topjohnwu.magisk
  135. com.kingroot.kinguser
  136. com.kingo.root
  137. com.smedialink.oneclickroot
  138. com.zhiqupk.root.global
  139. com.alephzain.framaroot
  140. input_method
  141. android.permission.POST_NOTIFICATIONS
  142. connectivity
  143. android.settings.NFC_SETTINGS
  144. EXIT
  145. minimizeApp
  146. android_id
  147. keyguard
  148. notification
  149. android.settings.APP_NOTIFICATION_SETTINGS
  150. android.provider.extra.APP_PACKAGE
  151. Version
  152. Device
  153. MainLog
  154. Extended Length APDU not supported
  155. ID read successfully
  156. ID_read_successfully
  157. Cancel
  158. OK
  159. device
  160. android.hardware.camera
  161. off
  162. torch
  163. rate
  164. Brak danych
  165. [on|off]
  166. Prints usage information. If specified all other arguments (except 'v') are ignored
  167. Prints version and copyright information
  168. verbose
  169. Prints information about the decoded codestream
  170. on
  171. pfile
  172. <filename>
  173. Loads the arguments from the specified file. Arguments that are specified on the command line override the ones from the file. The arguments file is a simple text file with one argument per line of the following form: <argument name>=<argument value> If the argument is of boolean type (i.e. its presence turns a feature on), then the 'on' value turns it on, while the 'off' value turns it off. The argument name does not include the '-' or '+' character. Long lines can be broken into several lines by terminating them with '\'. Lines starting with '#' are considered as comments. This option is not recursive: any 'pfile' argument appearing in the file is ignored.
  174. res
  175. <resolution level index>
  176. The resolution level at which to reconstruct the image (0 means the lowest available resolution whereas the maximum resolution level corresponds to the original image resolution). If the given index is greater than the number of available resolution levels of the compressed image, the image is reconstructed at its highest resolution (among all tile-components). Note that this option affects only the inverse wavelet transform and not the number of bytes read by the codestream parser: this number of bytes depends only on options '-nbytes' or '-rate'.
  177. <filename or url>
  178. The file containing the JPEG 2000 compressed data. This can be either a JPEG 2000 codestream or a JP2 file containing a JPEG 2000 codestream. In the latter case the first codestream in the file will be decoded. If an URL is specified (e.g., http://...) the data will be downloaded and cached in memory before decoding. This is intended for easy use in applets, but it is not a very efficient way of decoding network served data.
  179. This is the name of the file to which the decompressed image is written. If no output filename is given, the image is displayed on the screen. Output file format is PGX by default. If the extension is '.pgm' then a PGM file is written as output, however this is only permitted if the component bitdepth does not exceed 8. If the extension is '.ppm' then a PPM file is written, however this is only permitted if there are 3 components and none of them has a bitdepth of more than 8. If there is more than 1 component, suffices '-1', '-2', '-3', ... are added to the file name, just before the extension, except for PPM files where all three components are written to the same file.
  180. <decoding rate in bpp>
  181. Specifies the decoding rate in bits per pixel (bpp) where the number of pixels is related to the image's original size (Note: this number is not affected by the '-res' option). If it is equalto -1, the whole codestream is decoded. The codestream is either parsed (default) or truncated depending the command line option '-parsing'. To specify the decoding rate in bytes, use '-nbytes' options instead.
  182. -1
  183. nbytes
  184. <decoding rate in bytes>
  185. Specifies the decoding rate in bytes. The codestream is either parsed (default) or truncated depending the command line option '-parsing'. To specify the decoding rate in bits per pixel, use '-rate' options instead.
  186. parsing
  187. Enable or not the parsing mode when decoding rate is specified ('-nbytes' or '-rate' options). If it is false, the codestream is decoded as if it were truncated to the given rate. If it is true, the decoder creates, truncates and decodes a virtual layer progressive codestream with the same truncation points in each code-block.
  188. ncb_quit
  189. <max number of code blocks>
  190. Use the ncb and lbody quit conditions. If state information is found for more code blocks than is indicated with this option, the decoder will decode using only information found before that point. Using this otion implies that the 'rate' or 'nbyte' parameter is used to indicate the lbody parameter which is the number of packet body bytes the decoder will decode.
  191. l_quit
  192. <max number of layers>
  193. Specifies the maximum number of layers to decode for any code-block
  194. m_quit
  195. <max number of bit planes>
  196. Specifies the maximum number of bit planes to decode for any code-block
  197. poc_quit
  198. Specifies the whether the decoder should only decode code-blocks included in the first progression order.
  199. one_tp
  200. Specifies whether the decoder should only decode the first tile part of each tile.
  201. comp_transf
  202. Specifies whether the component transform indicated in the codestream should be used.
  203. debug
  204. Print debugging messages when an error is encountered.
  205. cdstr_info
  206. Display information about the codestream. This information is: - Marker segments value in main and tile-part headers, - Tile-part length and position within the code-stream.
  207. nocolorspace
  208. Ignore any colorspace information in the image.
  209. colorspace_debug
  210. Print debugging messages when an error is encountered in the colorspace module.
  211. MSWiA
  212. Polska Wytwórnia Papierów Wartościowych S.A.
  213. Podpis Sigillum
  214. Podpis osobisty e-dowodem
  215. Inny podpis
  216. Error stopping dialog
  217. #4C5358
  218. Error getting session token
  219. edoapp_session_
  220. biometry_match
  221. Biometry result
  222. Session interrupted
  223. EdoHubService session expired:
  224. or request details are null:
  225. EdoHubService biometry request details are null
  226. VerifyIdWithEnrollmentAndMatch response
  227. Failed
  228. OnAppOpenAttribution:
  229. OnAttributionFailure =
  230. OnInstallConversionFailure =
  231. OnConversionDataSuccess:
  232. Timestamp:
  233. https://e7d89e5a50a63199c58202068150b7a3@diagnostics.edoapp.pl/2
  234. [{Level}] {Message:l{NewLine:l}{Exception:l}
  235. screen_view
  236. firebase_screen_class
  237. TEST
  238. gplay_services_availibility
  239. reason
  240. unavailable
  241. ok
  242. <a href="
  243. ">
  244. </a>
  245. BuildNumber
  246. ID
  247. RequestDetails
  248. Data
  249. edo
  250. error_message
  251. (...)
  252. base64,
  253. serialNumber
  254. certSerialNumber
  255. dataRequest_
  256. ChangingSignatureTypeMode
  257. android.nfc.action.ADAPTER_STATE_CHANGED
  258. {0}
  259. {1}
  260. #DD3345
  261. _scroll
  262. _info_label
  263. #1E1E1E
  264. Operations
  265. ShowBuyCertificateButton
  266. Certificates
  267. ShouldCanBeRemembered
  268. Can
  269. TimeLeft
  270. nfc_passport
  271. nfc_dowod
  272. nfc_new
  273. POL
  274. error on SetElementsVisibility:
  275. <a href="https://www.edoapp.pl">www.edoapp.pl</a>
  276. #F0F8FE
  277. #FFFFFF
  278. #DDE3EF
  279. #282E6F
  280. #000000
  281. #ffffff
  282. ac_esc
  283. eb71b779-1756-49e8-ab9c-3dcca09b87b5
  284. RecordingCounter
  285. BottomText
  286. IsFaceDetected
  287. vm
  288. android.permission.CAMERA
  289. android.permission.RECORD_AUDIO
  290. FaceDetector gms is not operational!
  291. https://www.gov.pl/web/gov/uzyskaj-dowod-osobisty?utm_source=edoapp&utm_medium=app&utm_campaign=reflink
  292. <a href=
  293. <a href="http://e-dowod.gov.pl">e-dowod.gov.pl</a>.
  294. <a href="https://www.edoapp.pl/centrum-pomocy-i-kontaktu">https://www.edoapp.pl/centrum-pomocy-i-kontaktu</a>.
  295. MrzInfo
  296. pl
  297. Akceptuję <a href="
  298. ">regulamin</a>. Administratorem danych osobowych przetwarzanych w związku z korzystaniem z eDO App jest Polska Wytwórnia Papierów Wartościowych S.A. z siedzibą w Warszawie. Więcej informacji dostępnych jest <a href="
  299. ">tutaj</a>.
  300. I accept <a href="
  301. ">terms of service</a>. The administrator of personal data processed in connection with the use of the eDO App is Polska Wytwórnia Papierów Wartościowych S.A. which its registered office in Warsaw. More information is available <a href="
  302. ">here</a>.
  303. binding_error
  304. exception
  305. camera_not_found
  306. camera_init_error
  307. permission_missing
  308. process_camera_provider_instance_null
  309. Error starting camera
  310. INTENT_EXTRA_RESULT_KEY
  311. <a href = "https://www.gov.pl/web/gov/uzyskaj-dowod-osobisty?utm_source=edoapp&utm_medium=app&utm_campaign=reflink">
  312. </a> !
  313. change_environment
  314. change_sigillum_environment
  315. change_authenticator_environment
  316. Obecnie wybrane:
  317. Włączone
  318. Wyłączone
  319. MenuPositions
  320. UseExtendedApdu
  321. _validation
  322. _error
  323. _info
  324. FileName
  325. Splash EXIT
  326. sign
  327. android.intent.category.DEFAULT
  328. shortcut
  329. sign_document
  330. #E6E6E6
  331. Skaner kodu QR wymaga aparatu do działania.
  332. en
  333. Code received from intent
  334. code
  335. operation_id
  336. AppUpdateRequired
  337. https://play.google.com/store/apps/details?id=
  338. <a href="https://www.gov.pl/web/gov/uslugi-dla-obywatela/">
  339. </a>.
  340. Opublikowaliśmy nowy regulamin. Aktualną wersję regulaminu zawsze znajdziesz na naszej stronie <a href="
  341. We have published new terms of service. You can always find the current version of the regulations on our website <a href="
  342. pl.pwpw.edowod.AppSettings
  343. news
  344. content
  345. PESEL
  346. Gest
  347. Kalibracja
  348. Size cannot be negative.

Wniosek jest taki, że szyfrowanie literałów podnosi próg analizy pobieżnej, lecz przy samoopisującym się kluczu i zachowanych metadanych nie chroni ani architektury protokołu, ani parametrów kryptograficznych, ani osadzonego materiału klucza.

10. Pierwszy skrót w SearchValues i jego koszt

CompareInfo..cctor tworzył SearchValues<char> z prawidłowego, stałego zbioru ASCII. Pod Houdini TryGetSingleRange<char> rzucało IndexOutOfRangeException.

Pierwsze obejście zastąpiło fabryki SearchValues.Create obiektem EmptySearchValues<T>. Aplikacja przeszła do ekranu regulaminu, co jednak naruszyło semantykę parserów w bibliotece standardowej BCL. Po kliknięciu przycisku walidator nagłówka wywoływał:

int invalidIndex = input.IndexOfAnyExcept(s_tokenChars);

Pusty zbiór zwracał 0, więc X-Device był uznawany za nieprawidłowy. Doraźne EmptySearchValues<T>.IndexOfAnyExcept => -1 przepuściło nazwę, ale parser text/plain nadal nie znajdował ukośnika. Użytkownik widział „brak Internetu”, mimo że Internet działał.

11. Jak zdjąłem fałszywy komunikat sieciowy

Warstwa aplikacji przechwytywała właściwy wyjątek i zastępowała go probably no internet connection. Zrobiłem dwie tymczasowe kompilacje diagnostyczne.

W _bz::j(Exception,string,HttpResponseMessage), token 0x060001B9, IL 0x1B9, offset 0x24F91, zmieniłem:

02 28 55 01 00 0A 26 2A

na ldarg.0; throw:

02 7A 00 00 00 00 00 00

To odsłoniło wrapper. Następnie w _bz/ac::g(), token 0x060001F4, IL 0x1DA, offset 0x2FDC2, rzuciłem zachowany wyjątek wewnętrzny. logcat podał dokładną przyczynę:

System.FormatException: net_http_headers_invalid_value, text/plain
 at System.Net.Http.Headers.MediaTypeHeaderValue.CheckMediaTypeFormat
 at System.Net.Http.StringContent..ctor
 at Xamarin.Android.Net.AndroidMessageHandler.<DoProcessRequest>d__140.MoveNext
 at _bz+s.e
 at _bz+ac.g
 at RegulationsViewModel.<AcceptRegulations>d__20.MoveNext

Obie sondy zostały wycofane po diagnozie i nie występują w finalnym APK.

12. Sieć i TLS działały

Proces łączył się z 185.31.88.82:8443. TLS 1.2 dochodził do skutku. Serwer przedstawiał certyfikat *.edoapp.pl, PWPW S.A., ważny 24.10.2025–24.10.2026. Względny endpoint to edoservices_012/acceptTerms.

Awaria następowała wcześniej, przy tworzeniu StringContent(..., "text/plain"). Komunikat o braku Internetu był błędnym mapowaniem wyjątku parsera.

Potwierdziłem transport TLS i certyfikat serwera. Certificate pinning, odporność na kontrolowany MitM i pełna polityka sieciowa pozostają do osobnego testu.

13. Końcowy fix SearchValues

Zamiast omijać kolejne walidacje, przywróciłem prawdziwe zbiory znaków i wyłączyłem tylko dwie wadliwe optymalizacje.

Token Metoda Zmiana
0x060027FA SearchValues.Create(ReadOnlySpan<byte>) oryginalny IL, 157 B
0x060027FB SearchValues.Create(ReadOnlySpan<char>) oryginalny IL, 299 B
0x0600280E EmptySearchValues<T>.IndexOfAnyExcept oryginalne zachowanie, 13 B
0x060027A1 get_IsVectorizationSupported ldc.i4.0; ret pod 0x8D631
0x060027FD TryGetSingleRange<T> false; ret po zapisaniu min/max, 0x8EA89

TryGetSingleRange prawidłowo obliczał min i max, a padał dopiero w fazie mapy Span<bool> z localloc. Po wcześniejszym zwrocie fabryka wybiera skalarną implementację bitmapową lub probabilistyczną z prawdziwym zbiorem.

flowchart TD A[SearchValues.Create: prawdziwy zbiór] --> B[oblicz min/max] B --> C[return false przed localloc] C --> D[AdvSimd = false] D --> E[skalarna implementacja BCL] E --> F[X-Device poprawny] E --> G[text/plain poprawny]

Nieosiągalny kod po ret wypełniłem nop, dzięki czemu CoreLib skompresował się do 1 066 866 B. Rekord XALZ miał 1 066 878 B i zmieścił się w pierwotnym slocie 1 066 975 B. Round-trip dał SHA-256:

5A1B8E868BC219C864E66995C498FBC6FAA29E444D9184050F365266E0DF01B7

14. Odrzucone hipotezy i ślepe odnogi

W dwudniowym reverse engineeringu ważne są również próby, które nie weszły do rozwiązania. Każda z nich rozstrzygnęła konkretną hipotezę.

Próba Co rzeczywiście wykazała Dlaczego nie jest rozwiązaniem końcowym
Settings.get_Invariant = true getter w assembly da się zmienić jego wynik jest pop-owany; trimmer usunął warunek
System.Globalization.Invariant=true w libarc.bin.so runtimeconfig można zmodyfikować bez zmiany rozmiaru payloadu wycięty kod nie odczytuje ustawienia w krytycznym miejscu
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 / debug.mono.env ścieżka konfiguracji Mono istnieje dla niedebugowalnej aplikacji nie odtwarza usuniętej gałęzi
prywatne nazwy libicuux.so, libicuixxn.so można ominąć listę host bridging Houdini po dołączeniu prywatnych libcxx.so i libbasx.so proces dochodził do ucol_open_72+56, gdzie nadal padał
bezpośredni loader icudt72l.dat dane ICU 72 były mapowane poprawnie błąd przesunął się do reguł porządkowania tekstu; nie był już błędem odnalezienia pliku
APEX z ICU 70 i 72 + bind mount biblioteki vendor ARM proces widział oba zestawy danych ten sam ucol_open_72+56 kończył się SIGSEGV
czterobajtowy skok b 0x1b1af8 w libicuuc.so omijał destruktory wywołujące Scudo na błędnym ABI usuwał jeden SIGABRT, ale nie naprawiał reguł porządkowania tekstu ICU
wcześniejsza interpretacja problemu z Zygiskiem komunikat EM_AARCH64/EM_X86_64 pojawił się w konfiguracji pośredniej obalona: końcowa konfiguracja działa z aktywnym Zygiskiem
SearchValues.Create -> EmptySearchValues aplikacja przechodziła inicjalizację CompareInfo i dochodziła do regulaminu globalnie fałszował znaczenie zbiorów znaków
EmptySearchValues.IndexOfAnyExcept -> -1 przepuszczał nazwę X-Device następny parser odrzucał text/plain
patch kontrolki „Internet jest dostępny” mógł zmienić komunikat w UI transport działał; przyczyna leżała w lokalnym parserze BCL

Najważniejszy zwrot nastąpił po porzuceniu łatania kolejnych objawów. Skoro X-Device i text/plain psuły się w dwóch różnych walidatorach, wspólną przyczyną musiał być mechanizm wyszukiwania znaków. Przywrócenie prawdziwych SearchValues potwierdziło tę diagnozę.

Nie zostawiłem bind mountów ICU ani eksperymentalnych zmian w WSA jako wymogu uruchomienia aplikacji. Finalne dostosowanie znajduje się w kopii APK; WSA pozostaje wspólnym, stabilnym środowiskiem również dla mObywatela.

15. Weryfikacja końcowa po dwóch dniach pracy

APK zbudowałem przez apktool 3.0.3, wyrównałem zipalign i podpisałem niezależnym certyfikatem badawczym. apksigner potwierdził v2 i v3.

Test Wynik
adb install -r Success
Zimny start Status: ok, LaunchState: COLD
Czas startu 2444 ms
Pierwsza aktywność RegulationsView
Proces po 12 s żywy, PID 17679
Akceptacja regulaminu żądanie przeszło
Aktywność po akceptacji StartView, topResumedActivity
Krytyczne wyjątki brak w końcowym logu
Root Magisk aktywny
mObywatel nienaruszony
Dowód wykonaniowy. Ten sam przepływ, który wcześniej odrzucał X-Device, a potem text/plain, po naprawie semantyki zbudował żądanie, przyjął odpowiedź i otworzył StartView bez restartu procesu.

16. Wstępne ustalenia bezpieczeństwa

EDO-A-01: kontrole roota i blokady ekranu są po stronie klienta

Wynik detekcji roota i KeyguardManager.isDeviceSecure() sterują lokalną nawigacją i można je zmienić w przepakowanym kliencie. Potwierdziłem to do ekranu startowego. Nie potwierdziłem jeszcze obejścia operacji kryptograficznej, NFC ani podpisu.

EDO-P-01: obfuskacja nie chroni przepływu protokołu

Skrócone nazwy i szyfrowanie literałów utrudniają analizę, ale zachowane metadane oraz odwracalny offline szyfr łańcuchów wystarczyły do odtworzenia usług, endpointów, parametrów kryptograficznych i obsługi wyjątków (§9.2).

EDO-Q-01: mylące mapowanie wyjątków

Lokalny FormatException został pokazany jako brak Internetu. Tak szerokie mapowanie utrudnia diagnostykę i ukrywa klasę faktycznej awarii.

EDO-Q-02: pole PIN/PUK bez wymuszenia cyfr na klawiaturze sprzętowej

Ekranowy keypad ogranicza wejście tylko wizualnie; pole ma jedynie filtr długości, a w kodzie brak DigitsKeyListener i numerycznego inputType. Klawiaturą sprzętową (USB, Bluetooth, host WSA) można wpisać znaki niecyfrowe, w moim przypadku WESMAR zamiast sześciu cyfr. Sytuacja ta nie stanowi obejścia uwierzytelnienia, ponieważ PIN weryfikuje procesor karty, generuje jednak ryzyko wyczerpania limitu prób PIN i PUK oraz niespójności walidacji. Szczegóły i test w 19.3.2.

EDO-C-01: silne związanie z ARM64

Brak x86-64 nie jest podatnością. W połączeniu z natywną globalizacją i AdvSimd ogranicza jednak interoperacyjność i ujawnia założenia niewidoczne na fizycznym ARM.

17. Co pozostaje do zbadania

  • rejestracja dokumentu przez NFC, CAN/MRZ i PIN;
  • przechowywanie sekretów i danych dokumentu;
  • Android Keystore, TEE i StrongBox;
  • certificate pinning i kontrolowany MitM;
  • reakcja API na przepakowany, ponownie podpisany klient;
  • integralność żądań, nonce, replay i powiązanie z urządzeniem;
  • podpis PDF i walidacja dokumentu wynikowego;
  • zachowanie po restarcie i długiej sesji.

Samo sforsowanie uruchomienia na WSA nie stanowiło jeszcze podstawy do wystawienia oceny bezpieczeństwa aplikacji. Odblokowanie środowiska badawczego i odzyskanie spójnych binariów otworzyło dopiero drogę do właściwego badania kodu i protokołów. Dlatego ocenę punktową odnoszę wyłącznie do wyników deasemblacji i weryfikacji wykonawczej mechanizmów klienta, opisanych w kolejnych rozdziałach.

18. Narzędzia i ślad dowodowy

  • apktool, zipalign, apksigner, adb, logcat, dumpsys;
  • dnfile i ildasm do metadanych oraz IL;
  • własny parser assembly-store i dekompresor XALZ/LZ4;
  • K4os.Compression.LZ4 w trybie L12_MAX;
  • analiza ELF, symboli i kodu ARM64;
  • IDA Pro 9.4;
  • SHA-256 oraz round-trip każdego przebudowanego assembly.

Powtarzalny patcher ma wersjoniezależny punkt wejścia i profile rozpoznawane po SHA-256 całego APK. Profil 1.7.1 został dwukrotnie sprawdzony od czystego pakietu. Nieznana przyszła wersja nie dostanie automatycznie starych offsetów: narzędzie zapisuje raport ABI oraz hashy krytycznych bibliotek i zatrzymuje się do czasu przeglądu nowego profilu.

19. Analiza statyczna: pierwszy przekrój

Ten etap dotyczy wyłącznie oryginalnego APK 1.7.1 o SHA-256 07768A1F0A07493D6A89A981E9603863787D04A750E04536FBB2E3CA8105B84B. Nie przenoszę wniosków z przebudowanej kopii badawczej na wydanie producenta. Zdekodowałem manifest i zasoby, a z oryginalnego assembly store ARM64 wyciągnąłem Pwpw.Mobile.EdoHub.Core.dll, Pwpw.Mobile.EdoHub.Droid.dll, Pwpw.Mobile.EdoHub.Interfaces.dll oraz Microsoft.Maui.Essentials.dll.

19.1. Szyfrowany magazyn dla danych lokalnych

Mimo zaciemnienia nazw pól, definicje typów oraz wywołania metod pozostają jawne w metadanych IL. SecureStorageKeys obejmuje PIN1, PIN2, CAN, MRZ, identyfikator urządzenia i certyfikatu, sól oraz prywatny klucz uwierzytelniający zapisany jako Base64. Kod biznesowy odczytuje i zapisuje je przez Microsoft.Maui.Storage.SecureStorage; lista certyfikatów jest przed zapisem serializowana do JSON.

Wyciągnięta z tego samego APK implementacja Maui nie jest zwykłym SharedPreferences. Tworzy AndroidX.Security.Crypto.EncryptedSharedPreferences z MasterKey w schemacie AES-256-GCM, nazwami kluczy szyfrowanymi AES-256-SIV i wartościami AES-256-GCM.

flowchart LR A[PIN1 / PIN2 / CAN / MRZ] --> B[Maui SecureStorage] C[identyfikator, sól, prywatny klucz] --> B B --> D[EncryptedSharedPreferences] D --> E[MasterKey AES-256-GCM] D --> F[nazwy AES-256-SIV] D --> G[wartości AES-256-GCM]

To jest potwierdzona ochrona zapisów aplikacji przed prostym odczytem pliku preferencji. Nie oznacza jeszcze, że klucz jest sprzętowo związany z TEE lub StrongBox: sam ten fakt wymaga odczytu właściwości Android Keystore na fizycznym urządzeniu i osobnego testu.

Jest też ważny szczegół dostępności. Gdy utworzenie zaszyfrowanych preferencji rzuca KeyStoreException, Java.Security.KeyStoreException albo BadPaddingException, implementacja Maui czyści cały magazyn i tworzy go ponownie. Kod nie pozwala jeszcze rozstrzygnąć, czy eDO App wcześniej synchronizuje wszystkie dane potrzebne do odtworzenia stanu. To będzie osobny test po kontrolowanej zmianie klucza lub przywróceniu kopii aplikacji.

19.1.1. Brak lokalnego kontenera tożsamości

To podstawowa różnica architektoniczna względem mObywatela. Zawartość bezpiecznego magazynu SecureStorageKeys obejmuje wyłącznie zdefiniowaną listę kluczy:

Klucz Rola
PIN1, PIN2 sekrety dostępowe
CAN, MRZ dane dostępu do karty (PACE)
DEVICE_ID, CERT_ID identyfikatory urządzenia i certyfikatu
AUTHENTICATOR_ID, AUTHENTICATOR_SALT tożsamość i sól authenticatora
AUTHENTICATOR_PRIVATE_KEY_BASE64 prywatny klucz uwierzytelniający, zapisany jako Base64
AUTHENTICATOR_CERTIFICATE_LIST lista certyfikatów

Magazyn nie zawiera danych osobowych: numeru PESEL, imienia, nazwiska, wizerunku ani grup danych. Modele PersonalData, Photo oraz DataGroup, gdzie DG1 oznacza MRZ, a DG2 wizerunek twarzy, istnieją wyłącznie jako obiekty w pamięci operacyjnej. Powstają podczas odczytu karty NFC i są niszczone komunikatem EndSessionMessage. Kod aplikacji nie zawiera żadnej warstwy trwałego zapisu danych dokumentu w SQLite, Realm ani w plikach płaskich.

Funkcja zapamiętywania danych nie narusza tej zasady. Metoda MrzInfo.RememberData zapisuje wyłącznie sumy kontrolne MRZ do bezpiecznego magazynu pod kluczem MRZ. Z kolei DocumentChoiceViewModel.RememberDocumentChoice to flaga interfejsu w jawnym rejestrze StorageKeys pod kluczem IdDocumentChoiceKey. Aplikacja utrwala jedynie parametry dostępowe do chipa NFC oraz preferencje widoku, bez danych tożsamościowych.

Architektura mObywatela opierała się na trwałym kontenerze mDowodu zapisanym w pamięci masowej, chronionym programowo funkcją PBKDF2 bez powiązania z Android Keystore, co umożliwiało atak offline na kopię danych. Aplikacja eDO App nie posiada takiego kontenera. Źródłem danych pozostaje fizyczny chip dowodu osobistego odczytywany przez interfejs NFC. W pamięci urządzenia pozostają jedynie tokeny sesyjne chronione przez EncryptedSharedPreferences z kluczem głównym w Android Keystore. W warstwie ochrony danych w spoczynku eDO App reprezentuje bezpieczniejszy model.

Dwa elementy techniczne wymagają odnotowania. Po pierwsze, klucz AUTHENTICATOR_PRIVATE_KEY_BASE64 zapisano w formacie Base64 zaszyfrowanym kluczem głównym, zamiast użyć nieeksportowalnego uchwytu w Keystore. Po odszyfrowaniu magazynu klucz prywatny można wyodrębnić. Po drugie, sprzętowe wiązanie klucza zależy od konkretnego telefonu. Test na fizycznym Xiaomi 13T Pro z systemem Android 16 wykazał obecność flagi android.hardware.hardware_keystore, czyli wsparcie dla środowiska TEE, przy braku flagi android.hardware.strongbox_keystore. Domyślny MasterKey biblioteki AndroidX jest na tym urządzeniu chroniony sprzętowo w TEE, bez dedykowanego układu StrongBox. Sama zdolność sprzętowa jest potwierdzona i odróżnia eDO od czysto programowego kontenera mObywatela.

19.1.2. Weryfikacja magazynu na żywym urządzeniu (WSA z rootem)

Ustalenia statyczne z §19.1 i §19.1.1 potwierdziłem empirycznie, odczytując prywatny katalog aplikacji na zrootowanym WSA (/data/data/pl.pwpw.edohub/). Wartości poniżej częściowo zamaskowano.

Plik sekretów …maui.essentials.preferences.xml ma postać AndroidX EncryptedSharedPreferences. Nazwy kluczy są szyfrogramem deterministycznym AES-SIV (wspólny prefiks Base64 AfnyGfQ…), a wartości szyfrogramem uwierzytelnionym AES-GCM w formacie Tink (prefiks wersji i identyfikatora klucza AUqzru…). Obok nich występują dwie pozycje __androidx_security_crypto_encrypted_prefs_*_keyset__, przechowujące zestawy kluczy opakowane kluczem głównym z Android Keystore. W chwili badania magazyn zawierał dokładnie dwa wpisy danych o długości wartości 105 oraz 47 bajtów.

Zawartość obu wpisów zidentyfikowałem bez odszyfrowywania, na podstawie znanego formatu zapisu. Wartość w EncryptedSharedPreferences jest ramką 4 bajty (typ) + 4 bajty (długość) + N bajtów łańcucha UTF-8, a szyfrowanie Tink AES-GCM dokłada stały narzut 33 bajtów (5 bajtów wersji i identyfikatora klucza, 12 bajtów wektora inicjującego, 16 bajtów znacznika uwierzytelnienia). Po odjęciu narzutu i ramki wpis 47-bajtowy odpowiada łańcuchowi o długości sześciu znaków, a wpis 105-bajtowy łańcuchowi o długości sześćdziesięciu czterech znaków. Sześcioznakowy łańcuch odpowiada numerowi CAN, utrwalonemu po zaznaczeniu opcji „Zapamiętaj numer CAN" i prezentowanemu w interfejsie w postaci zamaskowanej …886, natomiast łańcuch sześćdziesięcioczteroznakowy odpowiada identyfikatorowi urządzenia DEVICE_ID. Wniosek jest zgodny z deterministyczną naturą AES-SIV: ponowny zapis tego samego numeru CAN odtwarza tę samą nazwę klucza przy nowym, losowym szyfrogramie wartości.

Numer CAN jest zatem utrwalany, lecz wyłącznie w postaci zaszyfrowanej; w pliku ustawień jawnych …AppSettings.xml nie występuje. Plik ten zawiera jedynie flagi przepływu, między innymi IS_REGULATIONS_ACCEPTED_KEY, FIRST_RUN, SIGN_DOCUMENT_VIEW i CAN_CHANGE_SHOW_HELP_DIALOG, oraz token powiadomień. Nie stwierdzono w nim numeru CAN, ciągu MRZ ani numeru PESEL w postaci jawnej.

Wymagane jest zdefiniowanie granicy zaufania dla tego mechanizmu ochronnego. Prezentacja zamaskowanego numeru CAN w interfejsie dowodzi, że aplikacja odszyfrowuje wartość na żądanie kluczem głównym z Android Keystore. Na urządzeniu z uprawnieniami root klucz ten pozostaje używalny w kontekście aplikacji, wobec czego zapamiętany CAN jest tam odtwarzalny. Jest to wspólna właściwość każdego szyfrowania danych w spoczynku opartego o Keystore: chroni ono przed wydobyciem danych z samego pliku, na przykład z kopii zapasowej lub skradzionego nośnika, a nie przed żywym, skompromitowanym systemem, w którym klucz jest dostępny. Waga tego ograniczenia jest przy tym niska. CAN to sześciocyfrowy numer nadrukowany na awersie karty, w standardzie ICAO 9303 pełniący rolę słabego sekretu dostępowego do zestawienia kanału PACE; jego znajomość, bez fizycznego kontaktu NFC z kartą i bez kodu PIN, nie umożliwia żadnej operacji.

Deszyfracja literałów (§9.2) pozwoliła odtworzyć rzeczywiste nazwy kluczy magazynu i sklasyfikować je według trwałości. Trwale, na życzenie użytkownika, pamiętane są EDOAPP_CAN oraz EDOAPP_MRZ; obu odpowiadają pozycje menu „zapomnij CAN" i „zapomnij MRZ". Charakter trwały operacyjny mają DEVICE_ID oraz komplet kluczy Authenticatora (EDOAPP_AUTHENTICATOR_ID, _SALT, _PRIVATE_KEY_BASE64, _CERTIFICATE_LIST).

Postępowanie z kodami PIN wymaga sprostowania wcześniejszego uproszczenia. PIN tożsamości (PIN1) oraz PIN podpisu (PIN2) nie są pamiętane na stałe, lecz przechodzą przez zaszyfrowany magazyn przejściowo, na czas operacji, po czym są z niego jawnie usuwane. Kod zawiera dedykowaną procedurę czyszczącą wywoływaną w cyklu życia widoku, która kasuje PIN1, PIN2 oraz CERT_ID (SecureStorage.Remove), a wylogowanie wykonuje pełne SecureStorage.RemoveAll. Zapis PIN1 realizowany jest w warstwie Droid (SecureStorage.SetAsync), a wartość podlega następnie opisanemu skasowaniu. Jest to poprawna higiena: sekret uwierzytelniający nie pozostaje w spoczynku po zakończeniu operacji.

W środowisku WSA, pozbawionym czytnika NFC, nie doszło do rejestracji Authenticatora ani do operacji na karcie, wobec czego zaobserwowany magazyn zawierał wyłącznie zapamiętany CAN oraz DEVICE_ID, bez klucza prywatnego Authenticatora, sekretu TOTP i bez pozostałości kodów PIN, co jest zgodne z powyższą procedurą czyszczącą.

Katalogi databases/ oraz files/ zawierają wyłącznie komponenty Google, Firebase i Sentry. Nie odnaleziono bazy dokumentu, danych karty ani wizerunku twarzy. Empirycznie potwierdza to model bez trwałego kontenera tożsamości (§19.1.1) oraz brak zapisu dokumentów na nośnik (§19.3.5). Pełny zestaw sekretów operacyjnych można zweryfikować dopiero na urządzeniu z rzeczywistym odczytem karty.

19.2. Transport i piny certyfikatów

Manifest wskazuje network_security_config.xml. Dla edoapp.pl zapisano 11 pinów SHA-256, dla sigillum.pl kolejnych 6; dla obu domen i subdomen wyłączono ruch jawny. Zaufanie ograniczono do magazynu systemowego, bez magazynu certyfikatów użytkownika. W pliku nie ma dat ważności pinów.

Warstwa .NET nie podmienia tego transportu dowolnym klientem. IHttpMessageHandlerProvider zwraca Xamarin.Android.Net.AndroidMessageHandler, a kod tworzy z niego HttpClient. W kodzie własnym aplikacji nie znalazłem callbacku walidacji certyfikatu ani ścieżki typu „zaakceptuj każdy certyfikat”. To spina konfigurację Androida z faktycznym klientem HTTP; wcześniejszy test regulaminu dodatkowo potwierdził działające połączenie HTTPS.

flowchart LR A[kod usługi eDO] --> B[HttpClient] B --> C[Xamarin.Android.Net.AndroidMessageHandler] C --> D[Android network security config] D --> E[edoapp.pl: HTTPS + 11 pinów] D --> F[sigillum.pl: HTTPS + 6 pinów]

Konfiguracja bazowa dopuszcza ruch cleartext dla pozostałych domen, w zasobach zidentyfikowano jednak wyłącznie archiwalny odnośnik ODR do http://ec.europa.eu/consumers/odr. Brak przesłanek wskazujących na transmisję danych wrażliwych otwartym kanałem HTTP.

19.3. Granice procesu Android

Wydanie produkcyjne ma android:debuggable="false" oraz android:allowBackup="false". Dostawca treści Maui deklaruje szeroki zestaw ścieżek plikowych obejmujący pamięć zewnętrzną oraz pamięć podręczną, jednak sam provider pl.pwpw.edohub.fileProvider ma atrybut exported="false". Dostęp z zewnętrznych procesów jest zablokowany, a pliki są udostępniane wyłącznie przez jawne nadanie uprawnień do URI.

Aplikacja udostępnia funkcję pobierania i przekazywania dokumentów. Weryfikacja zakresu uprawnień URI oraz czasu ich ważności wymaga testu z kontrolowaną aplikacją odbiorczą. Sama definicja ścieżek przy braku eksportu komponentu nie stanowi podatności.

Brak literału FLAG_SECURE w assembly biznesowych wynika z nakładania flagi w bazowej aktywności Androida. Weryfikacja dynamiczna na telefonie Xiaomi 13T Pro z systemem Android 16 potwierdziła obecność flagi SECURE w dumpsys window na wszystkich ekranach: CardActivityView, CanEntryView, AcceptSignDataView, SignDocumentView, RegulationsView oraz StartView. Próbny zrzut ekranu wykonany podczas procedury podpisu wygenerował czarny obraz, co potwierdza skuteczne działanie blokady przechwytywania widoku.

Eksportowana jest tylko jedna własna aktywność: SplashView. Manifest przypisuje jej cztery wejścia: start z ikony, edohub://application, edoapp://application i ACTION_SEND dla application/pdf. To jest zamierzona powierzchnia integracji z innymi aplikacjami.

Ścieżkę wejściową prześledziłem dalej niż manifest. IntentExtension.ToStartupAction bierze URI z intencji, otwiera je przez ContentResolver.OpenInputStream, kopiuje do MemoryStream, a następnie tworzy SignFileAction z nazwą i tablicą bajtów. Gdy odczyt się nie uda, adapter przekazuje pustą wartość zamiast kończyć proces. Ten adapter nie odczytuje Intent.Type i nie parsuje zawartości PDF; jest tylko mostem z Androida do kodu aplikacji.

Właściwa kontrola pojawia się w SignDocumentViewModel. Przed przejściem do dalszego kroku obowiązuje limit 0x500000, czyli 5 MiB. Następnie osobny walidator otrzymuje całą tablicę bajtów, odrzuca null, dane puste i dokument krótszy niż 290 B, a błędy walidacji zamienia na komunikat w interfejsie. To jest realna kontrola wejścia, choć sama obecność walidatora nie jest jeszcze dowodem odporności na uszkodzony lub celowo zbudowany PDF. Sprawdzę ją osobno dokumentami granicznymi i mutacjami struktury.

flowchart LR A[Inna aplikacja
ACTION_SEND / URI] --> B[SplashView] B --> C[ContentResolver
OpenInputStream] C --> D[SignFileAction
nazwa + bajty] D --> E[SignDocumentViewModel] E --> F{limit 5 MiB
i walidator} F -->|poprawny| G[przepływ podpisu] F -->|odrzucony| H[komunikat UI]

Te same wejścia URI prowadzą do dwóch odrębnych akcji startowych: parametr z URI może utworzyć CodeAction, a osobny parametr operacji jest najpierw parsowany jako Guid i dopiero wtedy tworzy NewOperationAction. Dynamicznie podam niepoprawne URI, pusty ClipData, wielokrotny ACTION_SEND i PDF o błędnej strukturze. Celem jest sprawdzenie walidacji całego łańcucha, nie tylko wywołanie ekranu.

Pozostałe eksportowane elementy nie są własną ścieżką biznesową eDO: odbiornik Firebase wymaga com.google.android.c2dm.permission.SEND, a androidx.profileinstaller.ProfileInstallReceiver wymaga android.permission.DUMP. Niezabezpieczony deklaracją permission odbiornik AppsFlyer przyjmuje tylko com.android.vending.INSTALL_REFERRER.

Własny AppsFlyerConversionDelegate odbiera dane konwersji i atrybucji, przechodzi po parach klucz–wartość i składa tekst diagnostyczny. W przejrzanej implementacji wynik nie jest zapisywany, przekazywany do logiki eDO ani używany do nawigacji. Ogranicza to wpływ spreparowanego referrera na aplikację, nie wyklucza jednak telemetrii instalacyjnej prowadzonej wewnętrznie przez pakiet SDK AppsFlyer. Zagadnienie to rozstrzygnie rejestracja ruchu sieciowego podczas pierwszego uruchomienia.

19.3.1. Sygnały integralności środowiska

IRootDetectionService nie sprowadza się do jednego testu su. Metoda IsRootedOrDangerous() łączy wynik rozpoznania roota, listy niepożądanych pakietów, wykrywania emulatora, flagi debugowalności i podłączonego debugera. Ten ostatni sygnał pochodzi bezpośrednio z Android.OS.Debug.IsDebuggerConnected. Są to lokalne wskaźniki stanu urządzenia.

Klasa IRootDetectionService nie wykonuje kryptograficznej atestacji stanu systemu. Weryfikacja ewentualnego odrzucania żądań przez serwer na podstawie parametrów nagłówka wymaga analizy ruchu sieciowego.

19.3.2. Pole PIN i PUK nie wymusza cyfr na klawiaturze sprzętowej

To ustalenie wyszło z obserwacji wykonaniowej, którą potem potwierdziłem w kodzie. Na ekranie PIN-u, mimo własnej ekranowej klawiatury numerycznej PinKeyboard, dało się wpisać znaki niecyfrowe z klawiatury sprzętowej: zamiast sześciu cyfr wpisałem słowo WESMAR. Na WSA jest to klawiatura hosta; na fizycznym Androidzie ten sam efekt da klawiatura USB albo Bluetooth.

Mechanizm jest znany: android:inputType="number" ogranicza jedynie miękką klawiaturę ekranową. Bez jawnego DigitsKeyListener albo InputFilter przepuszczającego tylko cyfry, klawiatura sprzętowa nie jest filtrowana. Kontrola kodu to potwierdza: w warstwie widoku PinView oraz PinKeyboard pole posiada wyłącznie filtr długości LengthFilter. W kodzie nie występuje DigitsKeyListener, numeryczny inputType ani filtr cyfr. Ograniczenie do cyfr wynika wyłącznie z układu graficznego klawiatury ekranowej, a nie z ograniczenia nałożonego na pole tekstowe. Ten sam wzorzec obejmuje ekrany zmiany PIN-u oraz PUK.

Wpływ na bezpieczeństwo: zjawisko to nie stanowi obejścia uwierzytelnienia, ponieważ PIN weryfikuje chip karty poleceniem APDU, które odrzuci ciąg znaków alfanumerycznych. Ryzyko dotyczy braku spójności walidacji oraz potencjalnego zużywania limitu prób. Jeżeli niepoprawny ciąg zostanie przekazany bezpośrednio do karty, każda próba dekrementuje licznik PIN, a w przypadku kodu PUK grozi trwałym zablokowaniem dowodu osobistego. Kwestię tego, czy PinValidatorBase.IsValid filtruje znaki niecyfrowe przed transmisją APDU, rozstrzygnie test z monitorowaniem licznika fizycznej karty. Ustalenie oznaczono jako EDO-Q-02.

19.3.3. Podpisywanie PDF: płytka walidacja klienta i pytanie WYSIWYS

Procedura składania podpisu kwalifikowanego została poddana deasemblacji oraz weryfikacji wykonawczej.

Po pierwsze, aplikacja kliencka nie zawiera biblioteki do parsowania plików PDF, takich jak PdfBox, iText czy PdfSharp. Walidacja w SignDocumentViewModel sprawdza jedynie nagłówek %PDF, rozmiar w granicach od 290 bajtów do 5 MiB oraz oblicza skrót SHA-256. Klient nie posiada powierzchni ataku parsera PDF, eliminując podatności typu XXE lub bomby dekompresyjne. Sam format PAdES generowany jest po stronie serwera Sigillum.

Po drugie, w kodzie aplikacji brak komponentu renderującego treść PDF. Widok StampPlacementView powiązany ze StampPlacementViewModel służy wyłącznie do określenia położenia pieczęci graficznej na siatce pól. W teście wykonawczym ekran ten wyświetlał jedynie schemat rozmieszczenia bez podglądu tekstu dokumentu.

Obecnie nie posiadam aktywnego certyfikatu kwalifikowanego; z podpisu mSzafir korzystałem wcześniej jako prezes Atlogika Sp. z o.o., dlatego test wykonawczy objął procedurę do etapu pozycjonowania pieczęci. Kluczowy fakt wynika bezpośrednio z kodu: aplikacja nie wyświetla podpisywanej treści. Przekazanie pliku do zewnętrznej przeglądarki przez akcję ACTION_VIEW jest opcjonalne i realizowane poza procesem eDO App. Narusza to zasadę WYSIWYS, czyli What You See Is What You Sign. Użytkownik autoryzuje operację kryptograficzną na danych binarnych, których integralności wizualnej klient nie wymusza.

Dochodzi do tego subtelniejsza warstwa, spójna z powyższym. Dokument i bajty podpisywane odczytywane są niezależnie: IntentExtension.ToStartupAction czyta URI przez ContentResolver.OpenInputStream do MemoryStream, osobno od jakiegokolwiek podglądu. Dla identyfikatora content:// przekazanego przez obcą aplikację zewnętrzny provider może zwrócić inną treść podczas podglądu, a inną w chwili podpisu, co otwiera drogę do podatności typu TOCTOU. Weryfikacja tego scenariusza wymaga osobnego testu z kontrolowanym dostawcą treści. Niezależnie jednak od podatności na wyścig, brak wymuszonego, czytelnego podglądu dokumentu przed złożeniem podpisu pozostaje najpoważniejszą wadą całej tej procedury.

Sprawdzenie 4 bajtów nagłówka %PDF i limitu rozmiaru to kontrola czysto formatowa. Aplikacja nie weryfikuje struktury dokumentu ani nie wyświetla jego stron. Użytkownik autoryzuje e-dowodem niezweryfikowane dane binarne, co przy braku podglądu pozwala na podrzucenie do podpisu zmanipulowanego pliku. Stanowi to podstawę do oznaczenia podatności jako EDO-A-02.

19.3.4. Walidacja wejścia QR i deep-link: potwierdzona dynamicznie

Jedyna eksportowana aktywność SplashView przyjmuje deep-linki edohub://application i edoapp://application; kod z QR mapuje się wewnętrznie na ten sam URI i przez IntentExtension.ToStartupAction na CodeAction albo NewOperationAction (parametry code, operation). To jest właściwa powierzchnia wejścia, więc sprawdziłem ją wykonaniowo.

Test był kontrolowany: wygenerowałem kod QR z treścią spoza formatu operacji (zwykłym tekstem) i zeskanowałem go w aplikacji. Reakcja jest wzorcowa: eDO odrzuciło kod komunikatem, że aplikacja odczytuje wyłącznie kody ze stron partnerów. dumpsys potwierdza, że proces nie padł i wrócił na StartView, bez crasha i bez przejścia w nieoczekiwany stan. Skaner QR waliduje więc treść względem oczekiwanego źródła, a nie przetwarza bezkrytycznie dowolnego ciągu.

Odfiltrowanie nieprawidłowych danych QR stanowi skuteczną warstwę ochronną klienta. W celu weryfikacji odporności na spreparowane parametry przeprowadzono test dynamiczny przez adb. Przekazano zestaw spreparowanych intencji: parametr operation ze zniekształconym formatem Guid, poprawny składniowo, niewystępujący w systemie Guid zerowy, parametr code z ciągiem wstrzyknięcia skryptu oraz pusty URI edoapp://application przy zimnym starcie procesu. W każdym przypadku parser ToStartupAction odrzucił wejście bez błędu krytycznego, a aplikacja powróciła do widoku StartView. Obsługa wejść zewnętrznych po stronie klienta zachowuje odporność na zniekształcone dane.

19.3.5. Brak zapisu dokumentów do pamięci współdzielonej

Zweryfikowałem odrębnie, czy aplikacja pozostawia pobrane lub podpisane dokumenty w lokalizacji dostępnej dla innych aplikacji, ponieważ była to realna słabość analizowana w przypadku mObywatela. W eDO nie stwierdzono ku temu podstaw. W kodzie nie występują typy odpowiedzialne za zapis ani udostępnianie plików na dysku, nie odnaleziono również literałów ścieżek do katalogu Downloads, pamięci zewnętrznej ani plików *.pdf. Liczne wystąpienia terminu „Download" odnoszą się do pobierania sieciowego, nie zaś do zapisu na nośniku. Dokument przeznaczony do podpisu jest wczytywany do pamięci (MemoryStream) i w niej pozostaje. Jest to ustalenie korzystne, spójne z modelem bez trwałego kontenera z §19.1.1: eDO nie utrwala treści dokumentów w pamięci urządzenia poza czasem operacji.

19.4. Diagnostyka i telemetria

W APK są Sentry, Firebase i AppsFlyer. Manifest ma io.sentry.auto-init=false, ale Setup inicjalizuje Sentry ręcznie, z własnym DSN ukrytym przez obfuskację. tracesSampleRate i profilesSampleRate ustawiono na 1.0. Kod wywołuje SentrySdk.CaptureException w blokach obsługi wyjątków, m.in. przy inicjalizacji aparatu oraz podczas odczytu i przetwarzania plików PDF. W warstwie Androida zidentyfikowałem ponadto dwa wywołania Android.Util.Log.Error dotyczące doboru rozdzielczości kamery, sparowane z wysyłką raportu do Sentry. Z kolei kod biznesowy implementuje interfejs IAnalyticsService z metodami TrackEvent i SendLog; zdarzenia telemetryczne rejestrowane są m.in. przy przejściu użytkownika do ekranu wprowadzania CAN.

W kodzie brak jawnego wywołania setSendDefaultPii(true), callbacku przypisującego tożsamość użytkownika oraz funkcji dołączania zrzutu ekranu. Nie wyklucza to jednak obecności danych wrażliwych w treści samych komunikatów wyjątków, a pełne próbkowanie śladów i profili (1.0) zwiększa ryzyko wycieku. Faktyczny zakres telemetrii zweryfikuje analiza ruchu sieciowego i bufora Sentry podczas wywołania kontrolowanego błędu testowego.

19.5. Biometria: FaceTec, przepływ danych twarzy i retencja

Weryfikacja tożsamości wykorzystuje silnik FaceTec, certyfikowane, komercyjne rozwiązanie do detekcji żywotności (Liveness 3D) oraz biometrycznego dopasowania twarzy. Kontrakty pozostają jawne w metadanych IL: interfejs IFaceTecClientService udostępnia operacje GetSessionToken, Enrollment3d, MatchFacePhoto, VerifyIdWithEnrollmentAndMatch oraz ConvertJP2ToJPEG, natomiast IFaceTecPlatformService odpowiada za sterowanie sesją. W warstwie prezentacji model FaceBiometryRecordingViewModel zarządza akwizycją klatek obrazu (OnNewFrame, StartRecording, HandleVideoSentMessage).

Obecność metody ConvertJP2ToJPEG jednoznacznie wyznacza schemat weryfikacji: skan trójwymiarowy z kamery jest zestawiany z wizerunkiem referencyjnym odczytanym z chipu dowodu osobistego, gdzie zgodnie ze specyfikacją ICAO 9303 zdjęcie twarzy (grupa DG2) kodowane jest w formacie JPEG 2000. Schemat ten zakłada zdalną autoryzację tożsamości, co wiąże się z transmisją danych biometrycznych poza urządzenie.

Zastosowanie renomowanego silnika biometrycznego, implementacja procedur czyszczenia buforów pamięci (wipe) oraz brak trwałego zapisu wizerunku na nośniku flash stanowią mocne punkty tej architektury. Kwestią wymagającą weryfikacji w teście dynamicznym pozostaje docelowy endpoint sieciowy, struktura przesyłanych pakietów oraz retencja danych po stronie serwera.

19.6. NFC i kryptografia karty

19.6.1. Zagrożenie skimmingiem i rola numeru CAN

Bezkontaktowy interfejs radiowy (ISO/IEC 14443) umożliwia transmisję danych drogą elektromagnetyczną bez bezpośredniego styku galwanicznego. W architekturze systemów RFID cecha ta rodzi specyficzne ryzyko: komunikacja może zostać zainicjowana przez czytnik przez materiał kieszeni, torebki czy portfela bez świadomości i woli posiadacza dokumentu. Zagadnienie to stanowiło realną podatność wczesnych generacji paszportów biometrycznych, w których brak wstępnego uwierzytelnienia pozwalał na pasywne odpytanie chipu z odległości i pozyskanie danych tożsamości (skimming).

W strukturze e-dowodu ochronę przed niekontrolowaną transmisją realizuje sześciocyfrowy numer CAN (Card Access Number) nadrukowany na awersie karty. Pełni on trzy kluczowe funkcje architektoniczne:

  1. Optyczny dowód fizycznego dostępu i zgody (line-of-sight consent token). Odczyt logiczny danych wymaga uprzedniego odczytania numeru widocznego na dokumencie. Wiąże to nawiązanie sesji radiowej z bezpośrednią kontrolą wzrokową i fizycznym dostępem posiadacza do dokumentu. Zdalny odczyt przez kieszeń lub portfel w przestrzeni publicznej zostaje wykluczony, ponieważ bez znajomości CAN mikrokontroler karty odrzuca żądanie nawiązania sesji.
  2. Materiał wejściowy dla protokołu PACE. Sześciocyfrowy ciąg stanowi ziarno (seed) dla funkcji wyprowadzania kluczy (KDF) w protokole PACE (Password Authenticated Connection Establishment, ICAO Doc 9303). W oparciu o ten parametr aplikacja i karta uzgadniają klucze sesyjne do szyfrowania i uwierzytelniania kanału (secure messaging). Chociaż przestrzeń 6 cyfr ($10^6$ kombinacji) jest niska w ujęciu czysto obliczeniowym, mikrokontroler karty egzekwuje sprzętowy licznik błędnych prób. Po przekroczeniu określonego limitu nieudanych autoryzacji procesor blokuje dalsze próby do momentu podania procedury odblokowującej, co uniemożliwia skuteczny atak siłowy (brute-force) na interfejsie radiowym.
  3. Ergonomiczny ekwiwalent strefy MRZ. W paszportach biometrycznych rolę sekretu wejściowego do procedur BAC lub PACE pełni odczyt strefy maszynowej (Machine Readable Zone, MRZ), co wymaga fizycznego otwarcia dokumentu i optycznego skanowania kodów. W dowodzie osobistym CAN stanowi zminiaturyzowany, numeryczny odpowiednik MRZ, możliwy do szybkiego wprowadzenia ręcznego lub za pomocą mechanizmu OCR kamery.

Granice ochrony i model bezpieczeństwa CAN:
CAN jest celowo uproszczonym sekretem chronionym wyłącznie barierą optyczną, a nie poufnością kryptograficzną. Każdy podmiot mający wgląd w awers dowodu (np. pracownik instytucji weryfikującej tożsamość, obsługa hotelowa czy odbiorca kserokopii) zna numer CAN. Numer ten nie chroni dokumentu przed osobą posiadającą fizyczny dostęp do karty lub jej odwzorowania cyfrowego. Z tego względu architektura bezpieczeństwa e-dowodu rozdziela role na trzy odrębne mechanizmy:

  • CAN: zabezpieczenie warstwy radiowej przed skimmingiem z dystansu (mechanizm zapobiegający nieautoryzowanemu wzbudzeniu interfejsu na odległość),
  • PIN: poufne uwierzytelnienie intencji posiadacza dla operacji wrażliwych (nie jest nadrukowany na karcie i pozostaje niezbędny do złożenia podpisu osobistego lub kwalifikowanego),
  • Chip Authentication: asymetryczna weryfikacja autentyczności mikrokontrolera (potwierdzenie, że fizyczny nośnik nie jest programową repliką lub emulatorem).

19.6.2. Wymiana APDU, protokół PACE i prymitywy kryptograficzne

NFC nie jest wyłącznie wywołaniem gotowego SDK. Interfejs ICardCommunicator ma surowe TransreceiveCommandRaw, wymianę APDU z limitem czasu, restart i anulowanie secure messaging oraz SetupChipAuthentication. Warstwa PACE tworzy własny klucz z sekretu, odszyfrowuje nonce, wylicza wspólny sekret i token uwierzytelnienia. W tym samym przepływie IAuthenticatorService otrzymuje PIN, identyfikator certyfikatu, klucz publiczny serwera, zakodowany sekret TOTP i klucz rejestracyjny.

Implementacja wyprowadzania kluczy jest czytelna mimo obfuskacji: dla KSenc dokleja do sekretu czterobajtowy licznik kończący się 01, dla KSmac analogiczny licznik z 02, po czym liczy skrót. Dostępne profile obejmują AES-128/192/256 i 3DES; dla chip authentication są DH i ECDH z 3DES-CBC-CBC lub AES-CBC-CMAC. Używana jest biblioteka Bouncy Castle.

flowchart LR A[CAN / MRZ / PIN] --> B[PACE: sekret i nonce] B --> C[KSenc oraz KSmac] C --> D[APDU i secure messaging] D --> E[chip authentication] E --> F[dane i certyfikaty karty] F --> G[operacja uwierzytelniająca]

W funkcji skrótu znajduje się także gałąź SHA-1. Fakt ten nie stanowi wprost luki bezpieczeństwa: standardy kartowe definiują profile wstecznej zgodności, a analiza statyczna kodu nie przesądza, czy procesor dowodu osobistego i jego OID wybierają tę ścieżkę. Test z fizycznym dokumentem zweryfikuje negocjowany algorytm, OID i długość kluczy.

Weryfikacja komponentów potwierdza zgodność implementacji ze standardami. W bibliotece Core.dll obecne są klasy Sha1Digest, Sha256Digest, AesEngine, DesEdeEngine (3DES), CMac z dopełnieniem ISO9797 oraz struktury PaceInfo, ChipAuthenticationInfo i SecurityInfos. Odpowiada to specyfikacji protokołów PACE oraz Chip Authentication według ICAO 9303, zrealizowanej w oparciu o bibliotekę Bouncy Castle. Zestaw szyfrów jest negocjowany na podstawie parametrów karty (SecurityInfos oraz identyfikatory OID), a nie narzucany statycznie przez aplikację. Gałąź SHA-1 oraz 3DES uaktywnia się wyłącznie w przypadku zadeklarowania jej przez chip dokumentu, co wynika z wymogów wstecznej zgodności ze specyfikacją ICAO, a nie z implementacji eDO App. Stanowi to różnicę względem mObywatela, w którym zastosowano własny kontener CBC ze stałą solą bez mechanizmów uwierzytelnienia; eDO App wykorzystuje standardowy protokół oraz audytowaną bibliotekę Bouncy Castle. Wybór algorytmu dla polskiego dowodu osobistego zostanie zweryfikowany podczas rejestracji sesji NFC w części dynamicznej.

19.7. Protokół Authenticatora: autoryzacja podpisu kwalifikowanego

Kluczowym modułem eDO App jest mechanizm Authenticatora, odpowiedzialny za autoryzację operacji podpisu kwalifikowanego. Protokół odtwarza się bezpośrednio z metadanych i interfejsu IAuthenticatorClient, struktur DTO oraz maszyn stanu w bibliotece Core.dll.

Procedura obejmuje cztery etapy:

  1. Rejestracja (SetupAuthenticator). Klient generuje parę kluczy asymetrycznych i przekazuje klucz publiczny w żądaniu SetupAuthenticatorRequest{PublicKey, PublicKeyType, AuthenticatorId, OperationId}. W odpowiedzi serwer zwraca SetupAuthenticatorResponse{TotpSecret, PublicKey, CertificateId}, provisionując sekret TOTP oraz identyfikator certyfikatu. Wygenerowany klucz prywatny oraz sekret TOTP trafiają do lokalnego magazynu SecureStorage.
  2. Pobranie operacji (LoadOperation). Odpowiedź serwera zawiera parametry Counter, CreatedAt oraz ExpiresAt. Numeryczna sekwencja operacji i ograniczenie czasu ważności zapewniają ochronę przed atakami typu replay oraz przetwarzaniem przeterminowanych żądań.
  3. Autoryzacja (ConfirmOperation{Totp, OperationId}). Potwierdzenie operacji bazuje na jednorazowym kodzie TOTP wyliczanym z lokalnego sekretu i powiązanym z identyfikatorem zadania.
  4. Złożenie podpisu. Obiekt SignaturePayloadDto przekazuje parametry Challenge, Timestamp, Data i Certificate. Z kolei struktura SignatureDataDto zawiera skróty grup danych dokumentu (Sod), flagi Authentication i OperationSecurity oraz numer seryjny karty (CardSerialNumber). Procedura implementuje mechanizm challenge-response z kryptograficznym wiązaniem z fizycznym chipem dowodu osobistego.
flowchart TD R[Rejestracja: para kluczy do serwera] --> T[Odpowiedz: TotpSecret + CertificateId] T --> L[LoadOperation: Counter, ExpiresAt] L --> C[ConfirmOperation: TOTP + OperationId] C --> S[Podpis: Challenge + Timestamp + Data] S --> B[Wiazanie karty: SOD + numer seryjny]

Wnioski z analizy architektury:
Sam protokół zaprojektowano prawidłowo. Wykorzystuje standardowe mechanizmy challenge-response, TOTP, sekwencyjne liczniki operacji, okna czasowe ważności oraz powiązanie podpisu z danymi fizycznej karty.

Identyfikowane ryzyko architektoniczne:
Prywatny klucz Authenticatora oraz sekret TOTP są serializowane do magazynu SecureStorage jako ciągi Base64. Mimo szyfrowania bazy kluczem z Android Keystore, sekrety te po odszyfrowaniu mają postać jawną w pamięci procesu, a nie nieeksportowalnych uchwytów sprzętowych chronionych w TEE. Ekstrakcja danych z odblokowanego lub zrootowanego urządzenia umożliwia odtworzenie generatora autoryzacji poza fizycznym telefonem.

Weryfikacja tego, czy serwer bezwzględnie egzekwuje parametry wyzwania, sekwencję licznika i limity czasu, stanowi przedmiot badania dynamicznego w kolejnym etapie prac.

20. Dygresja: brak czytnika NFC w WSA i przekaźnik APDU

WSA nie obsługuje kontrolera NFC, co uniemożliwia bezpośredni odczyt dowodu osobistego w maszynie wirtualnej. Podpis i odczyt zrealizowano na fizycznym telefonie Xiaomi 13T Pro. Poniższa architektura przedstawia koncepcję programowego przekaźnika ramek APDU przez sieć lokalną, pozwalającego na odczyt karty w środowisku WSA przy użyciu telefonu jako zdalnego czytnika.

Rozwiązaniem technicznym jest przekaźnik ramek APDU. Aplikacja eDO App komunikuje się z chipem dowodu poleceniami IsoDep.transceive, a protokoły PACE oraz bezpieczne kanały wymiany danych działają bezpośrednio między aplikacją a procesorem karty. Warstwa transmisyjna przesyła zaszyfrowane ramki bez konieczności ich modyfikacji.

Koncepcja wdrożenia:

  1. Moduł LSPosed w WSA. Przechwycenie wywołań NfcAdapter.getDefaultAdapter oraz isEnabled w celu emulacji dostępności kontrolera NFC. Metoda IsoDep.transceive w warstwie Java jest przechwytywana przez LSPosed pomimo wywołania ze środowiska Xamarin.
  2. Wstrzyknięcie tagu. Sfabrykowanie zdarzenia wykrycia karty w enableReaderMode oraz podstawienie instancji IsoDep przy użyciu mechanizmów refleksji.
  3. Warstwa transportowa. Przekazywanie przechwyconych ramek APDU przez protokół TCP z wykorzystaniem przekierowania adb forward.
  4. Demon na telefonie fizycznym. Aplikacja z aktywnym enableReaderMode wykonująca polecenie realIsoDep.transceive na przyłożonym dowodzie i odsyłająca odpowiedź do gniazda sieciowego w WSA. Wymaga obsługi rozszerzonych ramek APDU zadeklarowanych przez ExtendedLengthAPDUSupportedKey oraz zachowania limitów czasowych transmisji.
flowchart LR subgraph WSA["WSA (bez NFC)"] EDO[eDO App] --> ISO[IsoDep.transceive] ISO --> HOOK[Hook LSPosed] end HOOK -->|APDU po TCP / adb forward| DEMON subgraph XIAOMI["Xiaomi 13T Pro (czytnik)"] DEMON[Demon enableReaderMode] --> CARD[Fizyczny dowód] end CARD -->|odpowiedz APDU| DEMON DEMON -->|po TCP| HOOK

Architektura ta bazuje na mechanizmie przekaźnika APDU tożsamym z technikami typu relay attack na interfejsy zbliżeniowe. Z punktu widzenia inżynierskiego brak kontrolera NFC w WSA stanowi ograniczenie sprzętowe, które można rozwiązać programowo.

21. Reprodukowalność, dystrybucja i licencja

Procedurę modyfikacji zautomatyzowano skryptem weryfikującym sumę kontrolną SHA-256 oryginalnego pakietu APK oraz instalującym zmodyfikowane pliki poleceniem adb install -r z zachowaniem danych aplikacji.

Kod źródłowy mObywatela opublikowano na licencji MIT, co zezwalało na redystrybucję. Aplikacja eDO App stanowi oprogramowanie własnościowe PWPW S.A. Modyfikacja lokalnej kopii w celu zapewnienia interoperacyjności mieści się w granicach dozwolonego użytku, natomiast dystrybucja zmodyfikowanych binariów PWPW naruszałaby prawa autorskie producenta. Z tego względu repozytorium nie zawiera plików binarnych.

Paczka zawiera wyłącznie metodykę i skrypty weryfikacyjne: pliki automatyzujące, definicje profili oraz payload-manifest.json z sumami kontrolnymi SHA-256 wymaganych komponentów. Skrypt patch-eDO-App-1.7.1-WSA.ps1 sprawdza obecność podkatalogu payload/ zdefiniowanego w manifeście; skoro ze względów licencyjnych celowo go tam nie ma, skrypt zatrzyma się z błędem braku plików wsadowych. Dzięki temu repozytorium stanowi ścisły ślad dowodowy i weryfikator integralności, a nie kanał redystrybucji chronionego kodu producenta.

Potwierdzeniem działania procedury uruchomieniowej jest materiał wideo na YouTube rejestrujący start aplikacji na WSA z aktywnym MagiskSU, przejście przez akceptację regulaminu oraz załadowanie widoku StartView.

22. Ocena architektury bezpieczeństwa klienta

Ocena dotyczy warstwy aplikacji klienckiej Android (pl.pwpw.edohub) i opiera się na analizie statycznej trzech głównych assembly .NET, deasemblacji zasobów oraz testach dynamicznych wykonanych na telefonie bez uprawnień root. Nie stanowi audytu infrastruktury serwerowej PWPW ani fizycznego procesora karty kryptograficznej. Wagi poszczególnych osi odzwierciedlają model zagrożeń aplikacji tożsamościowej: decydujące znaczenie ma ochrona danych w spoczynku (25%) oraz integralność procedury podpisu kwalifikowanego (20%), podczas gdy podatność kodu na inżynierię wsteczną traktowana jest jako warstwa pomocnicza (5%). Weryfikacja ruchu telemetrii, atestacji klucza w Keystore, integralności po stronie serwera oraz odporności pinningu na inspekcję TLS wymaga środowiska z rootem i stanowi przedmiot kolejnego etapu badań.

Oś architektury Waga Ocena Uzasadnienie inżynierskie
Dane w spoczynku 25% 8,5/10 brak lokalnego kontenera tożsamości, dane odczytywane z karty, sekrety w Keystore ze wsparciem TEE; potwierdzenie dynamiczne na WSA (poprawne AES-SIV/AES-GCM); kody PIN przejściowe i jawnie usuwane (Remove/RemoveAll); zastrzeżenia: trwale pamiętane CAN i MRZ oraz eksportowalny klucz Authenticatora w Base64
Kryptografia NFC/PACE 15% 8,5/10 standardowe protokoły PACE i Chip Authentication wg ICAO oparte o Bouncy Castle, zestaw szyfrów negocjowany z karty
Transport i pinning 15% 8,0/10 deklaracja 11 i 6 pinów w konfiguracji sieciowej, powiązanie z AndroidMessageHandler, brak akceptacji niezweryfikowanych certyfikatów w kodzie klienta
Integralność podpisu (WYSIWYS) 20% 4,5/10 brak wymuszonego podglądu dokumentu w kroku podpisu kwalifikowanego, brak parsera i renderera PDF w kliencie, podatność EDO-A-02
Ochrona ekranu i granice procesu 10% 8,0/10 flaga FLAG_SECURE aktywna na wszystkich ekranach, brak flagi debuggowania, zamknięty fileProvider
Walidacja wejścia (QR, deep-link, PIN) 10% 6,5/10 filtry wejść QR i deep-link odporne na niepoprawne dane; brak filtrowania klawiatury sprzętowej w polach PIN i PUK, podatność EDO-Q-02
Odporność na reverse engineering 5% 7,0/10 pełna konfiguracja Dotfuscatora: zmiana nazw, spłaszczenie przepływu i szyfrowanie literałów; skuteczność ograniczona samoopisującym się strumieniem klucza (odwracalnym offline) oraz zachowanymi metadanymi .NET, podatność EDO-P-01

Wynik ważony: ≈ 7,3/10: ocena dobra (górny przedział skali).

Podsumowanie wyników. Architektura eDO App wykazuje wyższy poziom bezpieczeństwa danych w spoczynku niż mObywatel. Brak lokalnego kontenera danych tożsamościowych eliminuje ryzyko ich ekstrakcji offline z kopii zapasowej. Dane uwierzytelniające chroni Android Keystore powiązany z TEE, a operacje kryptograficzne bazują na standardzie ICAO 9303 oraz zweryfikowanej bibliotece Bouncy Castle. Główną wadą rozwiązania pozostaje brak wymuszonego podglądu dokumentu przed złożeniem podpisu kwalifikowanego (fakt weryfikowalny bezpośrednio w kodzie widoku i view-modelu), uzupełniony o uchybienia w walidacji wejścia klawiatury sprzętowej oraz o obfuskację, która mimo szyfrowania literałów pozostaje odwracalna offline.

Porównanie z architekturą mObywatela. Przyjęty przez PWPW model bezpieczeństwa cechuje większy rygor inżynierski. Oparcie tożsamości na fizycznej karcie kryptograficznej, brak programowych magazynów tożsamości podatnych na ekstrakcję offline oraz implementacja standardowych protokołów kryptograficznych stanowią rozwiązania dojrzalsze od pierwotnej architektury mObywatela.

Nota autorska. Piszę to jako inżynier, który od lat dziewięćdziesiątych rozkłada oprogramowanie na części pierwsze i buduje własne, ulepszone wersje, w tym natywnego, desktopowego klienta mObywatela. Moje publiczne projekty (github.com/wesmar) sięgają warstw HVCI, EFI, DSE i PPL, dlatego klienta operującego wyłącznie w przestrzeni użytkownika, przez PC/SC i publiczne biblioteki kryptograficzne, traktuję jako zadanie inżyniersko proste. Przy dzisiejszym warsztacie i narzędziach realistycznie szacuję około trzech tygodni na działający szkielet i kolejne trzy na dopracowanie całości klienta do wersji w pełni dojrzałej i wydajnej. Znając strukturę klienta, zestaw protokołów i format magazynu, stawiam tezę praktyczną: warstwę odczytu e-dowodu da się odtworzyć jako natywną aplikację desktopową dla Windows w nowoczesnym C++23 na WinAPI. Odczyt tożsamości opiera się na otwartym standardzie ICAO 9303 (PACE, Chip Authentication, Passive Authentication, DG1/DG2/SOD), dostępnym przez interfejs PC/SC (WinSCard) i publiczne biblioteki kryptograficzne, bez żadnego zastrzeżonego komponentu PWPW. Wiele laptopów ma dziś czytnik NFC, a tam gdzie go brak, wystarcza tani czytnik PC/SC na USB; niezależny klient rozwiązałby realny problem osób, które eDO App na telefonie mieć nie mogą lub nie chcą.

Zachowanie transparentności systemowej wymaga dokładnego zdefiniowania granic między obiema warstwami, zamiast jedynie deklaratywnego ujęcia problemu. Warstwa odczytu charakteryzuje się bezpośrednią odtwarzalnością. Warstwa usługowa (internetowy Authenticator eDO oraz komunikacja z Sigillum) jest odtwarzalna co do zasady, lecz wyższym kosztem: wymaga przechwycenia i odtworzenia protokołu sieciowego (zdjęcie przypinania certyfikatu na własnym urządzeniu, analiza ruchu), ewentualnie sprzętowego podsłuchu wymiany APDU z kartą. Jest to wykonalne metodą logika + ruch + sniffing, a znajomość kodu daje tu istotną przewagę. Nie przesądzam wykonalności końcowej, ponieważ nie zbadałem jeszcze, czy serwery PWPW nie wymuszają atestacji integralności klienta, co stanowi właściwe pytanie etapu dynamicznego. Odrębną barierą, tym razem nie techniczną, lecz prawną, pozostaje status podpisu kwalifikowanego: jego moc wynika z certyfikowanego procesu dostawcy usług zaufania (QTSP) i ważnego certyfikatu w rozumieniu eIDAS, a nie z samego klienta. Taka implementacja byłaby moim autorskim kodem, dystrybuowanym bez binariów PWPW, i pozostawałaby w domenie interoperacyjności, nie zaś obchodzenia zabezpieczeń.

23. Puenta

Etap uruchomieniowy na WSA potwierdził konieczność wyeliminowania niezgodności w pięciu obszarach: native bridge, biblioteka ICU, zachowanie trimmera kodu, obfuskacja klienta oraz błędy translatora Houdini przy obsłudze wektorowych optymalizacji SIMD w bibliotece standardowej .NET 9. Moduł Zygisk działa poprawnie w konfiguracji końcowej.

Przywrócenie prawidłowej semantyki parsera HTTP oraz precyzyjne wyłączenie wadliwych optymalizacji umożliwiło przeprowadzenie audytu bezpieczeństwa klienta: interfejsu NFC, stosowanych algorytmów kryptograficznych, protokołu komunikacji oraz magazynu danych.

Badania dynamiczne wymagające uprawnień root, obejmujące inspekcję ruchu TLS, analizę atestacji kluczy w Keystore oraz weryfikację reakcji serwera na zmodyfikowanego klienta, zostaną zrealizowane w dedykowanym środowisku laboratoryjnym.


Badanie prowadzę na własnym urządzeniu wirtualnym, własnym koncie i własnym dokumencie. Opis dotyczy interoperacyjności i oceny bezpieczeństwa klienta. Nie zawiera danych osobowych ani PIN-u.

Add a comment

human test