Decompiling an Android Application Written in .NET MAUI 9 (Xamarin)
.NET MAUI, as the successor to Xamarin, enables the development of cross-platform applications, including Android, using C#. In previous versions (up to .NET MAUI 8), applications stored their DLL libraries in assemblies.blob and assemblies.manifest files, making extraction and analysis easier. Tools already existed to decompile these files, such as pyxamstore, which allowed for easy recovery of original libraries. With the release of .NET MAUI 9, a significant change was introduced in the way .NET libraries are packaged in Android applications. Instead of storing DLLs in separate files, all libraries are now embedded in ELF (.so) files named libassemblies.<arch>.blob.so, where <arch> refers to the processor architecture, e.g., arm64-v8a. This change aimed to improve performance, enhance security, and provide better compatibility with Android. In this article, I will explain how to extract these DLL files.

Structure of the libassemblies.arm64-v8a.blob.so File: What has changed?
In .NET MAUI 9, DLL libraries are no longer stored as separate files in the Android application directory. Instead, Microsoft has embedded them within ELF files—a format well-known in Unix-like systems, including Android. At first glance, the libassemblies.arm64-v8a.blob.so
file looks like a standard native shared library (with a .so extension), similar to libmonosgen-2.0.so
. However, it hides something more - a complete set of .NET libraries tailored for a specific processor architecture, in this case, the 64-bit ARMv8-A (denoted as arm64-v8a).
Where to find these files?
Let’s start with the basics: to access this data, we need to unpack an APK (Android Package) or AAB (Android App Bundle) file. You can do this using tools like apktool or simply unzip for an APK. After unpacking, you’ll see a directory structure similar to this:
lib/
├── arm64-v8a/
│ ├── libassemblies.arm64-v8a.blob.so
│ ├── libmonosgen-2.0.so
│ ├── libaot-*.so
├── x86_64/
│ ├── [similar files for another architecture]
├── armeabi-v7a/
│ ├── [and so on]
libassemblies.arm64-v8a.blob.so
: The key ELF file containing all .NET libraries for the arm64-v8a architecture.libmonosgen-2.0.so
: The Mono engine, responsible for running .NET code on Android.libaot-*.so
: Files related to Ahead-of-Time (AOT) compilation, i.e., precompiled native code for improved performance.
Depending on the architectures supported by the application, you’ll find different versions of these files. In this article, we’ll focus on libassemblies.arm64-v8a.blob.so
, as it’s the most common architecture in modern Android devices.
What’s inside an ELF file?
ELF files are a standard binary format used to store libraries, executable programs, and other data in Unix-like systems. To peek inside libassemblies.arm64-v8a.blob.so
, we can use the llvm-readelf
tool (available in the LLVM package). It will display the 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), ...
The most interesting section for us is payload
:
- Size:
1586de4
bytes, approximately 22 MB. - Type:
PROGBITS
, indicating binary data ready to be loaded into memory. - Name:
payload
non-standard section created specifically for .NET MAUI 9, where the .NET libraries are hidden.
Step 1: Extracting the payload section
To extract the contents of the payload
section, we’ll use the llvm-objcopy
tool. It allows us to dump a specific section from an ELF file into a separate binary file:
$ llvm-objcopy --dump-section=payload=payload.bin libassemblies.arm64-v8a.blob.so
After running this command, we get a payload.bin
file. Let’s check its size to ensure everything went as planned:
$ ls -lh payload.bin
-rw-rw-r-- 1 22572516 Mar 14 12:56 payload.bin
The size (around 22 MB) matches what we saw in the ELF headers, so we’re confident we’ve extracted the correct data.
What’s Inside?
Let’s examine the first few bytes of payload.bin
using hexdump to confirm it’s in a .NET format:
$ hexdump -c -n 4 payload.bin
0000000 X A B A
0000004
The XABA
sequence (in hex: 0x41424158
) is a so-called magic number that identifies the assembly store format in .NET MAUI 9. It’s the equivalent of the older assemblies.blob
.
Step 2: Breaking down the Assembly Store format
The payload.bin
file isn’t a single DLL but a store containing multiple .NET libraries along with metadata. Its structure is as follows:
- Header: 20 bytes containing basic information such as the signature (
XABA
), format version, and number of libraries. - Descriptors: A list of records specifying the offsets and sizes of each library in the file.
- Library Names: UTF-8 encoded strings specifying the names of DLL files (e.g.,
System.IO.dll
). - Library Data: The raw binary data of the individual libraries.
Parsing the header
The header has a fixed size of 20 bytes. We can parse it in Python by defining an AssemblyStoreHeader
class:
import struct
class AssemblyStoreHeader:
def __init__(self, magic, version, entry_count, index_entry_count, index_size):
self.magic = magic # "XABA" signature (0x41424158)
self.version = version # Format version
self.entry_count = entry_count # Number of libraries in the store
self.index_entry_count = index_entry_count # Number of index entries (optional)
self.index_size = index_size # Size of the index section in bytes
@staticmethod
def from_file(f):
data = f.read(20) # Read 20 bytes
magic, version, entry_count, index_entry_count, index_size = struct.unpack('<5I', data)
if magic != 0x41424158: # Check the "XABA" signature
raise ValueError("Invalid magic number, not an assembly store file")
return AssemblyStoreHeader(magic, version, entry_count, index_entry_count, index_size)
Reading descriptors
Each library has a descriptor-a 28-byte record that describes the location and size of its data. Here’s the Python definition:
class AssemblyStoreEntryDescriptor:
def __init__(self, mapping_index, data_offset, data_size, debug_offset, debug_size, config_offset, config_size):
self.mapping_index = mapping_index # Mapping index (unused in extraction)
self.data_offset = data_offset # Offset of library data
self.data_size = data_size # Size of library data
self.debug_offset = debug_offset # Offset of debug data (optional)
self.debug_size = debug_size # Size of debug data
self.config_offset = config_offset # Offset of configuration data (optional)
self.config_size = config_size # Size of configuration data
@staticmethod
def from_file(f):
data = f.read(28) # 7 fields of 4 bytes each = 28 bytes
return AssemblyStoreEntryDescriptor(*struct.unpack('<7I', data))
Reading library names
Following the descriptors are the library names, stored in the format: length of the string (4 bytes) + UTF-8 encoded string. Here’s the Python code to read them:
def read_names(f, entry_count):
names = []
for _ in range(entry_count):
name_length = struct.unpack('<I', f.read(4))[0] # Length of the name in bytes
name = f.read(name_length).decode('utf-8') # Read the name
names.append(name)
return names
Step 3: Extracting libraries
Now that we have all the metadata, we can extract the library data and save it as individual DLL files. Here’s the complete extraction method:
import os
def extract_assemblies(file_path, output_dir):
os.makedirs(output_dir, exist_ok=True)
with open(file_path, 'rb') as f:
# Read the header
header = AssemblyStoreHeader.from_file(f)
print(f"Found {header.entry_count} assemblies in store")
# Read the descriptors
descriptors = [AssemblyStoreEntryDescriptor.from_file(f) for _ in range(header.entry_count)]
# Skip to the names (after skipping the index, if present)
f.seek(header.index_size, 1)
names = read_names(f, header.entry_count)
# Extract libraries
for desc, name in zip(descriptors, names):
f.seek(desc.data_offset)
assembly_data = f.read(desc.data_size)
# Add .dll extension if missing
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}")
Running this code produces a directory with extracted DLL files, such as System.IO.dll
, MainApp.dll
, etc.
Step 4: LZ4 decompression
If you try opening these files in a decompilation tool like dotPeek, you might notice something’s off. Let’s check the first few bytes of one of the files:
bash
$ hexdump -c -n 4 ./output/System.IO.FileSystem.Primitives.dll
0000000 X A L Z
0000004
The XALZ
signature indicates that the data is additionally compressed using the LZ4 algorithm. To decompress it, we need the lz4 library in Python:
import lz4.block
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)
After decompression, we obtain valid DLL files that can be opened in .NET decompilation tools like dotPeek (from JetBrains).libassemblies.arm64-v8a.blob.so
.
You can find a script automating the entire process here.
References
- 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