Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@ pip install .
After installation, the `droidasc` CLI command is available globally:

```
usage: droidasc [-h] {getclass,getmanifest,findrefs} ...
usage: droidasc [-h] {getclass,listclass,getmanifest,findrefs} ...

ASC tooling entry.

positional arguments:
{getclass,getmanifest,findrefs}
{getclass,listclass,getmanifest,findrefs}
getclass Locate the target class in APK, extract one DEX in memory, then decompile.
listclass List classes defined across all DEX entries in APK.
getmanifest Decode AndroidManifest.xml from APK and print it as XML.
findrefs Find code references for string/type/method/field across all DEX entries in APK.

Expand All @@ -41,6 +42,8 @@ examples:
droidasc app.apk --gui
droidasc getclass app.apk Lcom/poc/Main; -o Main.java
droidasc getclass app.apk com.poc.Main --threads 16
droidasc listclass app.apk -o classes.txt
droidasc listclass app.apk --prefix com.poc
droidasc getmanifest app.apk -o AndroidManifest.xml
droidasc findrefs app.apk string token -o string_refs.txt
droidasc findrefs app.apk type com.poc.Main
Expand All @@ -49,4 +52,9 @@ examples:
droidasc findrefs app.apk field apiKey -o field_refs.txt
```

`listclass` prints Dalvik class descriptors in APK/DEX definition order, one per
line. With `-o`, output is written to the selected file instead of stdout.
`--prefix com.poc` filters by `Lcom/poc`; an already normalized prefix such as
`Lcom/poc` is kept unchanged.

You can also use `python main.py` as before — it delegates to the same entry point.
94 changes: 94 additions & 0 deletions droidasc/asc_client/apk_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,100 @@ def get_class_dex(self, dalvik_class : str):
mm.close()
fp.close()

def list_classes(self, prefix : str = None):
if self.max_workers <= 0:
raise ValueError("Worker count must be greater than zero")
if prefix is not None:
prefix = prefix.strip()
if not prefix:
raise ValueError("Class prefix cannot be empty")
if not prefix.startswith("L"):
prefix = "L" + prefix.replace(".", "/")
prefix_bytes = prefix.encode("ascii") if prefix and prefix.isascii() else None

def list_dex_classes(buf):
if len(buf) < 0x70 or buf[:3] != b"dex":
raise ValueError("invalid DEX header")

string_ids_size = _U32_FROM(buf, 0x38)[0]
string_ids_off = _U32_FROM(buf, 0x3C)[0]
type_ids_size = _U32_FROM(buf, 0x40)[0]
type_ids_off = _U32_FROM(buf, 0x44)[0]
class_defs_size = _U32_FROM(buf, 0x60)[0]
class_defs_off = _U32_FROM(buf, 0x64)[0]

if class_defs_size == 0:
return []
if string_ids_off + string_ids_size * 4 > len(buf):
raise ValueError("bad string_ids range")
if type_ids_off + type_ids_size * 4 > len(buf):
raise ValueError("bad type_ids range")
if class_defs_off + class_defs_size * 32 > len(buf):
raise ValueError("bad class_defs range")

names = []
append = names.append
for class_def_off in range(class_defs_off, class_defs_off + class_defs_size * 32, 32):
type_idx = _U32_FROM(buf, class_def_off)[0]
if type_idx >= type_ids_size:
raise ValueError("bad class_def->type_idx")
string_idx = _U32_FROM(buf, type_ids_off + (type_idx << 2))[0]
if string_idx >= string_ids_size:
raise ValueError("bad type_id->string_idx")
string_off = _U32_FROM(buf, string_ids_off + (string_idx << 2))[0]
if string_off >= len(buf):
raise ValueError("bad string_data_off")
try:
raw_name = _read_string_data_bytes(buf, string_off)
except IndexError as error:
raise ValueError("bad string_data_off") from error

if prefix_bytes is not None:
if raw_name.startswith(prefix_bytes):
append(raw_name.decode("utf-8", errors="replace"))
else:
name = raw_name.decode("utf-8", errors="replace")
if prefix is None or name.startswith(prefix):
append(name)
return names

t_start = time.perf_counter()
fp, mm = self._open_apk()
try:
entries = _parse_cd_dex_entries(mm)
if not entries:
return []

def list_entry(entry):
data = _inflate_dex(mm, entry)
names = []
for dex_name, dex_buf in iter_logical_dex_buffers(entry[0], data):
names.extend(list_dex_classes(dex_buf))
return entry[0], names

workers = min(len(entries), self.max_workers)
if workers == 1:
results = [list_entry(entry) for entry in entries]
else:
with ThreadPoolExecutor(max_workers=workers) as ex:
results = list(ex.map(list_entry, entries))

names = []
for dex_name, dex_names in results:
names.extend(dex_names)
self._log(f"[APK] '{dex_name}' class_count={len(dex_names)}")

