Jak wstrzykiwać kod do jądra Linux bez kodu źródłowego i symboli debugowania
Wyobraź sobie taką sytuację: masz gotowy obraz jądra Linux dla urządzenia mobilnego lub wbudowanego boarda ARM64. Nie masz dostępu do kodu źródłowego, informacje debugowania zostały usunięte podczas kompilacji, a ty musisz przechwycić wywołanie systemowe lub dodać własną logikę bezpośrednio na poziomie jądra.
Zwyczajowe podejście w takich przypadkach jest czasochłonne i żmudne. Musisz zreversować binarkę w IDA Pro lub Ghidrze, ręcznie znaleźć offsety funkcji i pisać niestabilne łaty. Programista działający pod nickiem bmax121 postanowił uprościć ten proces i wydał projekt KernelPatch.
Dlaczego KernelPatch jest potrzebny
KernelPatch to framework do łatania i hakowania jądra Linux, gdy jedynym dostępnym materiałem jest obraz jądra bez symboli, taki jak vmlinux lub boot.img.
Gdy jądro jest budowane bez informacji debugowania, tradycyjne narzędzia debugujące, takie jak kprobes, odmawiają współpracy bez dodatkowych obejść. KernelPatch rozwiązuje ten problem samodzielnie. Odzyskuje tablicę symboli bezpośrednio z pliku binarnego jądra i udostępnia wygodne API do modyfikacji kodu wykonywalnego.
Projekt jest przydatny dla reverserów, badaczy bezpieczeństwa Androida oraz programistów oprogramowania niskiego poziomu dla systemów wbudowanych.
Co potrafi ten framework
Autor zbudował w projekcie kilka kluczowych funkcjonalności do pracy z jądrem:
- Wyszukiwanie symboli bez kodu źródłowego. Narzędzie parsuje wewnętrzne struktury
kallsymsw gotowym obrazie. Nie potrzebuje zewnętrznych plikówSystem.mapani kodu źródłowego. Podobne podejście jest wykorzystywane w narzędziuvmlinux-to-elf, z którego autor zaczerpnął część pomysłów. - Dwa tryby wstrzykiwania kodu. Możesz wykonać statyczną łatę obrazu (na przykład nadpisać
boot.imgna dysku) lub załadować kod dynamicznie w czasie działania. - Przechwytywanie funkcji przez inline hook. Dla architektury ARM64 przechwytywanie wywołań bazuje na pracy
android-inline-hook. Framework poprawnie obsługuje instrukcje z adresowaniem relatywnym (b,bl,adrp), aby nie zakłócić wykonywania oryginalnego kodu. - Hooki wywołań systemowych. Zastąpienie adresów w tabeli
sys_call_tablezajmuje zaledwie kilka poleceń. - Wbudowany moduł SU. Dla urządzeń z Androidem projekt zawiera własną implementację superusera.
Jak to działa wewnętrznie
Aby ładować moduły użytkownika (KPM — Kernel Patch Modules) na bieżąco, framework potrzebuje pamięci wewnątrz jądra. W tym celu KernelPatch zawiera kompaktowy alokator TLSF. Pomaga przydzielać strony pamięci wykonywalnej (ROX) bezpośrednio w przestrzeni adresowej jądra.
Podczas wykonywania inline hooka framework odczytuje pierwsze instrukcje funkcji docelowej, tworzy z nich tak zwany trampoline i zapisuje skok do twojego handlera. Gdy twój kod zakończy wykonywanie, sterowanie wraca do oryginalnej funkcji przez trampoline.
Projekt ma jasno określone granice stosowalności, które warto poznać zawczasu:
- Obsługiwana jest tylko architektura ARM64. Nie ma jeszcze wersji dla x86_64 ani RISC-V.
- Zakres wersji jądra — od Linux 3.18 do 6.6.
- Głównym wymaganiem jest, aby jądro było zbudowane z flagą
CONFIG_KALLSYMS=y.
Jeśli producent firmware całkowicie usunął CONFIG_KALLSYMS na etapie kompilacji, KernelPatch nie będzie w stanie automatycznie parsować adresów funkcji. Na szczęście ta flaga jest włączona w większości firmware urządzeń z Androidem i ARM.
Praktyczny przykład użycia
Załóżmy, że chcesz śledzić wszystkie uruchomienia procesów przez wywołanie sys_execve na testowym urządzeniu z Androidem, ale nie chcesz przebudowywać jądra z kodu źródłowego dostarczonego przez producenta.
Wcześniej musiałbyś zbudować pełny moduł ładujący jądra (LKM), zmierzyć się z problemami weryfikacji podpisu cyfrowego i brakiem loadera modułów w jądrze.
Z KernelPatch proces wygląda inaczej:
- Piszesz niewielki kod w C z logiką przechwytywania.
- Kompilujesz go do formatu KPM.
- Podajesz narzędziu KernelPatch swój
boot.imgi skompilowany moduł KPM. - Otrzymujesz zmodyfikowany
boot.imgi wgrywasz go przez fastboot.
Po uruchomieniu urządzenia każde wywołanie execve przechodzi najpierw przez twój handler.
Dokumentacja projektu README jest dość zwięzła, a całą szczegółową dokumentację trzeba będzie przestudiować w kodzie źródłowym i plikach w folderze doc/. Mimo to projekt aktywnie się rozwija i zdobył już ponad 14 000 gwiazdek na GitHubie.
A przy okazji, jeśli nie potrzebujesz samego frameworka do eksperymentów, ale raczej gotowego roota dla Androida opartego na nim, autor zaleca przyjrzenie się jego pobocznemu projektowi — APatch. To wyżej poziomowa alternatywa dla Magisk i KernelSU, korzystająca z KernelPatch pod spodem.
Dla badaczy bezpieczeństwa i programistów ARM64 KernelPatch to świetne znalezisko, oszczędzające dziesiątki godzin podczas analizy jąder o zamkniętym kodzie źródłowym.
Powiązane projekty