Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

axmlite

Read Android binary XML (AXML) with the Python standard library alone.

An APK does not ship its manifest as readable XML. AndroidManifest.xml inside the archive is compiled into Android's binary resource format, so anything that needs to know what an app declares — exported components, permissions, intent filters, deep links, cleartext policy — has to decode that format first.

import axmlite

for el in axmlite.from_apk("app.apk"):
    if el.tag == "activity" and el.get("exported"):
        print(el.attrs["name"], "is exported")
$ python -m axmlite app.apk
<manifest package="com.example.bank" versionCode="42">
  <uses-permission android:name="android.permission.READ_SMS" />
  <application android:debuggable="true" android:usesCleartextTraffic="true">
    <activity android:name=".Main" android:exported="true" />
    <provider android:name=".Data" android:exported="false" />
  </application>
</manifest>

$ python -m axmlite app.apk --json | jq '.[] | select(.tag=="uses-permission")'

No dependencies. Python 3.8+.


Why another AXML reader

It runs where pip install does not. Malware triage often happens on an isolated VM, an air-gapped lab machine, or a customer's laptop you were handed ten minutes ago. A tool that needs network access at that moment is a tool you do not have. axmlite imports struct, zipfile and json. That is the whole dependency list, and it is also why it is a single file you can copy into a repository or paste into a notebook.

It assumes the input is hostile. A manifest handed to an analyst is frequently malformed on purpose: string counts that overflow, offsets pointing outside the buffer, chunk sizes of zero that spin a naive loop forever. Parsers written against well-formed input crash, hang, or exhaust memory on exactly the samples you most want to read. Here, every length and offset taken from the file is validated against the buffer before use.

Mode Behaviour
parse(data) Tolerant. Returns whatever was readable. Never raises.
parse(data, strict=True) Raises AXMLError on the first inconsistency, and nothing else — no IndexError, no struct.error, no MemoryError.

Both contracts are enforced by fuzzing, not by inspection: 3 000 random byte mutations plus every possible truncation point of a valid file.


How it is verified, and why round-tripping is not enough

This library exists because of a specific bug, and the test suite is shaped around not repeating it.

The AXML node layout is:

ResXMLTree_node                      offset 0
    chunk header    type, headerSize, size      8 bytes
    lineNumber                                  4 bytes
    comment                                     4 bytes
ResXMLTree_attrExt                   offset 16
    ns, name                                    8 bytes
    attributeStart, attributeSize, ...         12 bytes
attributes                           offset 16 + attributeStart

attributeStart is documented in AOSP's ResourceTypes.h as "byte offset from the start of this structure" — this structure being ResXMLTree_attrExt, not the chunk. Real manifests write 20, so attributes begin at offset 36.

An earlier version of this code measured from the chunk start and read at offset 20 — sixteen bytes early. It returned zero attributes from every conformant manifest. No exception, no warning: just an empty dictionary where exported, debuggable and usesCleartextTraffic should have been. A manifest audit built on it would have reported a clean app, always.

It survived because the round-trip test used an encoder written by the same author, which wrote 36 to match. Encoder and parser were wrong in the same direction, so they agreed, and the test passed.

The lesson is in the test layout:

Layer What it proves
Round-trip against the bundled encoder The parser agrees with us. Cheap; weakest.
Byte-exact specification vectors The parser agrees with the format. Offsets are written out with the AOSP struct in the comment, so a reviewer can check them by reading.
Hostile input and fuzzing It survives data written to break it.

There is also a test asserting that an encoder writing the wrong offset produces visibly wrong output — so the round-trip layer can never again quietly certify a mutually consistent error.


API

parse(data: bytes, *, strict: bool = False) -> list[Element]
parse_file(path, *, strict=False)  -> list[Element]
from_apk(path, *, strict=False)    -> list[Element]     # reads AndroidManifest.xml
to_xml(elements, indent="  ")      -> str

Element is a dataclass in document order:

Field Meaning
tag element name
attrs attributes by local name — exported
qualified attributes by prefixed name — android:exported
path manifest/application/activity
depth, line, namespace position and source line

Why both attrs and qualified

A manifest may declare both exported and android:exported on the same element. Android honours only the namespaced one. A tool that flattens to local names sees whichever happened to come last — which makes the pair a usable way to show one value to an analysis tool and another to the platform. Keep attrs for convenience; reach for qualified when the answer matters.

Typed values are decoded to Python: booleans to bool, integers to signed int, floats, dimensions (16dip), fractions, colours (#ff0000ff), and references as @0x7f010001.


Scope

What it does not do, deliberately:

  • No resource resolution. A @0x7f010001 reference is returned as-is; mapping it to a string needs resources.arsc, which is a different format and a different problem.
  • No text nodes. CDATA chunks are skipped. Manifests do not use them.
  • No writing. A reader should not carry a writer it does not need. The encoder used to exercise the parser lives in the test file.
  • No APK integrity checks. from_apk uses zipfile, which reads the central directory. An APK can carry a local header that disagrees with it — a known way to show one manifest to a tool and another to Android. axmlite does not pretend to resolve that ambiguity; if it matters to your threat model, compare the headers yourself.

If you need resource resolution, DEX parsing and a full APK model, use androguard. axmlite is for the case where you want one answer, quickly, with nothing installed.


Tests

python -m unittest discover -s tests

22 tests, no dependencies, no network.

Licence

MIT.

About

Read Android binary XML (AndroidManifest.xml) with the Python standard library alone. No dependencies, safe on hostile input.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages