diff --git a/sample/tests/test_android_pipeline.py b/sample/tests/test_android_pipeline.py index 430a4c4..4695c39 100644 --- a/sample/tests/test_android_pipeline.py +++ b/sample/tests/test_android_pipeline.py @@ -464,6 +464,44 @@ def _meta_per_path(path): f"expected warning mentioning libzbroken.so, got {result.warnings!r}", ) + def test_first_lief_failure_still_analyzes_later_libs(self): + """A broken lex-first .so must not hide valid libraries in all mode.""" + with tempfile.TemporaryDirectory() as td: + tdp = Path(td) + apk = _make_apk_with_lib( + tdp, + {"arm64-v8a": ["libaaa_broken.so", "libcrypto.so"]}, + ) + from venomhook.binary_meta import BinaryMetaError + + def _meta_per_path(path): + name = Path(path).name + if name == "libaaa_broken.so": + raise BinaryMetaError(f"synthetic parse failure for {name}") + if name == "libcrypto.so": + return _stub_binary_meta(str(path), ["JNI_OnLoad"]) + raise AssertionError(path) + + with mock.patch( + "venomhook.android_pipeline.extract_binary_meta", + side_effect=_meta_per_path, + ): + result = analyze_apk( + apk, + tdp / "work", + use_apktool=False, + use_jadx=False, + analyze_all_libs=True, + ) + + self.assertIsNotNone(result.so_meta) + self.assertEqual(result.so_meta.name, "libcrypto.so") + self.assertEqual(result.additional_so_metas, []) + self.assertTrue( + any("libaaa_broken.so" in w for w in result.warnings), + f"expected warning mentioning libaaa_broken.so, got {result.warnings!r}", + ) + class TestAnalyzeApkAbiSelection(unittest.TestCase): def test_explicit_abi(self): diff --git a/src/venomhook/android_pipeline.py b/src/venomhook/android_pipeline.py index 78bc12c..fe005df 100644 --- a/src/venomhook/android_pipeline.py +++ b/src/venomhook/android_pipeline.py @@ -360,7 +360,35 @@ def analyze_apk( ) # ----- Step 3: BinaryMeta of the .so (REQUIRED for JNI correlation) ----- - if so_path is not None: + if analyze_all_libs and extracted_paths: + parsed: list[tuple[Path, BinaryMeta]] = [] + failures: list[str] = [] + for candidate_path in extracted_paths: + try: + parsed.append((candidate_path, extract_binary_meta(candidate_path))) + except BinaryMetaError as e: + failures.append( + f"{candidate_path}에 대한 binary_meta 추출 실패 " + f"(계속 진행): {e}" + ) + + if parsed: + # Pick the first successfully parsed library as the legacy + # primary. A corrupt lexicographic first .so must not prevent + # --apk-lib all from analysing the remaining valid libraries. + so_path, so_meta = parsed[0] + for extra_path, extra_meta in parsed[1:]: + additional_so_metas.append(extra_meta) + additional_so_paths.append(str(extra_path)) + warnings.extend(failures) + else: + msg = "; ".join(failures) or ( + f"{apk}의 {selected_abi} ABI에서 분석 가능한 .so가 없습니다" + ) + if require_native: + raise AndroidPipelineError(msg) + warnings.append(f"{msg} — 네이티브 분석을 건너뜁니다") + elif so_path is not None: try: so_meta = extract_binary_meta(so_path) except BinaryMetaError as e: @@ -369,23 +397,6 @@ def analyze_apk( raise AndroidPipelineError(msg) from e warnings.append(f"{msg} — 네이티브 분석을 건너뜁니다") - if analyze_all_libs and so_meta is not None: - # Run lief on each remaining .so. A failure on a non-primary - # library is recorded as a warning and the rest of the - # analysis continues — losing one library's exports degrades - # JNI correlation but does not invalidate the report. - for extra_path in extracted_paths[1:]: - try: - extra_meta = extract_binary_meta(extra_path) - except BinaryMetaError as e: - warnings.append( - f"{extra_path}에 대한 binary_meta 추출 실패 " - f"(계속 진행): {e}" - ) - continue - additional_so_metas.append(extra_meta) - additional_so_paths.append(str(extra_path)) - # ----- Step 4: AndroidManifest decode (optional) ----- app_meta: Optional[AndroidAppMeta] = None if use_apktool: