droidasc 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- droidasc/__init__.py +3 -0
- droidasc/__main__.py +3 -0
- droidasc/asc_client/__init__.py +0 -0
- droidasc/asc_client/apk_handler.py +463 -0
- droidasc/asc_client/asc_handler.py +129 -0
- droidasc/asc_client/dex_container.py +149 -0
- droidasc/asc_client/gui/__init__.py +1 -0
- droidasc/asc_client/gui/app.py +1578 -0
- droidasc/asc_client/gui/runtime.py +547 -0
- droidasc/asc_client/gui/search_worker.py +36 -0
- droidasc/asc_client/gui/settings.py +39 -0
- droidasc/asc_client/gui/source_edit.py +301 -0
- droidasc/asc_client/gui/text_utils.py +81 -0
- droidasc/asc_client/gui/theme.py +194 -0
- droidasc/asc_client/gui/widgets.py +527 -0
- droidasc/asc_client/manifest_handler.py +29 -0
- droidasc/asc_core/__init__.py +1 -0
- droidasc/asc_core/core/__init__.py +0 -0
- droidasc/asc_core/core/dex/dex_builder.py +483 -0
- droidasc/asc_core/core/dex/dex_constructor.py +325 -0
- droidasc/asc_core/core/dex/dex_manager.py +104 -0
- droidasc/asc_core/core/dex/dex_remapper.py +396 -0
- droidasc/asc_core/core/dvm_handlers.py +110 -0
- droidasc/asc_core/core/dvm_interpreter.py +55 -0
- droidasc/asc_core/findrefs/__init__.py +3 -0
- droidasc/asc_core/findrefs/findrefs_manager.py +110 -0
- droidasc/asc_core/findrefs/locator/__init__.py +1 -0
- droidasc/asc_core/findrefs/locator/base_locator.py +27 -0
- droidasc/asc_core/findrefs/locator/field_locator.py +147 -0
- droidasc/asc_core/findrefs/locator/insn_locator.py +204 -0
- droidasc/asc_core/findrefs/locator/method_locator.py +146 -0
- droidasc/asc_core/findrefs/locator/string_locator.py +76 -0
- droidasc/asc_core/findrefs/locator/type_locator.py +48 -0
- droidasc/asc_core/findrefs/scan/code_item_scan.py +271 -0
- droidasc/asc_core/models/__init__.py +0 -0
- droidasc/asc_core/models/dvm_opcode.py +298 -0
- droidasc/asc_core/resource/dvmopcode_template.py +74 -0
- droidasc/asc_core/resource/gen_dvmopcode.py +27 -0
- droidasc/asc_core/utils/__init__.py +0 -0
- droidasc/asc_core/utils/decompiler.py +285 -0
- droidasc/asc_core/utils/decompiler_simple.py +134 -0
- droidasc/asc_core/utils/dex_parser.py +294 -0
- droidasc/asc_core/utils/dvm_regdec.py +151 -0
- droidasc/asc_core/utils/leb128.py +80 -0
- droidasc/asc_core/utils/tinydex.py +536 -0
- droidasc/cli.py +325 -0
- droidasc-0.1.0.dist-info/METADATA +51 -0
- droidasc-0.1.0.dist-info/RECORD +52 -0
- droidasc-0.1.0.dist-info/WHEEL +5 -0
- droidasc-0.1.0.dist-info/entry_points.txt +2 -0
- droidasc-0.1.0.dist-info/licenses/LICENSE +202 -0
- droidasc-0.1.0.dist-info/top_level.txt +1 -0
droidasc/__init__.py
ADDED
droidasc/__main__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import mmap
|
|
3
|
+
import os
|
|
4
|
+
import struct
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
import types
|
|
9
|
+
import zlib
|
|
10
|
+
from concurrent.futures import ThreadPoolExecutor, FIRST_COMPLETED, wait
|
|
11
|
+
|
|
12
|
+
from droidasc.asc_client.dex_container import iter_logical_dex_buffers
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_U16 = struct.Struct("<H")
|
|
16
|
+
_U32 = struct.Struct("<I")
|
|
17
|
+
_U16_FROM = _U16.unpack_from
|
|
18
|
+
_U32_FROM = _U32.unpack_from
|
|
19
|
+
_CD_SIG = b"PK\x01\x02"
|
|
20
|
+
_LH_SIG = b"PK\x03\x04"
|
|
21
|
+
_EOCD_SIG = b"PK\x05\x06"
|
|
22
|
+
_DEX_SUFFIX = b".dex"
|
|
23
|
+
_SLASH = ord("/")
|
|
24
|
+
_DEFLATE_CHUNK = 1 << 19
|
|
25
|
+
|
|
26
|
+
_WORKER_APK_PATH = None
|
|
27
|
+
_WORKER_APK_FP = None
|
|
28
|
+
_WORKER_APK_MM = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _process_pool_context():
|
|
32
|
+
"""Prefer fork so workers do not have to re-import the whole module graph.
|
|
33
|
+
|
|
34
|
+
spawn re-executes the parent's __main__ and re-imports every module a worker
|
|
35
|
+
touches; measured on the 352MB sample that is 85ms of pool cost versus 19ms for
|
|
36
|
+
fork. fork is only used when it is safe: never on Windows, and never from a
|
|
37
|
+
multi-threaded parent (fork() there can deadlock on inherited locks).
|
|
38
|
+
"""
|
|
39
|
+
if os.name == "nt":
|
|
40
|
+
return None
|
|
41
|
+
try:
|
|
42
|
+
import multiprocessing
|
|
43
|
+
# decompiler.py stubs optional imports for startup; a stub is not a real
|
|
44
|
+
# module and would be accepted silently by __getattr__, so verify before
|
|
45
|
+
# handing it to ProcessPoolExecutor (a bogus context hangs instead of raising)
|
|
46
|
+
if not isinstance(multiprocessing, types.ModuleType) or not hasattr(multiprocessing, "get_context"):
|
|
47
|
+
return None
|
|
48
|
+
if threading.active_count() != 1:
|
|
49
|
+
return None
|
|
50
|
+
return multiprocessing.get_context("fork")
|
|
51
|
+
except (ImportError, ValueError, OSError):
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _skip_uleb128(buf, off : int) -> int:
|
|
56
|
+
while buf[off] & 0x80:
|
|
57
|
+
off += 1
|
|
58
|
+
return off + 1
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _read_string_data_bytes(buf, str_off : int) -> bytes:
|
|
62
|
+
ptr = _skip_uleb128(buf, str_off)
|
|
63
|
+
end = buf.find(b"\x00", ptr)
|
|
64
|
+
if end < 0:
|
|
65
|
+
raise ValueError("unterminated string_data_item")
|
|
66
|
+
return buf[ptr:end]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _find_type_idx(buf, target_bytes : bytes) -> int:
|
|
70
|
+
if len(buf) < 0x70 or buf[:3] != b"dex":
|
|
71
|
+
return -1
|
|
72
|
+
|
|
73
|
+
string_ids_size = _U32_FROM(buf, 0x38)[0]
|
|
74
|
+
string_ids_off = _U32_FROM(buf, 0x3C)[0]
|
|
75
|
+
type_ids_size = _U32_FROM(buf, 0x40)[0]
|
|
76
|
+
type_ids_off = _U32_FROM(buf, 0x44)[0]
|
|
77
|
+
|
|
78
|
+
if string_ids_size == 0 or type_ids_size == 0:
|
|
79
|
+
return -1
|
|
80
|
+
if string_ids_off + string_ids_size * 4 > len(buf):
|
|
81
|
+
raise ValueError("bad string_ids range")
|
|
82
|
+
if type_ids_off + type_ids_size * 4 > len(buf):
|
|
83
|
+
raise ValueError("bad type_ids range")
|
|
84
|
+
|
|
85
|
+
left = 0
|
|
86
|
+
right = type_ids_size - 1
|
|
87
|
+
while left <= right:
|
|
88
|
+
mid = (left + right) >> 1
|
|
89
|
+
str_idx = _U32_FROM(buf, type_ids_off + (mid << 2))[0]
|
|
90
|
+
if str_idx >= string_ids_size:
|
|
91
|
+
raise ValueError("bad type_id->string_idx")
|
|
92
|
+
str_off = _U32_FROM(buf, string_ids_off + (str_idx << 2))[0]
|
|
93
|
+
if str_off >= len(buf):
|
|
94
|
+
raise ValueError("bad string_data_off")
|
|
95
|
+
|
|
96
|
+
cls_bytes = _read_string_data_bytes(buf, str_off)
|
|
97
|
+
if cls_bytes == target_bytes:
|
|
98
|
+
return mid
|
|
99
|
+
if cls_bytes < target_bytes:
|
|
100
|
+
left = mid + 1
|
|
101
|
+
else:
|
|
102
|
+
right = mid - 1
|
|
103
|
+
|
|
104
|
+
return -1
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _class_defs_contains_type_idx(buf, type_idx : int) -> bool:
|
|
108
|
+
class_defs_size = _U32_FROM(buf, 0x60)[0]
|
|
109
|
+
class_defs_off = _U32_FROM(buf, 0x64)[0]
|
|
110
|
+
|
|
111
|
+
if class_defs_size == 0:
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
class_defs_end = class_defs_off + (class_defs_size << 5)
|
|
115
|
+
if class_defs_end > len(buf):
|
|
116
|
+
raise ValueError("bad class_defs range")
|
|
117
|
+
|
|
118
|
+
needle = type_idx.to_bytes(4, "little")
|
|
119
|
+
pos = buf.find(needle, class_defs_off, class_defs_end)
|
|
120
|
+
while pos != -1:
|
|
121
|
+
if ((pos - class_defs_off) & 0x1F) == 0:
|
|
122
|
+
return True
|
|
123
|
+
pos = buf.find(needle, pos + 1, class_defs_end)
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _dex_defines_class(buf, target_bytes : bytes) -> bool:
|
|
128
|
+
type_idx = _find_type_idx(buf, target_bytes)
|
|
129
|
+
if type_idx < 0:
|
|
130
|
+
return False
|
|
131
|
+
return _class_defs_contains_type_idx(buf, type_idx)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _find_eocd(mm : mmap.mmap) -> int:
|
|
135
|
+
search_start = max(0, len(mm) - 65536 - 22)
|
|
136
|
+
return mm.rfind(_EOCD_SIG, search_start)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _parse_cd_dex_entries(mm : mmap.mmap):
|
|
140
|
+
eocd_idx = _find_eocd(mm)
|
|
141
|
+
if eocd_idx < 0:
|
|
142
|
+
raise ValueError("EOCD not found")
|
|
143
|
+
|
|
144
|
+
cd_size = _U32_FROM(mm, eocd_idx + 12)[0]
|
|
145
|
+
cd_off = _U32_FROM(mm, eocd_idx + 16)[0]
|
|
146
|
+
cd_end = cd_off + cd_size
|
|
147
|
+
entries = []
|
|
148
|
+
seen_names = set()
|
|
149
|
+
pos = cd_off
|
|
150
|
+
|
|
151
|
+
while True:
|
|
152
|
+
pos = mm.find(b"classes", pos, cd_end)
|
|
153
|
+
if pos < 0:
|
|
154
|
+
break
|
|
155
|
+
header_off = pos - 46
|
|
156
|
+
pos += 7
|
|
157
|
+
if header_off < cd_off or mm[header_off:header_off + 4] != _CD_SIG:
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
name_len = _U16_FROM(mm, header_off + 28)[0]
|
|
161
|
+
name_start = pos - 7
|
|
162
|
+
name_end = name_start + name_len
|
|
163
|
+
if name_end > cd_end:
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
name_bytes = mm[name_start:name_end]
|
|
167
|
+
if (
|
|
168
|
+
not name_bytes.endswith(_DEX_SUFFIX)
|
|
169
|
+
or _SLASH in name_bytes
|
|
170
|
+
or name_bytes in seen_names
|
|
171
|
+
):
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
seen_names.add(name_bytes)
|
|
175
|
+
entries.append((
|
|
176
|
+
name_bytes.decode("utf-8", errors="ignore"),
|
|
177
|
+
_U32_FROM(mm, header_off + 24)[0],
|
|
178
|
+
_U32_FROM(mm, header_off + 20)[0],
|
|
179
|
+
_U32_FROM(mm, header_off + 42)[0],
|
|
180
|
+
_U16_FROM(mm, header_off + 10)[0],
|
|
181
|
+
))
|
|
182
|
+
|
|
183
|
+
if entries:
|
|
184
|
+
return entries
|
|
185
|
+
|
|
186
|
+
ptr = cd_off
|
|
187
|
+
while ptr + 46 <= cd_end:
|
|
188
|
+
if mm[ptr:ptr + 4] != _CD_SIG:
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
name_len = _U16_FROM(mm, ptr + 28)[0]
|
|
192
|
+
extra_len = _U16_FROM(mm, ptr + 30)[0]
|
|
193
|
+
comment_len = _U16_FROM(mm, ptr + 32)[0]
|
|
194
|
+
name_start = ptr + 46
|
|
195
|
+
name_end = name_start + name_len
|
|
196
|
+
name_bytes = mm[name_start:name_end]
|
|
197
|
+
if name_bytes.endswith(_DEX_SUFFIX) and _SLASH not in name_bytes:
|
|
198
|
+
entries.append((
|
|
199
|
+
name_bytes.decode("utf-8", errors="ignore"),
|
|
200
|
+
_U32_FROM(mm, ptr + 24)[0],
|
|
201
|
+
_U32_FROM(mm, ptr + 20)[0],
|
|
202
|
+
_U32_FROM(mm, ptr + 42)[0],
|
|
203
|
+
_U16_FROM(mm, ptr + 10)[0],
|
|
204
|
+
))
|
|
205
|
+
|
|
206
|
+
ptr = name_end + extra_len + comment_len
|
|
207
|
+
|
|
208
|
+
return entries
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _close_worker_apk():
|
|
212
|
+
global _WORKER_APK_PATH, _WORKER_APK_FP, _WORKER_APK_MM
|
|
213
|
+
|
|
214
|
+
mm = _WORKER_APK_MM
|
|
215
|
+
fp = _WORKER_APK_FP
|
|
216
|
+
_WORKER_APK_MM = None
|
|
217
|
+
_WORKER_APK_FP = None
|
|
218
|
+
_WORKER_APK_PATH = None
|
|
219
|
+
|
|
220
|
+
if mm is not None:
|
|
221
|
+
mm.close()
|
|
222
|
+
if fp is not None:
|
|
223
|
+
fp.close()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
atexit.register(_close_worker_apk)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _get_worker_apk_mm(apk_path : str):
|
|
230
|
+
global _WORKER_APK_PATH, _WORKER_APK_FP, _WORKER_APK_MM
|
|
231
|
+
|
|
232
|
+
mm = _WORKER_APK_MM
|
|
233
|
+
if mm is not None and _WORKER_APK_PATH == apk_path:
|
|
234
|
+
return mm
|
|
235
|
+
|
|
236
|
+
_close_worker_apk()
|
|
237
|
+
fp = open(apk_path, "rb")
|
|
238
|
+
mm = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ)
|
|
239
|
+
_WORKER_APK_PATH = apk_path
|
|
240
|
+
_WORKER_APK_FP = fp
|
|
241
|
+
_WORKER_APK_MM = mm
|
|
242
|
+
return mm
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _inflate_deflate_chunks(comp_view, stop_event):
|
|
246
|
+
if stop_event is None:
|
|
247
|
+
# nothing to cancel: hand zlib the whole stream in one call instead of
|
|
248
|
+
# re-entering it every _DEFLATE_CHUNK bytes (measured ~4% faster per dex)
|
|
249
|
+
return zlib.decompress(comp_view, -15)
|
|
250
|
+
decomp = zlib.decompressobj(-15)
|
|
251
|
+
out = bytearray()
|
|
252
|
+
pos = 0
|
|
253
|
+
total = len(comp_view)
|
|
254
|
+
while pos < total:
|
|
255
|
+
if stop_event.is_set():
|
|
256
|
+
return None
|
|
257
|
+
end = min(pos + _DEFLATE_CHUNK, total)
|
|
258
|
+
out.extend(decomp.decompress(comp_view[pos:end]))
|
|
259
|
+
pos = end
|
|
260
|
+
out.extend(decomp.flush())
|
|
261
|
+
if stop_event.is_set():
|
|
262
|
+
return None
|
|
263
|
+
return bytes(out)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _inflate_dex(mm : mmap.mmap, entry, stop_event = None):
|
|
267
|
+
name, uncomp_size, comp_size, local_header_off, comp_method = entry
|
|
268
|
+
if mm[local_header_off:local_header_off + 4] != _LH_SIG:
|
|
269
|
+
raise ValueError("bad local header signature")
|
|
270
|
+
|
|
271
|
+
name_len = _U16_FROM(mm, local_header_off + 26)[0]
|
|
272
|
+
extra_len = _U16_FROM(mm, local_header_off + 28)[0]
|
|
273
|
+
data_off = local_header_off + 30 + name_len + extra_len
|
|
274
|
+
comp_view = memoryview(mm)[data_off:data_off + comp_size]
|
|
275
|
+
|
|
276
|
+
if comp_method == 0:
|
|
277
|
+
data = bytes(comp_view)
|
|
278
|
+
elif comp_method == 8:
|
|
279
|
+
data = _inflate_deflate_chunks(comp_view, stop_event)
|
|
280
|
+
if data is None:
|
|
281
|
+
return None
|
|
282
|
+
else:
|
|
283
|
+
raise ValueError(f"unsupported compression method: {comp_method}")
|
|
284
|
+
|
|
285
|
+
if uncomp_size and len(data) != uncomp_size:
|
|
286
|
+
raise ValueError(f"size mismatch: expect {uncomp_size}, got {len(data)}")
|
|
287
|
+
return data
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _findrefs_worker(apk_path : str, entry, find_type : str, find : dict, aggregate : bool = True):
|
|
291
|
+
from droidasc.asc_client.asc_handler import AscHandler
|
|
292
|
+
|
|
293
|
+
mm = _get_worker_apk_mm(apk_path)
|
|
294
|
+
t0 = time.perf_counter()
|
|
295
|
+
data = _inflate_dex(mm, entry)
|
|
296
|
+
t1 = time.perf_counter()
|
|
297
|
+
lines = []
|
|
298
|
+
handler = AscHandler(False)
|
|
299
|
+
for dex_name, dex_buf in iter_logical_dex_buffers(entry[0], data):
|
|
300
|
+
lines.extend(handler.findrefs(dex_name, dex_buf, find_type, find, aggregate=aggregate))
|
|
301
|
+
t2 = time.perf_counter()
|
|
302
|
+
return (
|
|
303
|
+
entry[0],
|
|
304
|
+
lines,
|
|
305
|
+
(t1 - t0) * 1000000,
|
|
306
|
+
(t2 - t1) * 1000000,
|
|
307
|
+
os.getpid(),
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _inflate_and_hit(mm : mmap.mmap, entry, target_bytes : bytes, stop_event : threading.Event, log):
|
|
312
|
+
tid = threading.get_ident() & 0xFFFF
|
|
313
|
+
name = entry[0]
|
|
314
|
+
if stop_event.is_set():
|
|
315
|
+
return False, None, None
|
|
316
|
+
|
|
317
|
+
t0 = time.perf_counter()
|
|
318
|
+
data = _inflate_dex(mm, entry, stop_event)
|
|
319
|
+
if data is None:
|
|
320
|
+
return False, None, None
|
|
321
|
+
t1 = time.perf_counter()
|
|
322
|
+
if stop_event.is_set():
|
|
323
|
+
return False, None, None
|
|
324
|
+
|
|
325
|
+
hit = False
|
|
326
|
+
hit_name = None
|
|
327
|
+
hit_data = None
|
|
328
|
+
for dex_name, dex_buf in iter_logical_dex_buffers(name, data):
|
|
329
|
+
if stop_event.is_set():
|
|
330
|
+
return False, None, None
|
|
331
|
+
if _dex_defines_class(dex_buf, target_bytes):
|
|
332
|
+
hit = True
|
|
333
|
+
hit_name = dex_name
|
|
334
|
+
hit_data = dex_buf
|
|
335
|
+
break
|
|
336
|
+
t2 = time.perf_counter()
|
|
337
|
+
if hit:
|
|
338
|
+
stop_event.set()
|
|
339
|
+
log(
|
|
340
|
+
f"[APK] [T{tid:04x}] '{name}' inflate={(t1 - t0) * 1000000:.2f} us "
|
|
341
|
+
f"lookup={(t2 - t1) * 1000000:.2f} us hit={hit}"
|
|
342
|
+
)
|
|
343
|
+
return hit, hit_name, hit_data
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class ApkHandler:
|
|
347
|
+
def __init__(self, apk_path : str, debug : bool = False, max_workers : int = 8):
|
|
348
|
+
self.apk_path = apk_path
|
|
349
|
+
self.debug = debug
|
|
350
|
+
self.max_workers = max_workers
|
|
351
|
+
|
|
352
|
+
def _log(self, msg : str):
|
|
353
|
+
if self.debug:
|
|
354
|
+
print(msg)
|
|
355
|
+
|
|
356
|
+
def _open_apk(self):
|
|
357
|
+
fp = open(self.apk_path, "rb")
|
|
358
|
+
mm = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ)
|
|
359
|
+
return fp, mm
|
|
360
|
+
|
|
361
|
+
def get_class_dex(self, dalvik_class : str):
|
|
362
|
+
target_bytes = dalvik_class.encode("utf-8")
|
|
363
|
+
t_start = time.perf_counter()
|
|
364
|
+
fp, mm = self._open_apk()
|
|
365
|
+
try:
|
|
366
|
+
entries = _parse_cd_dex_entries(mm)
|
|
367
|
+
entries.sort(key=lambda x: x[2])
|
|
368
|
+
if self.debug:
|
|
369
|
+
t_ready = time.perf_counter()
|
|
370
|
+
self._log(f"[APK] scan setup={(t_ready - t_start) * 1000000:.2f} us entries={len(entries)}")
|
|
371
|
+
if not entries:
|
|
372
|
+
return None
|
|
373
|
+
|
|
374
|
+
stop_event = threading.Event()
|
|
375
|
+
hit_name = None
|
|
376
|
+
hit_data = None
|
|
377
|
+
cap = min(len(entries), max(6, min(self.max_workers, 12)))
|
|
378
|
+
|
|
379
|
+
with ThreadPoolExecutor(max_workers=self.max_workers) as ex:
|
|
380
|
+
inflight = {}
|
|
381
|
+
idx = 0
|
|
382
|
+
while idx < len(entries) or inflight:
|
|
383
|
+
while idx < len(entries) and len(inflight) < cap and not stop_event.is_set():
|
|
384
|
+
entry = entries[idx]
|
|
385
|
+
idx += 1
|
|
386
|
+
fut = ex.submit(_inflate_and_hit, mm, entry, target_bytes, stop_event, self._log)
|
|
387
|
+
inflight[fut] = entry
|
|
388
|
+
|
|
389
|
+
if not inflight:
|
|
390
|
+
break
|
|
391
|
+
|
|
392
|
+
done, _pending = wait(list(inflight.keys()), return_when=FIRST_COMPLETED)
|
|
393
|
+
for fut in done:
|
|
394
|
+
entry = inflight.pop(fut)
|
|
395
|
+
ok, dex_name, data = fut.result()
|
|
396
|
+
if ok:
|
|
397
|
+
hit_name = dex_name
|
|
398
|
+
hit_data = data
|
|
399
|
+
stop_event.set()
|
|
400
|
+
break
|
|
401
|
+
|
|
402
|
+
if hit_name is not None:
|
|
403
|
+
for fut in inflight:
|
|
404
|
+
fut.cancel()
|
|
405
|
+
break
|
|
406
|
+
|
|
407
|
+
if self.debug:
|
|
408
|
+
t_end = time.perf_counter()
|
|
409
|
+
self._log(f"[APK] getclass total={(t_end - t_start) * 1000000:.2f} us")
|
|
410
|
+
if hit_name is None:
|
|
411
|
+
return None
|
|
412
|
+
return hit_name, hit_data
|
|
413
|
+
finally:
|
|
414
|
+
mm.close()
|
|
415
|
+
fp.close()
|
|
416
|
+
|
|
417
|
+
def for_each_findrefs(self, find_type : str, find : dict):
|
|
418
|
+
# imported before the timer so this once-per-process import is not charged to a
|
|
419
|
+
# single search; it used to happen at apk_handler import time, and keeping the
|
|
420
|
+
# getclass path (thread pool only) from paying for it is worth ~8ms of startup
|
|
421
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
422
|
+
|
|
423
|
+
t_start = time.perf_counter()
|
|
424
|
+
fp, mm = self._open_apk()
|
|
425
|
+
try:
|
|
426
|
+
entries = _parse_cd_dex_entries(mm)
|
|
427
|
+
entries.sort(key=lambda x: x[2])
|
|
428
|
+
finally:
|
|
429
|
+
mm.close()
|
|
430
|
+
fp.close()
|
|
431
|
+
|
|
432
|
+
if not entries:
|
|
433
|
+
return
|
|
434
|
+
|
|
435
|
+
# a forked child inherits whatever is still sitting in the parent's stdio
|
|
436
|
+
# buffers; flush first so it cannot be emitted a second time on exit
|
|
437
|
+
sys.stdout.flush()
|
|
438
|
+
sys.stderr.flush()
|
|
439
|
+
with ProcessPoolExecutor(max_workers=self.max_workers,
|
|
440
|
+
mp_context=_process_pool_context()) as ex:
|
|
441
|
+
futures = {}
|
|
442
|
+
for entry in entries:
|
|
443
|
+
fut = ex.submit(_findrefs_worker, self.apk_path, entry, find_type, find)
|
|
444
|
+
futures[fut] = entry[0]
|
|
445
|
+
|
|
446
|
+
while futures:
|
|
447
|
+
done, _pending = wait(list(futures.keys()), return_when=FIRST_COMPLETED)
|
|
448
|
+
for fut in done:
|
|
449
|
+
futures.pop(fut)
|
|
450
|
+
dex_name, lines, inflate_us, process_us, pid = fut.result()
|
|
451
|
+
if self.debug:
|
|
452
|
+
self._log(
|
|
453
|
+
f"[APK] [P{pid}] '{dex_name}' inflate={inflate_us:.2f} us "
|
|
454
|
+
f"process={process_us:.2f} us"
|
|
455
|
+
)
|
|
456
|
+
yield dex_name, lines
|
|
457
|
+
|
|
458
|
+
if self.debug:
|
|
459
|
+
t_end = time.perf_counter()
|
|
460
|
+
self._log(
|
|
461
|
+
f"[APK] for_each_findrefs total={(t_end - t_start) * 1000000:.2f} us "
|
|
462
|
+
f"count={len(entries)} workers={self.max_workers}"
|
|
463
|
+
)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import importlib.util
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import types
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_DexManager = None
|
|
10
|
+
_FindRefManager = None
|
|
11
|
+
_decompile_dex_bytes = None
|
|
12
|
+
_DEX = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _install_pure_python_mutf8_shim():
|
|
16
|
+
if "mutf8.cmutf8" in sys.modules:
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
pkg_spec = importlib.util.find_spec("mutf8")
|
|
20
|
+
if pkg_spec is None or not pkg_spec.submodule_search_locations:
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
pkg_dir = pkg_spec.submodule_search_locations[0]
|
|
24
|
+
py_impl = os.path.join(pkg_dir, "mutf8.py")
|
|
25
|
+
mod_spec = importlib.util.spec_from_file_location("_asc_client_mutf8_py", py_impl)
|
|
26
|
+
if mod_spec is None or mod_spec.loader is None:
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
module = importlib.util.module_from_spec(mod_spec)
|
|
30
|
+
mod_spec.loader.exec_module(module)
|
|
31
|
+
|
|
32
|
+
shim = types.ModuleType("mutf8.cmutf8")
|
|
33
|
+
shim.decode_modified_utf8 = module.decode_modified_utf8
|
|
34
|
+
shim.encode_modified_utf8 = module.encode_modified_utf8
|
|
35
|
+
sys.modules["mutf8.cmutf8"] = shim
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _lazy_import():
|
|
39
|
+
global _DexManager, _FindRefManager, _decompile_dex_bytes, _DEX
|
|
40
|
+
if _DexManager is not None:
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
_install_pure_python_mutf8_shim()
|
|
44
|
+
from droidasc.asc_core.core.dex.dex_manager import DexManager
|
|
45
|
+
from droidasc.asc_core.findrefs.findrefs_manager import FindRefManager
|
|
46
|
+
from droidasc.asc_core.utils.tinydex import DEX
|
|
47
|
+
from droidasc.asc_core.utils.decompiler import decompile_dex_bytes
|
|
48
|
+
|
|
49
|
+
_DexManager = DexManager
|
|
50
|
+
_FindRefManager = FindRefManager
|
|
51
|
+
_decompile_dex_bytes = decompile_dex_bytes
|
|
52
|
+
_DEX = DEX
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class AscHandler:
|
|
56
|
+
def __init__(self, debug : bool = False):
|
|
57
|
+
self.debug = debug
|
|
58
|
+
|
|
59
|
+
def getclass(self, dex_buf : bytes, dalvik_class : str) -> str:
|
|
60
|
+
_lazy_import()
|
|
61
|
+
manager = _DexManager(memoryview(dex_buf), debug=self.debug)
|
|
62
|
+
new_dex_bytes = manager.extract_and_rebuild(dalvik_class)
|
|
63
|
+
return _decompile_dex_bytes(new_dex_bytes, dalvik_class)
|
|
64
|
+
|
|
65
|
+
def _format_method(self, dex, midx : int) -> str:
|
|
66
|
+
method = dex.methods[midx]
|
|
67
|
+
return f"{method.cls.fullname}->{method.name}"
|
|
68
|
+
|
|
69
|
+
def _format_matched_name(self, dex, find_type : str, idx : int) -> str:
|
|
70
|
+
if find_type == "string":
|
|
71
|
+
return str(dex.strings[idx])
|
|
72
|
+
if find_type == "type":
|
|
73
|
+
return dex.types[idx].descriptor
|
|
74
|
+
if find_type == "method":
|
|
75
|
+
method = dex.methods[idx]
|
|
76
|
+
return f"{method.cls.fullname}->{method.name}"
|
|
77
|
+
field = dex.fields[idx]
|
|
78
|
+
return f"{field.cls.fullname}->{field.name}"
|
|
79
|
+
|
|
80
|
+
def findrefs(self, dex_name : str, dex_buf : bytes, find_type : str, find : dict, aggregate : bool = True) -> list:
|
|
81
|
+
from droidasc.asc_core.findrefs.findrefs_manager import FindRefManager
|
|
82
|
+
from droidasc.asc_core.utils.tinydex import DEX
|
|
83
|
+
|
|
84
|
+
dex = DEX.parse(memoryview(dex_buf), dex_name)
|
|
85
|
+
ref_manager = FindRefManager(dex)
|
|
86
|
+
query = copy.deepcopy(find)
|
|
87
|
+
matched_idxs = ref_manager.find_ref(query, True)
|
|
88
|
+
mids = query[find_type]
|
|
89
|
+
if not aggregate:
|
|
90
|
+
ret = []
|
|
91
|
+
for i in range(len(mids)):
|
|
92
|
+
midx = mids[i]
|
|
93
|
+
if midx is None:
|
|
94
|
+
continue
|
|
95
|
+
idx = matched_idxs[i]
|
|
96
|
+
if isinstance(midx, list):
|
|
97
|
+
midxs = midx
|
|
98
|
+
else:
|
|
99
|
+
midxs = [midx]
|
|
100
|
+
matched = self._format_matched_name(dex, find_type, idx)
|
|
101
|
+
for mid in midxs:
|
|
102
|
+
ret.append(
|
|
103
|
+
f"{dex_name} | {self._format_method(dex, mid)} | matched=({matched})"
|
|
104
|
+
)
|
|
105
|
+
return ret
|
|
106
|
+
|
|
107
|
+
grouped = defaultdict(set)
|
|
108
|
+
for i in range(len(mids)):
|
|
109
|
+
midx = mids[i]
|
|
110
|
+
if midx is None:
|
|
111
|
+
continue
|
|
112
|
+
idx = matched_idxs[i]
|
|
113
|
+
if isinstance(midx, list):
|
|
114
|
+
midxs = midx
|
|
115
|
+
else:
|
|
116
|
+
midxs = [midx]
|
|
117
|
+
for mid in midxs:
|
|
118
|
+
grouped[mid].add(idx)
|
|
119
|
+
|
|
120
|
+
ret = []
|
|
121
|
+
for mid in sorted(grouped):
|
|
122
|
+
matched = "; ".join(
|
|
123
|
+
self._format_matched_name(dex, find_type, idx)
|
|
124
|
+
for idx in sorted(grouped[mid])
|
|
125
|
+
)
|
|
126
|
+
ret.append(
|
|
127
|
+
f"{dex_name} | {self._format_method(dex, mid)} | matched=({matched})"
|
|
128
|
+
)
|
|
129
|
+
return ret
|