#!/usr/bin/env python3 # -*- coding: utf-8 -*- """libweapon.so 纯静态 OLLVM/kcode-guard 初筛工具。 用途: - 不运行目标 so,不依赖 Frida / 抓包 / 真机。 - 用 ELF 元数据 + Capstone 反汇编做轻量画像: 1. 节区、符号、导入、熵; 2. JNI_OnLoad 周边反汇编; 3. br/blr/adrp/movk 等 OLLVM/间接调度指标; 4. .data/.data.rel.ro 中连续指向 .text 的指针表候选; 5. 明文字符串及近似 ADRP 直连引用。 运行示例: uv run --with capstone --with pyelftools python -m tools.analyze_libweapon_ollvm \ out/p0_64_extract/lib/arm64-v8a/libweapon.2.2174a68a..so \ --json-out out/libweapon_ollvm_report.json \ --md-out out/libweapon_ollvm_report.md """ from __future__ import annotations import argparse import collections import hashlib import json import math import re import struct from dataclasses import asdict, dataclass, field from pathlib import Path from typing import Any, Iterable from capstone import Cs, CS_ARCH_ARM64, CS_MODE_ARM from elftools.elf.elffile import ELFFile TARGET_STRINGS = [ "VIMG_", "a_y_q_z", "gifshow", "wcfg", "putString", "getString", "getSharedPreferences", "RegisterNatives", "JNI_OnLoad", "KsBridge", "Engine", "/NP/vmp", ] @dataclass(frozen=True) class SectionInfo: name: str addr: int offset: int size: int flags: str entropy: float @dataclass(frozen=True) class SymbolInfo: name: str value: int size: int section: str bind: str typ: str @dataclass(frozen=True) class StringHit: pattern: str offset: int vaddr: int text: str direct_adrp_refs: list[int] = field(default_factory=list) @dataclass(frozen=True) class PointerRun: section: str file_offset: int vaddr: int count: int targets: list[int] @dataclass(frozen=True) class DisasmLine: address: int mnemonic: str op_str: str @dataclass(frozen=True) class PltXref: name: str plt: int calls: list[int] @dataclass(frozen=True) class OllvmMetrics: text_instruction_count: int branch_count: int indirect_branch_count: int call_count: int indirect_call_count: int ret_count: int adrp_count: int movk_count: int csel_family_count: int cond_branch_count: int suspicious_dispatch_windows: list[dict[str, Any]] mnemonic_top: list[tuple[str, int]] @dataclass(frozen=True) class Report: path: str size: int sha256: str elf_machine: str entry: int sections: list[SectionInfo] defined_function_count: int imported_function_count: int exports_interesting: list[SymbolInfo] imports_interesting: list[SymbolInfo] string_hits: list[StringHit] pointer_runs: list[PointerRun] plt_xrefs_interesting: list[PltXref] jni_onload: SymbolInfo | None jni_onload_disasm: list[DisasmLine] ollvm_metrics: OllvmMetrics def _entropy(data: bytes) -> float: if not data: return 0.0 counts = collections.Counter(data) total = len(data) return -sum((n / total) * math.log2(n / total) for n in counts.values()) def _section_flags(sec) -> str: flags = int(sec["sh_flags"]) out = [] if flags & 0x1: out.append("W") if flags & 0x2: out.append("A") if flags & 0x4: out.append("X") return "".join(out) or "-" class ElfView: def __init__(self, path: Path): self.path = path self.data = path.read_bytes() self.elf = ELFFile(path.open("rb")) self.sections = [sec for sec in self.elf.iter_sections()] self.load_segments = [seg for seg in self.elf.iter_segments() if seg["p_type"] == "PT_LOAD"] self.rela_addends = self._load_rela_addends() def _load_rela_addends(self) -> dict[int, int]: """返回 r_offset -> r_addend,用于识别重定位写入的调度表。""" out: dict[int, int] = {} for sec in self.sections: if not sec.name.startswith(".rela"): continue try: for rel in sec.iter_relocations(): if rel.is_RELA(): out[int(rel["r_offset"])] = int(rel["r_addend"]) except Exception: continue return out def section_by_name(self, name: str): return self.elf.get_section_by_name(name) def vaddr_to_offset(self, vaddr: int) -> int | None: for seg in self.load_segments: start = int(seg["p_vaddr"]) end = start + int(seg["p_filesz"]) if start <= vaddr < end: return int(seg["p_offset"]) + (vaddr - start) return None def offset_to_vaddr(self, offset: int) -> int | None: for seg in self.load_segments: start = int(seg["p_offset"]) end = start + int(seg["p_filesz"]) if start <= offset < end: return int(seg["p_vaddr"]) + (offset - start) return None def bytes_at_vaddr(self, vaddr: int, size: int) -> bytes: off = self.vaddr_to_offset(vaddr) if off is None: return b"" return self.data[off : off + size] def _iter_symbols(view: ElfView) -> Iterable[SymbolInfo]: for sec_name in (".dynsym", ".symtab"): sec = view.section_by_name(sec_name) if sec is None: continue for sym in sec.iter_symbols(): name = sym.name if not name: continue info = sym["st_info"] yield SymbolInfo( name=name, value=int(sym["st_value"]), size=int(sym["st_size"]), section="UND" if sym["st_shndx"] == "SHN_UNDEF" else sec_name, bind=str(info["bind"]), typ=str(info["type"]), ) def _extract_ascii_strings(data: bytes, min_len: int = 4) -> list[tuple[int, str]]: regex = rb"[ -~]{" + str(min_len).encode("ascii") + rb",}" return [(m.start(), m.group(0).decode("ascii", errors="replace")) for m in re.finditer(regex, data)] def _disasm_section(view: ElfView, name: str) -> list[Any]: sec = view.section_by_name(name) if sec is None: return [] md = Cs(CS_ARCH_ARM64, CS_MODE_ARM) md.detail = True md.skipdata = True return [ins for ins in md.disasm(sec.data(), int(sec["sh_addr"])) if ins.mnemonic != ".byte"] def _disasm_window(view: ElfView, vaddr: int, size: int) -> list[DisasmLine]: md = Cs(CS_ARCH_ARM64, CS_MODE_ARM) md.detail = False lines = [] for ins in md.disasm(view.bytes_at_vaddr(vaddr, size), vaddr): lines.append(DisasmLine(address=int(ins.address), mnemonic=ins.mnemonic, op_str=ins.op_str)) return lines def _adrp_ref_map(text_insns: list[Any]) -> dict[int, list[int]]: """近似收集 ADRP 目标页 -> 指令地址。 Capstone 对 ARM64 ADRP 的第二操作数通常给出归一后的页地址。这里用页粒度 做粗筛,适合回答“明文字符串是否有直连 ADRP 引用”。 """ refs: dict[int, list[int]] = collections.defaultdict(list) for ins in text_insns: if ins.mnemonic != "adrp": continue try: if len(ins.operands) >= 2 and hasattr(ins.operands[1], "imm"): refs[int(ins.operands[1].imm) & ~0xFFF].append(int(ins.address)) except Exception: continue return refs def _string_hits(view: ElfView, text_insns: list[Any]) -> list[StringHit]: refs_by_page = _adrp_ref_map(text_insns) hits: list[StringHit] = [] for off, text in _extract_ascii_strings(view.data): matched = [pat for pat in TARGET_STRINGS if pat.lower() in text.lower()] if not matched: continue vaddr = view.offset_to_vaddr(off) or 0 page = vaddr & ~0xFFF refs = refs_by_page.get(page, []) for pat in matched: hits.append( StringHit( pattern=pat, offset=off, vaddr=vaddr, text=text[:240], direct_adrp_refs=refs[:32], ) ) return hits def _pointer_runs(view: ElfView, min_run: int = 4) -> list[PointerRun]: text = view.section_by_name(".text") if text is None: return [] text_start = int(text["sh_addr"]) text_end = text_start + int(text["sh_size"]) candidates: list[PointerRun] = [] # kcode-guard 在 JNI_OnLoad 里使用: # ldr x8, [table, idx, uxtw #3] # add x8, x8, #BASE_CONST # br x8 # 表项不是裸指针,而是 signed/unsigned 64-bit delta。这里同时尝试直连 # 指针和常见 delta base,先覆盖当前样本的 0x7c585548。 delta_bases = [0, 0x7C585548] for sec_name in (".data.rel.ro", ".data", ".bss.rel.ro", ".got", ".got.plt"): sec = view.section_by_name(sec_name) if sec is None or int(sec["sh_size"]) < 8: continue data = sec.data() run_targets: list[int] = [] run_start_idx = 0 def flush(idx: int) -> None: nonlocal run_targets, run_start_idx if len(run_targets) >= min_run: file_off = int(sec["sh_offset"]) + run_start_idx * 8 vaddr = int(sec["sh_addr"]) + run_start_idx * 8 candidates.append( PointerRun( section=sec_name, file_offset=file_off, vaddr=vaddr, count=len(run_targets), targets=run_targets[:64], ) ) run_targets = [] for idx in range(0, len(data) // 8): raw = struct.unpack_from(" OllvmMetrics: mnemonic_counter = collections.Counter(ins.mnemonic for ins in text_insns) branch_mnemonics = {"b", "b.eq", "b.ne", "b.cs", "b.hs", "b.cc", "b.lo", "b.mi", "b.pl", "b.vs", "b.vc", "b.hi", "b.ls", "b.ge", "b.lt", "b.gt", "b.le", "cbz", "cbnz", "tbz", "tbnz"} cond_branch_count = sum(1 for ins in text_insns if ins.mnemonic in branch_mnemonics and ins.mnemonic != "b") indirect = [ins for ins in text_insns if ins.mnemonic == "br"] indirect_call = [ins for ins in text_insns if ins.mnemonic == "blr"] branches = [ins for ins in text_insns if ins.mnemonic.startswith("b") or ins.mnemonic in {"cbz", "cbnz", "tbz", "tbnz"}] calls = [ins for ins in text_insns if ins.mnemonic in {"bl", "blr"}] csel_family = [ins for ins in text_insns if ins.mnemonic in {"csel", "csinc", "csinv", "csneg", "cset", "csetm"}] suspicious: list[dict[str, Any]] = [] for i, ins in enumerate(text_insns): if ins.mnemonic != "br": continue window = text_insns[max(0, i - 8) : i + 1] if any(w.mnemonic == "ldr" and "x8" in w.op_str for w in window) or any(w.mnemonic == "movk" for w in window): suspicious.append( { "br": int(ins.address), "window": [ {"address": int(w.address), "mnemonic": w.mnemonic, "op_str": w.op_str} for w in window ], } ) return OllvmMetrics( text_instruction_count=len(text_insns), branch_count=len(branches), indirect_branch_count=len(indirect), call_count=len(calls), indirect_call_count=len(indirect_call), ret_count=mnemonic_counter.get("ret", 0), adrp_count=mnemonic_counter.get("adrp", 0), movk_count=mnemonic_counter.get("movk", 0), csel_family_count=len(csel_family), cond_branch_count=cond_branch_count, suspicious_dispatch_windows=suspicious[:80], mnemonic_top=mnemonic_counter.most_common(40), ) def _plt_symbol_map(view: ElfView) -> dict[int, str]: """AArch64 PLT 地址 -> 导入符号名。 当前样本 `.plt` 大小满足:PLT0(0x20) + N * 0x10。 `.rela.plt` 的第 i 项通常对应 `.plt + 0x20 + i*0x10`。 """ plt = view.section_by_name(".plt") rela_plt = view.section_by_name(".rela.plt") dynsym = view.section_by_name(".dynsym") if plt is None or rela_plt is None or dynsym is None: return {} out: dict[int, str] = {} base = int(plt["sh_addr"]) + 0x20 for idx, rel in enumerate(rela_plt.iter_relocations()): sym_idx = rel["r_info_sym"] sym = dynsym.get_symbol(sym_idx) if sym and sym.name: out[base + idx * 0x10] = sym.name return out def _plt_xrefs(view: ElfView, text_insns: list[Any]) -> list[PltXref]: plt_map = _plt_symbol_map(view) calls_by_plt: dict[int, list[int]] = collections.defaultdict(list) for ins in text_insns: if ins.mnemonic != "bl": continue try: if ins.operands and hasattr(ins.operands[0], "imm"): target = int(ins.operands[0].imm) if target in plt_map: calls_by_plt[target].append(int(ins.address)) except Exception: continue interesting = re.compile( r"mprotect|mmap|munmap|dlopen|dlsym|deflate|inflate|open|fopen|read|" r"clock|gettimeofday|system_property|socket|connect|send|recv", re.I, ) rows = [ PltXref(name=plt_map[addr], plt=addr, calls=calls[:120]) for addr, calls in sorted(calls_by_plt.items(), key=lambda item: plt_map[item[0]]) if interesting.search(plt_map[addr]) or calls ] return rows def analyze(path: Path) -> Report: view = ElfView(path) text_insns = _disasm_section(view, ".text") all_symbols = list(_iter_symbols(view)) defined_funcs = [s for s in all_symbols if s.typ == "STT_FUNC" and s.section != "UND"] imported_funcs = [s for s in all_symbols if s.typ == "STT_FUNC" and s.section == "UND"] interesting_re = re.compile(r"JNI|Java_|RegisterNatives|KsBridge|Engine|SharedPreferences|putString|getString|dlopen|dlsym|mmap|mprotect|deflate|inflate", re.I) jni = next((s for s in defined_funcs if s.name == "JNI_OnLoad"), None) jni_disasm = _disasm_window(view, jni.value, min(max(jni.size, 0x240), 0x800)) if jni else [] sections = [] for sec in view.sections: name = sec.name if not name: continue data = sec.data() if sec["sh_type"] != "SHT_NOBITS" else b"" sections.append( SectionInfo( name=name, addr=int(sec["sh_addr"]), offset=int(sec["sh_offset"]), size=int(sec["sh_size"]), flags=_section_flags(sec), entropy=round(_entropy(data), 4), ) ) return Report( path=str(path), size=len(view.data), sha256=hashlib.sha256(view.data).hexdigest(), elf_machine=str(view.elf["e_machine"]), entry=int(view.elf.header["e_entry"]), sections=sections, defined_function_count=len(defined_funcs), imported_function_count=len(imported_funcs), exports_interesting=[s for s in defined_funcs if interesting_re.search(s.name)][:120], imports_interesting=[s for s in imported_funcs if interesting_re.search(s.name)][:160], string_hits=_string_hits(view, text_insns), pointer_runs=_pointer_runs(view), plt_xrefs_interesting=_plt_xrefs(view, text_insns), jni_onload=jni, jni_onload_disasm=jni_disasm, ollvm_metrics=_ollvm_metrics(text_insns), ) def _hx(value: int) -> str: return f"0x{value:x}" def write_markdown(report: Report, path: Path) -> None: lines: list[str] = [] lines.append("# libweapon.so OLLVM 静态初筛报告") lines.append("") lines.append(f"- 文件:`{Path(report.path).name}`") lines.append(f"- 大小:{report.size} bytes") lines.append(f"- SHA256:`{report.sha256}`") lines.append(f"- 架构:{report.elf_machine}") lines.append(f"- Entry:`{_hx(report.entry)}`") lines.append(f"- 定义函数数:{report.defined_function_count}") lines.append(f"- 导入函数数:{report.imported_function_count}") lines.append("") lines.append("## OLLVM / kcode-guard 指标") m = report.ollvm_metrics lines.extend( [ f"- .text 指令数:{m.text_instruction_count}", f"- 分支数:{m.branch_count}", f"- 条件分支数:{m.cond_branch_count}", f"- `br` 间接跳转数:{m.indirect_branch_count}", f"- `blr` 间接调用数:{m.indirect_call_count}", f"- `movk` 常量拼装数:{m.movk_count}", f"- `adrp` 页寻址数:{m.adrp_count}", f"- 可疑 dispatch window 数:{len(m.suspicious_dispatch_windows)}", ] ) lines.append("") lines.append("## 指针表候选") if report.pointer_runs: for run in report.pointer_runs[:20]: target_preview = ", ".join(_hx(v) for v in run.targets[:8]) lines.append( f"- {run.section} `{_hx(run.vaddr)}` file `{_hx(run.file_offset)}` " f"count={run.count} targets={target_preview}" ) else: lines.append("- 未发现连续 .text 指针表候选") lines.append("") lines.append("## JNI_OnLoad 反汇编窗口") if report.jni_onload: lines.append(f"- `JNI_OnLoad`:`{_hx(report.jni_onload.value)}` size={report.jni_onload.size}") lines.append("```asm") for item in report.jni_onload_disasm[:180]: lines.append(f"{_hx(item.address)}: {item.mnemonic} {item.op_str}".rstrip()) lines.append("```") else: lines.append("- 未找到 JNI_OnLoad 导出") lines.append("") lines.append("## 关键字符串命中与直连引用") if report.string_hits: for hit in report.string_hits[:80]: refs = ", ".join(_hx(v) for v in hit.direct_adrp_refs[:8]) or "" lines.append( f"- `{hit.pattern}` vaddr=`{_hx(hit.vaddr)}` off=`{_hx(hit.offset)}` " f"adrp_refs={refs} text=`{hit.text}`" ) else: lines.append("- 未命中关键明文字符串") lines.append("") lines.append("## PLT 调用交叉引用") if report.plt_xrefs_interesting: for row in report.plt_xrefs_interesting: call_preview = ", ".join(_hx(v) for v in row.calls[:12]) or "" lines.append(f"- `{row.name}` plt=`{_hx(row.plt)}` calls={len(row.calls)} at {call_preview}") else: lines.append("- 未识别到直接 PLT 调用") lines.append("") lines.append("## 关键导入") if report.imports_interesting: for sym in report.imports_interesting: lines.append(f"- `{sym.name}`") else: lines.append("- 无关键导入命中") lines.append("") lines.append("## 高熵/可疑节区") for sec in sorted(report.sections, key=lambda s: s.entropy, reverse=True)[:16]: lines.append( f"- `{sec.name}` addr=`{_hx(sec.addr)}` off=`{_hx(sec.offset)}` " f"size=`{_hx(sec.size)}` flags={sec.flags} entropy={sec.entropy}" ) lines.append("") path.write_text("\n".join(lines) + "\n", encoding="utf-8") def main() -> int: parser = argparse.ArgumentParser(description="libweapon.so OLLVM/kcode-guard 静态初筛") parser.add_argument("so", type=Path) parser.add_argument("--json-out", type=Path, default=Path("out/libweapon_ollvm_report.json")) parser.add_argument("--md-out", type=Path, default=Path("out/libweapon_ollvm_report.md")) args = parser.parse_args() report = analyze(args.so) args.json_out.parent.mkdir(parents=True, exist_ok=True) args.json_out.write_text(json.dumps(asdict(report), ensure_ascii=False, indent=2), encoding="utf-8") write_markdown(report, args.md_out) print(f"path={Path(report.path).name} size={report.size} sha256={report.sha256[:16]}") print(f"defined_funcs={report.defined_function_count} imports={report.imported_function_count}") print( "ollvm " f"insns={report.ollvm_metrics.text_instruction_count} " f"br={report.ollvm_metrics.indirect_branch_count} " f"blr={report.ollvm_metrics.indirect_call_count} " f"movk={report.ollvm_metrics.movk_count} " f"dispatch_windows={len(report.ollvm_metrics.suspicious_dispatch_windows)}" ) if report.pointer_runs: top = report.pointer_runs[0] print(f"top_ptr_run={top.section}@0x{top.vaddr:x} count={top.count} first=0x{top.targets[0]:x}") print(f"json={args.json_out}") print(f"md={args.md_out}") return 0 if __name__ == "__main__": raise SystemExit(main())