Dekompilacja aplikacji Android napisanej w .NET MAUI 9 (Xamarin)
.NET MAUI, jako następca Xamarin, umożliwia tworzenie aplikacji wieloplatformowych, w tym na system Android, przy użyciu języka C#. W poprzednich wersjach (do .NET MAUI 8) aplikacje przechowywały zestawy bibliotek DLL w plikach assemblies.blob oraz assemblies.manifest, co ułatwiało ich ekstrakcję i analizę. Istniały już narzędzia umożliwiające dekompilację tych plików, takie jak pyxamstore, pozwalające na łatwe odzyskanie oryginalnych bibliotek. Wraz z premierą .NET MAUI 9 wprowadzono istotną zmianę w sposobie pakowania bibliotek .NET w aplikacjach Androidowych. Zamiast przechowywania DLL w osobnych plikach, wszystkie biblioteki są teraz osadzane w plikach ELF (.so) o nazwie libassemblies.<arch>.blob.so, gdzie <arch> odnosi się do architektury procesora, np. arm64-v8a. Ta zmiana miała na celu poprawę wydajności, zwiększenie bezpieczeństwa oraz lepszą kompatybilność z systemem Android. W tym artykule wyjaśnię, jak wydobyć wspomniane pliki DLL.

Struktura pliku libassemblies.arm64-v8a.blob.so: Co się zmieniło?
W .NET MAUI 9 biblioteki DLL nie są już zapisywane jako oddzielne pliki w katalogu aplikacji Androida. Zamiast tego Microsoft osadził je w plikach ELF - formacie dobrze znanym w systemach uniksowych, w tym w Androidzie. Na pierwszy rzut oka plik libassemblies.arm64-v8a.blob.so
wygląda jak standardowa natywna biblioteka współdzielona (z rozszerzeniem .so), podobna do libmonosgen-2.0.so
. Ale w rzeczywistości kryje w sobie coś więcej - kompletny zestaw bibliotek .NET dla danej architektury procesora, w tym przypadku 64-bitowego ARMv8-A (oznaczonego jako arm64-v8a).
Gdzie szukać tych plików?
Zacznijmy od podstaw: aby dostać się do tych danych, musimy rozpakować plik APK (Android Package) lub AAB (Android App Bundle). Możesz to zrobić za pomocą narzędzi takich jak apktool albo zwykłego unzip w przypadku APK. Po rozpakowaniu zobaczysz strukturę katalogów podobną do tej:
lib/
├── arm64-v8a/
│ ├── libassemblies.arm64-v8a.blob.so
│ ├── libmonosgen-2.0.so
│ ├── libaot-*.so
├── x86_64/
│ ├── [podobne pliki dla innej architektury]
├── armeabi-v7a/
│ ├── [itd.]
libassemblies.arm64-v8a.blob.so
: Kluczowy plik ELF, który zawiera wszystkie biblioteki .NET dla architektury arm64-v8a.libmonosgen-2.0.so
: Silnik Mono, który odpowiada za uruchamianie kodu .NET na Androidzie.libaot-*.so
: Pliki związane z kompilacją AOT (Ahead-of-Time), czyli wstępnie skompilowanym kodem natywnym dla lepszej wydajności.
W zależności od tego, jakie architektury wspiera aplikacja, znajdziesz różne wersje tych plików. W tym artykule skupimy się na libassemblies.arm64-v8a.blob.so
, bo to najpopularniejsza architektura w nowoczesnych urządzeniach z Androidem.
Co kryje się w pliku ELF?
Pliki ELF to standardowy format binarny używany do przechowywania bibliotek, programów wykonywalnych i innych danych w systemach uniksowych. Aby zajrzeć do środka libassemblies.arm64-v8a.blob.so
, możemy użyć narzędzia llvm-readelf
(dostępnego w pakiecie LLVM). Wyświetli nam ono nagłówki sekcji (section headers):
$ llvm-readelf --section-headers libassemblies.arm64-v8a.blob.so
There are 11 section headers, starting at offset 0x158ade8:
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .note.gnu.build-id NOTE 0000000000000200 000200 000024 00 A 0 0 4
[ 2] .dynsym DYNSYM 0000000000000228 000228 000030 18 A 5 1 8
[ 3] .gnu.hash GNU_HASH 0000000000000258 000258 000020 00 A 2 0 8
[ 4] .hash HASH 0000000000000278 000278 000018 04 A 2 0 4
[ 5] .dynstr STRTAB 0000000000000290 000290 000040 00 A 0 0 1
[ 6] .dynamic DYNAMIC 00000000000042d0 0002d0 0000b0 10 WA 5 0 8
[ 7] .relro_padding NOBITS 0000000000004380 000380 000c80 00 WA 0 0 1
[ 8] .data PROGBITS 0000000000008380 000380 000001 00 WA 0 0 1
[ 9] .shstrtab STRTAB 0000000000000000 000381 00005e 00 0 0 1
[10] payload PROGBITS 0000000000000000 004000 1586de4 00 0 0 16384
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info), ...
Najciekawsza dla nas jest sekcja payload
:
- Rozmiar:
1586de4
bajtów, czyli około 22 MB. - Typ:
PROGBITS
, co oznacza dane binarne gotowe do załadowania do pamięci. - Nazwa:
payload
- niestandardowa sekcja stworzona specjalnie dla .NET MAUI 9, w której ukryto biblioteki .NET.
Krok 1: Wypakowanie sekcji payload
Aby wydobyć zawartość sekcji payload
, użyjemy narzędzia llvm-objcopy
. Pozwala ono na wyodrębnienie konkretnej sekcji z pliku ELF do osobnego pliku binarnego:
$ llvm-objcopy --dump-section=payload=payload.bin libassemblies.arm64-v8a.blob.so
Po wykonaniu tego polecenia otrzymujemy plik payload.bin
. Sprawdźmy jego rozmiar, aby upewnić się, że wszystko poszło zgodnie z planem:
$ ls -lh payload.bin
-rw-rw-r-- 1 22572516 Mar 14 12:56 payload.bin
Rozmiar (około 22 MB) zgadza się z tym, co widzieliśmy w nagłówkach ELF - mamy więc pewność, że wyodrębniliśmy właściwe dane.
Co jest w środku?
Sprawdźmy pierwsze bajty pliku payload.bin
za pomocą hexdump, aby potwierdzić, że mamy do czynienia z formatem .NET:
$ hexdump -c -n 4 payload.bin
0000000 X A B A
0000004
Sekwencja XABA
(w hex: 0x41424158
) to tzw. magic number, który identyfikuje format assembly store w .NET MAUI 9. To odpowiednik dawnego assemblies.blob
.
Krok 2: Rozpracowanie formatu Assembly Store
Plik payload.bin
nie jest pojedynczym plikiem DLL, lecz magazynem (store), który zawiera wiele bibliotek .NET wraz z metadanymi. Jego struktura wygląda następująco:
- Nagłówek (Header): 20 bajtów zawierających podstawowe informacje, takie jak sygnatura (
XABA
), wersja formatu i liczba bibliotek. - Deskryptory (Descriptors): Lista rekordów określających offsety i rozmiary każdej biblioteki w pliku.
- Nazwy bibliotek: Ciągi znaków w formacie UTF-8 określające nazwy plików DLL (np.
System.IO.dll
). - Dane bibliotek: Surowe dane binarne poszczególnych bibliotek.
Parsowanie nagłówka
Nagłówek ma stały rozmiar 20 bajtów. Możemy go sparsować w Pythonie, definiując klasę AssemblyStoreHeader
:
import struct
class AssemblyStoreHeader:
def __init__(self, magic, version, entry_count, index_entry_count, index_size):
self.magic = magic # Sygnatura "XABA" (0x41424158)
self.version = version # Wersja formatu
self.entry_count = entry_count # Liczba bibliotek w magazynie
self.index_entry_count = index_entry_count # Liczba wpisów w indeksie (opcjonalne)
self.index_size = index_size # Rozmiar sekcji indeksu w bajtach
@staticmethod
def from_file(f):
data = f.read(20) # Czytamy 20 bajtów
magic, version, entry_count, index_entry_count, index_size = struct.unpack('<5I', data)
if magic != 0x41424158: # Sprawdzamy sygnaturę "XABA"
raise ValueError("Invalid magic number, not an assembly store file")
return AssemblyStoreHeader(magic, version, entry_count, index_entry_count, index_size)
Odczyt deskryptorów
Każda biblioteka ma swój deskryptor - rekord o rozmiarze 28 bajtów, który opisuje położenie i rozmiar jej danych. Oto definicja w Pythonie:
class AssemblyStoreEntryDescriptor:
def __init__(self, mapping_index, data_offset, data_size, debug_offset, debug_size, config_offset, config_size):
self.mapping_index = mapping_index # Indeks mapowania (nieużywany w ekstrakcji)
self.data_offset = data_offset # Offset danych biblioteki
self.data_size = data_size # Rozmiar danych biblioteki
self.debug_offset = debug_offset # Offset danych debugowania (opcjonalne)
self.debug_size = debug_size # Rozmiar danych debugowania
self.config_offset = config_offset # Offset danych konfiguracyjnych (opcjonalne)
self.config_size = config_size # Rozmiar danych konfiguracyjnych
@staticmethod
def from_file(f):
data = f.read(28) # 7 pól po 4 bajty = 28 bajtów
return AssemblyStoreEntryDescriptor(*struct.unpack('<7I', data))
Odczyt nazw bibliotek
Po deskryptorach znajdują się nazwy bibliotek w formacie: długość ciągu (4 bajty) + ciąg znaków w UTF-8. Kod do ich odczytu wygląda tak:
def read_names(f, entry_count):
names = []
for _ in range(entry_count):
name_length = struct.unpack('<I', f.read(4))[0] # Długość nazwy w bajtach
name = f.read(name_length).decode('utf-8') # Odczyt nazwy
names.append(name)
return names
Krok 3: Ekstrakcja bibliotek
Teraz, gdy mamy wszystkie metadane, możemy wydobyć dane bibliotek i zapisać je jako osobne pliki DLL. Oto pełna metoda ekstrakcji:
import os
def extract_assemblies(file_path, output_dir):
os.makedirs(output_dir, exist_ok=True)
with open(file_path, 'rb') as f:
# Odczyt nagłówka
header = AssemblyStoreHeader.from_file(f)
print(f"Found {header.entry_count} assemblies in store")
# Odczyt deskryptorów
descriptors = [AssemblyStoreEntryDescriptor.from_file(f) for _ in range(header.entry_count)]
# Przesunięcie do nazw (po pominięciu indeksu, jeśli istnieje)
f.seek(header.index_size, 1)
names = read_names(f, header.entry_count)
# Ekstrakcja bibliotek
for desc, name in zip(descriptors, names):
f.seek(desc.data_offset)
assembly_data = f.read(desc.data_size)
# Dodajemy rozszerzenie .dll, jeśli brakuje
if not name.endswith('.dll'):
name += '.dll'
out_path = os.path.join(output_dir, name)
with open(out_path, 'wb') as out_file:
out_file.write(assembly_data)
print(f"Extracted: {out_path}")
Po uruchomieniu tego kodu otrzymujemy katalog z wyodrębnionymi plikami DLL, np. System.IO.dll
, MainApp.dll
itp.
Krok 4: Dekompresja LZ4
Jeśli spróbujemy otworzyć te pliki w narzędziu do dekompilacji, jak dotPeek, możemy zauważyć, że coś jest nie tak. Sprawdźmy pierwsze bajty jednego z plików:
$ hexdump -c -n 4 ./output/System.IO.FileSystem.Primitives.dll
0000000 X A L Z
0000004
Sygnatura XALZ
oznacza, że dane są dodatkowo skompresowane algorytmem LZ4. Aby je zdekompresować, potrzebujemy biblioteki lz4 w Pythonie:
def decompress_lz4(compressed_data):
uncompressed_size = struct.unpack('<I', compressed_data[8:12])[0]
compressed_payload = compressed_data[12:]
return lz4.block.decompress(compressed_payload, uncompressed_size=uncompressed_size)
Po dekompresji otrzymujemy poprawne pliki DLL, które można otworzyć w narzędziach do dekompilacji .NET, takich jak dotPeek (od JetBrains).libassemblies.arm64-v8a.blob.so
.
Skrypt automtyzujący cały proces możesz znaleźć tutaj.
Bibliografia
- https://github.com/dotnet/android/blob/main/Documentation/project-docs/ApkSharedLibraries.md
- https://github.com/dotnet/android/blob/main/Documentation/project-docs/AssemblyStores.md
- https://github.com/dotnet/android/blob/main/Documentation/project-docs/ExploringSources.md
- https://github.com/jakev/pyxamstore/blob/master/pyxamstore/explorer.py
- https://github.com/dotnet/maui/releases/tag/9.0.0
- https://github.com/mwalkowski/pymauistore/tree/main