add update-suppression and account-gate references; fix two script defects
- references/updates-and-forced-upgrade.md: keeping a patched build alive (locating the version check, the two-layer patch, what not to touch, self-update and hot-update channels, verifying no version request is issued) - references/account-gates.md: client-side sign-in/binding gates vs account-scoped resources, why forging a session is worse than being signed out, session loss after reinstall - SKILL.md / README.md: wire both into the decision list, workflow and indexes - scripts/dex_classdiff.py: had no argparse, so --help crashed with IndexError; added proper CLI, non-zero exit on structural difference, removed a dead duplicate read - scripts/dex_strings.py: --find compiled the regex as bytes but matched decoded text, raising TypeError on the main path; split into text and byte patterns, added --max-print output cap
This commit is contained in:
+42
-21
@@ -29,6 +29,8 @@ Notes
|
||||
*uses* that feature. Confirm with runtime behavior before acting.
|
||||
* `--urls` output is the fastest way to see an app's whole API surface and its
|
||||
third-party endpoints at once.
|
||||
* A dex may be a multi-dex set: run this per directory, not per file, to see which
|
||||
split holds a given marker.
|
||||
"""
|
||||
import argparse
|
||||
import glob
|
||||
@@ -38,7 +40,6 @@ import struct
|
||||
import sys
|
||||
|
||||
URL_RE = re.compile(rb'https?://[A-Za-z0-9\.\-_:/%\.\?=&~#]{4,200}')
|
||||
ASCII_RE = re.compile(rb'[\x20-\x7e]{4,200}')
|
||||
|
||||
|
||||
def uleb128(data, off):
|
||||
@@ -55,7 +56,7 @@ def uleb128(data, off):
|
||||
|
||||
|
||||
def dex_strings(path):
|
||||
"""Yield the dex string table in table order."""
|
||||
"""Yield the dex string table in table order, as bytes."""
|
||||
data = open(path, 'rb').read()
|
||||
if data[:4] != b'dex\n':
|
||||
return
|
||||
@@ -74,18 +75,28 @@ def iter_dex(target):
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(add_help=True)
|
||||
ap.add_argument('target')
|
||||
ap.add_argument('--urls', action='store_true')
|
||||
ap.add_argument('--strings', action='store_true')
|
||||
ap.add_argument('--classes', metavar='PREFIX')
|
||||
ap.add_argument('--find', metavar='REGEX')
|
||||
ap.add_argument('--per-file', action='store_true')
|
||||
ap.add_argument('--diff', nargs=2, metavar=('A', 'B'))
|
||||
ap.add_argument('--min', type=int, default=6)
|
||||
ap.add_argument('--max', type=int, default=200)
|
||||
ap = argparse.ArgumentParser(
|
||||
description='Extract strings / URLs / class descriptors / vendor markers from dex files '
|
||||
'without a decompiler.')
|
||||
ap.add_argument('target', nargs='?', default=None,
|
||||
help='a .dex file or a directory containing .dex files (omit when using --diff)')
|
||||
ap.add_argument('--urls', action='store_true', help='print only http(s) URLs')
|
||||
ap.add_argument('--strings', action='store_true', help='dump raw string table entries')
|
||||
ap.add_argument('--classes', metavar='PREFIX',
|
||||
help='print only entries starting with this prefix (e.g. Lcom/example/app/)')
|
||||
ap.add_argument('--find', metavar='REGEX',
|
||||
help='only keep entries matching this regex (applied to the decoded text)')
|
||||
ap.add_argument('--per-file', action='store_true',
|
||||
help='with --find: report one line per dex with a hit count, instead of each string')
|
||||
ap.add_argument('--diff', nargs=2, metavar=('A', 'B'),
|
||||
help='compare two dex files string tables')
|
||||
ap.add_argument('--min', type=int, default=6, help='minimum string length (default 6)')
|
||||
ap.add_argument('--max', type=int, default=200, help='maximum string length (default 200)')
|
||||
ap.add_argument('--max-print', type=int, default=5000,
|
||||
help='stop printing after this many lines (default 5000; use 0 for unlimited)')
|
||||
a = ap.parse_args()
|
||||
|
||||
# --diff mode needs no target
|
||||
if a.diff:
|
||||
A = set(dex_strings(a.diff[0]))
|
||||
B = set(dex_strings(a.diff[1]))
|
||||
@@ -100,24 +111,32 @@ def main():
|
||||
print(' +', x.decode('utf-8', 'replace'))
|
||||
return 0
|
||||
|
||||
pat = re.compile(a.find.encode()) if a.find else None
|
||||
if not a.target:
|
||||
ap.error('target is required unless --diff is used')
|
||||
|
||||
# Two patterns: the per-file mode scans raw bytes (fast), the per-string mode
|
||||
# matches decoded text. Keeping both avoids the bytes/str mismatch that silently
|
||||
# breaks one of the two paths.
|
||||
pat_text = re.compile(a.find) if a.find else None
|
||||
pat_bytes = re.compile(a.find.encode('utf-8')) if a.find else None
|
||||
files = iter_dex(a.target)
|
||||
|
||||
if a.find and a.per_file:
|
||||
for fp in files:
|
||||
data = open(fp, 'rb').read()
|
||||
hits = len(pat.findall(data))
|
||||
hits = len(pat_bytes.findall(data))
|
||||
if hits:
|
||||
print('%-22s hits=%d' % (os.path.basename(fp), hits))
|
||||
return 0
|
||||
|
||||
printed = 0
|
||||
seen = set()
|
||||
for fp in files:
|
||||
for raw in dex_strings(fp):
|
||||
s = raw.decode('utf-8', 'replace')
|
||||
if not (a.min <= len(s) <= a.max):
|
||||
continue
|
||||
if pat and not pat.search(s):
|
||||
if pat_text and not pat_text.search(s):
|
||||
continue
|
||||
if a.classes and not s.startswith(a.classes):
|
||||
continue
|
||||
@@ -126,18 +145,20 @@ def main():
|
||||
if s in seen:
|
||||
continue
|
||||
seen.add(s)
|
||||
if a.urls:
|
||||
print(s)
|
||||
elif a.classes:
|
||||
if a.max_print and printed >= a.max_print:
|
||||
print('[note] output cap reached (%d). Use --max-print 0 to lift.' % a.max_print)
|
||||
return 0
|
||||
if a.classes:
|
||||
print('%-18s %s' % (os.path.basename(fp), s))
|
||||
else:
|
||||
print('%s' % s)
|
||||
print(s)
|
||||
printed += 1
|
||||
|
||||
if a.urls and not seen:
|
||||
# fall back to raw byte scan (covers URLs stored outside the string table)
|
||||
# fall back to a raw byte scan: covers URLs stored outside the string table
|
||||
print('[note] no URLs in string tables; raw scan:')
|
||||
for fp in files:
|
||||
for m in set(URL_RE.findall(open(fp, 'rb').read())):
|
||||
for m in sorted(set(URL_RE.findall(open(fp, 'rb').read()))):
|
||||
print(os.path.basename(fp), m.decode('utf-8', 'replace'))
|
||||
return 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user