292 lines
11 KiB
Python
292 lines
11 KiB
Python
#!/usr/bin/env python3
|
||
"""离线执行 ``libw.so`` 的 ``W.pr(99999, 2, ...)`` 作为差分 oracle。
|
||
|
||
该工具只服务于逆向和测试向量生成。生产代码不得依赖 Unicorn、ELF 样本或
|
||
本模块。运行示例:
|
||
|
||
uv run --with pyelftools --with unicorn \
|
||
python -m tools.libw_pr_oracle --payload PAYLOAD
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import struct
|
||
from pathlib import Path
|
||
from typing import Callable
|
||
|
||
|
||
DEFAULT_LIBRARY = Path(
|
||
"out/agent_apk_triage/apktool/lib/arm64-v8a/libw.so"
|
||
)
|
||
PR_ENTRY = 0xECB0
|
||
|
||
STACK_BASE = 0x70000000
|
||
STACK_SIZE = 0x400000
|
||
AUX_BASE = 0x71000000
|
||
AUX_SIZE = 0x20000
|
||
HEAP_BASE = 0x72000000
|
||
HEAP_SIZE = 0x1000000
|
||
|
||
|
||
class OracleError(RuntimeError):
|
||
"""离线 native oracle 执行失败。"""
|
||
|
||
|
||
def _java_modified_utf8(value: str) -> bytes:
|
||
utf16 = value.encode("utf-16-be", errors="surrogatepass")
|
||
output = bytearray()
|
||
for offset in range(0, len(utf16), 2):
|
||
code_unit = int.from_bytes(utf16[offset : offset + 2], "big")
|
||
if 0x01 <= code_unit <= 0x7F:
|
||
output.append(code_unit)
|
||
elif code_unit <= 0x7FF:
|
||
output.extend((0xC0 | (code_unit >> 6), 0x80 | (code_unit & 0x3F)))
|
||
else:
|
||
output.extend(
|
||
(
|
||
0xE0 | (code_unit >> 12),
|
||
0x80 | ((code_unit >> 6) & 0x3F),
|
||
0x80 | (code_unit & 0x3F),
|
||
)
|
||
)
|
||
return bytes(output)
|
||
|
||
|
||
def _java_utf16_length(value: str) -> int:
|
||
return len(value.encode("utf-16-be", errors="surrogatepass")) // 2
|
||
|
||
|
||
class LibwPrOracle:
|
||
def __init__(self, library: Path = DEFAULT_LIBRARY) -> None:
|
||
self.library = Path(library)
|
||
|
||
def generate(
|
||
self,
|
||
payload: str,
|
||
code_observer: Callable[[object, int], None] | None = None,
|
||
format_observer: Callable[[str, int], None] | None = None,
|
||
) -> str:
|
||
"""返回与 ``W.pr(99999, 2, len * 2, payload)`` 相同的 KAS。"""
|
||
|
||
try:
|
||
from elftools.elf.elffile import ELFFile
|
||
from unicorn import UC_ARCH_ARM64, UC_HOOK_CODE, UC_MODE_LITTLE_ENDIAN, Uc
|
||
from unicorn.arm64_const import (
|
||
UC_ARM64_REG_PC,
|
||
UC_ARM64_REG_SP,
|
||
UC_ARM64_REG_TPIDR_EL0,
|
||
UC_ARM64_REG_X0,
|
||
UC_ARM64_REG_X1,
|
||
UC_ARM64_REG_X2,
|
||
UC_ARM64_REG_X3,
|
||
UC_ARM64_REG_X4,
|
||
UC_ARM64_REG_X5,
|
||
UC_ARM64_REG_X30,
|
||
)
|
||
except ImportError as exc:
|
||
raise OracleError(
|
||
"需要通过 `uv run --with pyelftools --with unicorn` 运行 oracle"
|
||
) from exc
|
||
|
||
if not self.library.is_file():
|
||
raise OracleError(f"libw 样本不存在: {self.library}")
|
||
|
||
with self.library.open("rb") as stream:
|
||
elf = ELFFile(stream)
|
||
loads = [
|
||
segment
|
||
for segment in elf.iter_segments()
|
||
if segment["p_type"] == "PT_LOAD"
|
||
]
|
||
if not loads:
|
||
raise OracleError("ELF 不含 PT_LOAD 段")
|
||
|
||
page = 0x1000
|
||
image_end = max(
|
||
(int(segment["p_vaddr"]) + int(segment["p_memsz"]) + page - 1)
|
||
& -page
|
||
for segment in loads
|
||
)
|
||
emulator = Uc(UC_ARCH_ARM64, UC_MODE_LITTLE_ENDIAN)
|
||
emulator.mem_map(0, image_end)
|
||
for segment in loads:
|
||
emulator.mem_write(int(segment["p_vaddr"]), segment.data())
|
||
|
||
for section in elf.iter_sections():
|
||
if not section.name.startswith(".rela"):
|
||
continue
|
||
for relocation in section.iter_relocations():
|
||
if relocation.is_RELA():
|
||
emulator.mem_write(
|
||
int(relocation["r_offset"]),
|
||
struct.pack(
|
||
"<Q",
|
||
int(relocation["r_addend"]) & 0xFFFFFFFFFFFFFFFF,
|
||
),
|
||
)
|
||
|
||
rela_plt = elf.get_section_by_name(".rela.plt")
|
||
plt = elf.get_section_by_name(".plt")
|
||
if rela_plt is None or plt is None:
|
||
raise OracleError("ELF 缺少 .plt/.rela.plt")
|
||
dynsym = elf.get_section(rela_plt["sh_link"])
|
||
plt_base = int(plt["sh_addr"]) + 0x20
|
||
plt_names = {
|
||
plt_base + index * 0x10: dynsym.get_symbol(rel["r_info_sym"]).name
|
||
for index, rel in enumerate(rela_plt.iter_relocations())
|
||
}
|
||
|
||
emulator.mem_map(STACK_BASE, STACK_SIZE)
|
||
emulator.mem_map(AUX_BASE, AUX_SIZE)
|
||
emulator.mem_map(HEAP_BASE, HEAP_SIZE)
|
||
|
||
tls = AUX_BASE + 0x1000
|
||
env_address = AUX_BASE + 0x2000
|
||
jni_table = AUX_BASE + 0x3000
|
||
input_address = AUX_BASE + 0x8000
|
||
jni_stubs = AUX_BASE + 0x10000
|
||
sentinel = AUX_BASE + 0x1F000
|
||
payload_bytes = _java_modified_utf8(payload)
|
||
if len(payload_bytes) + 1 > 0x8000:
|
||
raise OracleError("oracle payload 超过 32767 字节")
|
||
|
||
emulator.mem_write(input_address, payload_bytes + b"\0")
|
||
emulator.mem_write(env_address, struct.pack("<Q", jni_table))
|
||
for index in range(240):
|
||
stub = jni_stubs + index * 4
|
||
emulator.mem_write(jni_table + index * 8, struct.pack("<Q", stub))
|
||
emulator.mem_write(stub, b"\xc0\x03\x5f\xd6") # ret
|
||
|
||
emulator.reg_write(UC_ARM64_REG_TPIDR_EL0, tls)
|
||
emulator.mem_write(tls + 0x28, struct.pack("<Q", 0x123456789ABCDEF0))
|
||
emulator.reg_write(UC_ARM64_REG_SP, STACK_BASE + STACK_SIZE - 0x10000)
|
||
emulator.reg_write(UC_ARM64_REG_X0, env_address)
|
||
emulator.reg_write(UC_ARM64_REG_X1, AUX_BASE + 0x7000)
|
||
emulator.reg_write(UC_ARM64_REG_X2, 99999)
|
||
emulator.reg_write(UC_ARM64_REG_X3, 2)
|
||
emulator.reg_write(UC_ARM64_REG_X4, _java_utf16_length(payload) * 2)
|
||
emulator.reg_write(UC_ARM64_REG_X5, input_address)
|
||
emulator.reg_write(UC_ARM64_REG_X30, sentinel)
|
||
|
||
heap_next = HEAP_BASE + 0x1000
|
||
digest = bytearray()
|
||
completed = False
|
||
|
||
def read_c_string(address: int, limit: int = 0x10000) -> bytes:
|
||
if address == 0:
|
||
return b""
|
||
data = bytes(emulator.mem_read(address, limit))
|
||
return data.split(b"\0", 1)[0]
|
||
|
||
def return_from_call(value: int = 0) -> None:
|
||
emulator.reg_write(UC_ARM64_REG_X0, value)
|
||
emulator.reg_write(
|
||
UC_ARM64_REG_PC,
|
||
emulator.reg_read(UC_ARM64_REG_X30),
|
||
)
|
||
|
||
def read_va_int(va_list: int) -> int:
|
||
stack_pointer, gr_top, _vr_top, gr_offs, _vr_offs = struct.unpack(
|
||
"<QQQii", bytes(emulator.mem_read(va_list, 32))
|
||
)
|
||
source = gr_top + gr_offs if gr_offs < 0 else stack_pointer
|
||
return struct.unpack("<Q", bytes(emulator.mem_read(source, 8)))[0]
|
||
|
||
def hook_code(_uc, address: int, _size: int, _user_data) -> None:
|
||
nonlocal heap_next, completed
|
||
|
||
if code_observer is not None:
|
||
code_observer(emulator, address)
|
||
|
||
if jni_stubs <= address < jni_stubs + 240 * 4:
|
||
index = (address - jni_stubs) // 4
|
||
if index == 169: # GetStringUTFChars
|
||
return_from_call(input_address)
|
||
return
|
||
raise OracleError(f"未实现的 JNI 调用槽位: {index}")
|
||
|
||
name = plt_names.get(address)
|
||
if name is None:
|
||
return
|
||
|
||
x0 = emulator.reg_read(UC_ARM64_REG_X0)
|
||
x1 = emulator.reg_read(UC_ARM64_REG_X1)
|
||
x2 = emulator.reg_read(UC_ARM64_REG_X2)
|
||
x3 = emulator.reg_read(UC_ARM64_REG_X3)
|
||
x4 = emulator.reg_read(UC_ARM64_REG_X4)
|
||
|
||
if name in {"malloc", "calloc"}:
|
||
size = x0 * x1 if name == "calloc" else x0
|
||
result = (heap_next + 15) & -16
|
||
heap_next = result + max(size, 16)
|
||
if heap_next > HEAP_BASE + HEAP_SIZE:
|
||
raise OracleError("oracle heap 耗尽")
|
||
emulator.mem_write(result, b"\0" * max(size, 1))
|
||
return_from_call(result)
|
||
return
|
||
if name == "free":
|
||
return_from_call()
|
||
return
|
||
if name == "memcpy":
|
||
emulator.mem_write(x0, bytes(emulator.mem_read(x1, x2)))
|
||
return_from_call(x0)
|
||
return
|
||
if name == "memset":
|
||
emulator.mem_write(x0, bytes([x1 & 0xFF]) * x2)
|
||
return_from_call(x0)
|
||
return
|
||
if name == "strlen":
|
||
return_from_call(len(read_c_string(x0)))
|
||
return
|
||
if name == "__vsprintf_chk":
|
||
format_string = read_c_string(x3).decode("ascii")
|
||
value = read_va_int(x4)
|
||
if format_observer is not None:
|
||
format_observer(format_string, value)
|
||
if format_string == "%08x ":
|
||
rendered = f"{value & 0xFFFFFFFF:08x} ".encode("ascii")
|
||
elif format_string == "%02x":
|
||
rendered = f"{value & 0xFF:02x}".encode("ascii")
|
||
digest.append(value & 0xFF)
|
||
else:
|
||
raise OracleError(
|
||
f"未实现的 __vsprintf_chk 格式: {format_string!r}"
|
||
)
|
||
emulator.mem_write(x0, rendered + b"\0")
|
||
return_from_call(len(rendered))
|
||
if len(digest) == 16:
|
||
completed = True
|
||
emulator.emu_stop()
|
||
return
|
||
if name == "__stack_chk_fail":
|
||
raise OracleError("native 栈保护校验失败")
|
||
raise OracleError(f"未实现的 PLT 调用: {name}")
|
||
|
||
emulator.hook_add(UC_HOOK_CODE, hook_code)
|
||
try:
|
||
emulator.emu_start(PR_ENTRY, sentinel, count=10_000_000)
|
||
except Exception as exc:
|
||
if isinstance(exc, OracleError):
|
||
raise
|
||
raise OracleError(
|
||
f"native 执行失败,PC={emulator.reg_read(UC_ARM64_REG_PC):#x}: {exc}"
|
||
) from exc
|
||
|
||
if not completed or len(digest) != 16:
|
||
raise OracleError(f"native 未产生完整摘要: {digest.hex()}")
|
||
return "00" + digest.hex()
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description=__doc__)
|
||
parser.add_argument("--library", type=Path, default=DEFAULT_LIBRARY)
|
||
parser.add_argument("--payload", required=True)
|
||
args = parser.parse_args()
|
||
print(LibwPrOracle(args.library).generate(args.payload))
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|