if self.debug:
t_end = time.perf_counter()
self._log(
f"[APK] listclass total={(t_end - t_start) * 1000000:.2f} us "
f"count={len(names)} workers={workers}"
)
return names
finally:
mm.close()
fp.close()

def for_each_findrefs(self, find_type : str, find : dict):
# imported before the timer so this once-per-process import is not charged to a
# single search; it used to happen at apk_handler import time, and keeping the
Expand Down
54 changes: 52 additions & 2 deletions droidasc/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,32 @@ def _handle_getclass(args):
print(source_code)


def _handle_listclass(args):
from droidasc.asc_client.apk_handler import ApkHandler

names = ApkHandler(args.apk_path, debug=args.debug, max_workers=args.threads).list_classes(
args.prefix
)

output_fp = (
open(args.output, "w", encoding="utf-8", errors="replace", newline="\n")
if args.output
else None
)
out = output_fp if output_fp is not None else sys.stdout
try:
for start in range(0, len(names), 8192):
text = "\n".join(names[start:start + 8192]) + "\n"
out.write(text)
finally:
if output_fp is not None:
output_fp.close()

if args.debug:
t_end = time.perf_counter()
print(f"[DEBUG] Total Execution Time: {(t_end - t_start) * 1000000:.2f} us")


def _handle_getmanifest(args):
from droidasc.asc_client.manifest_handler import get_manifest_xml

Expand Down Expand Up @@ -189,9 +215,11 @@ def main():
try:
if args.command == "getclass":
_handle_getclass(args)
elif args.command == "listclass":
_handle_listclass(args)
elif args.command == "getmanifest":
_handle_getmanifest(args)
else:
elif args.command == "findrefs":
_handle_findrefs(args)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
Expand All @@ -209,6 +237,8 @@ def _build_main_parser():
droidasc app.apk --gui
droidasc getclass app.apk Lcom/poc/Main; -o Main.java
droidasc getclass app.apk com.poc.Main --threads 16
droidasc listclass app.apk -o classes.txt
droidasc listclass app.apk --prefix com.poc
droidasc getmanifest app.apk -o AndroidManifest.xml
droidasc findrefs app.apk string token -o string_refs.txt
droidasc findrefs app.apk type com.poc.Main
Expand All @@ -235,6 +265,26 @@ def _build_main_parser():
getclass_parser.add_argument("apk_path", help="Path to the input APK file.")
getclass_parser.add_argument("dalvik_class", help="The Dalvik format class name to extract (e.g., Lcom/poc/Main;).")

listclass_parser = subparsers.add_parser(
"listclass",
help="List classes across all DEX entries; use --prefix to filter by package/class prefix.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""examples:
droidasc listclass app.apk
droidasc listclass app.apk --prefix com.poc
droidasc listclass app.apk -o classes.txt
droidasc listclass app.apk --threads 16 --debug
""",
)
listclass_parser.add_argument("--debug", action="store_true", help="Enable debug profiling output.")
listclass_parser.add_argument("--threads", "--thread", type=int, default=8, help="Worker thread count.")
listclass_parser.add_argument(
"--prefix",
help="Only list classes with this package/class prefix (e.g., com.poc or Lcom/poc).",
)
listclass_parser.add_argument("-o", "--output", help="Write class names to this file instead of stdout.")
listclass_parser.add_argument("apk_path", help="Path to the input APK file.")

getmanifest_parser = subparsers.add_parser(
"getmanifest",
help="Decode AndroidManifest.xml from APK and print it as XML.",
Expand Down Expand Up @@ -322,4 +372,4 @@ def _build_main_parser():


if __name__ == "__main__":
main()
main()
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "droidasc"
version = "0.1.0"
version = "0.1.1.post1"
description = "ASC is a super FAST python Android Decompiler for Agents/Mobile Researchers"
readme = "README.md"
license = "Apache-2.0"
Expand Down
6 changes: 5 additions & 1 deletion tests/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ The regression runner fails if Androguard is missing or any test is skipped in
strict mode. It covers index zero, empty/fallback instruction maps, string layout,
zeroed reconstructed DEX signatures/checksums, dummy imports, the MUTF-8 shim,
CLI stored/Deflate multidex operation, GUI data-store reuse, and reference searches
without site packages. It does not open GUI windows.
without site packages. `listclass` coverage includes local class-table
enumeration, stable DEX definition order, normalized package-prefix filtering,
stored/Deflate multidex APKs, DEX 041 containers, malformed indices, operation
without site packages, and a large-tail Deflate fixture. It does not open GUI
windows.

The synthetic startup gate measures `getclass --debug` in nine fresh interpreters.
Its median budgets are 100 ms internal time and 250 ms wall time on Linux CI.
Expand Down
Loading