98 lines
3.3 KiB
Python
Executable File
98 lines
3.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Disassemble and (optionally) emulate raw shellcode.
|
|
|
|
``disasm`` uses Capstone to linearly disassemble a flat byte blob at a chosen architecture.
|
|
``emulate`` uses Unicorn to run the blob in an isolated emulator with bounded instruction
|
|
count, capturing executed addresses — useful for self-decoding stubs. Both operate in-process
|
|
on the emulator/disassembler; nothing runs on the real CPU as a process.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
def disasm(data: bytes, arch: str, base: int, limit: int = 200) -> list[dict]:
|
|
from capstone import ( # type: ignore
|
|
Cs, CS_ARCH_X86, CS_MODE_32, CS_MODE_64,
|
|
)
|
|
mode = CS_MODE_64 if arch == "x64" else CS_MODE_32
|
|
md = Cs(CS_ARCH_X86, mode)
|
|
out = []
|
|
for i, insn in enumerate(md.disasm(data, base)):
|
|
if i >= limit:
|
|
break
|
|
out.append({"addr": hex(insn.address), "mnemonic": insn.mnemonic,
|
|
"op_str": insn.op_str})
|
|
return out
|
|
|
|
|
|
def emulate(data: bytes, arch: str, base: int, max_insns: int = 5000) -> dict:
|
|
from unicorn import ( # type: ignore
|
|
Uc, UC_ARCH_X86, UC_MODE_32, UC_MODE_64, UcError,
|
|
)
|
|
from unicorn.x86_const import UC_X86_REG_RIP, UC_X86_REG_EIP # type: ignore
|
|
|
|
mode = UC_MODE_64 if arch == "x64" else UC_MODE_32
|
|
mu = Uc(UC_ARCH_X86, mode)
|
|
region = 0x10 * 1024 * 1024
|
|
map_base = base & ~0xFFF
|
|
mu.mem_map(map_base, region)
|
|
mu.mem_write(base, data)
|
|
|
|
executed: list[int] = []
|
|
|
|
def hook_code(uc, address, size, user_data): # noqa: ANN001
|
|
executed.append(address)
|
|
if len(executed) >= max_insns:
|
|
uc.emu_stop()
|
|
|
|
from unicorn import UC_HOOK_CODE # type: ignore
|
|
mu.hook_add(UC_HOOK_CODE, hook_code)
|
|
error = None
|
|
try:
|
|
mu.emu_start(base, base + len(data), count=max_insns)
|
|
except UcError as exc:
|
|
error = str(exc)
|
|
reg = UC_X86_REG_RIP if arch == "x64" else UC_X86_REG_EIP
|
|
return {
|
|
"instructions_executed": len(executed),
|
|
"first_addrs": [hex(a) for a in executed[:20]],
|
|
"stopped_at": hex(mu.reg_read(reg)),
|
|
"error": error,
|
|
}
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description="Shellcode disasm/emulate")
|
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
|
|
p_d = sub.add_parser("disasm")
|
|
p_d.add_argument("file")
|
|
p_d.add_argument("--arch", choices=["x86", "x64"], default="x64")
|
|
p_d.add_argument("--base", default="0x1000")
|
|
|
|
p_e = sub.add_parser("emulate")
|
|
p_e.add_argument("file")
|
|
p_e.add_argument("--arch", choices=["x86", "x64"], default="x64")
|
|
p_e.add_argument("--base", default="0x1000")
|
|
|
|
args = parser.parse_args(argv)
|
|
data = Path(args.file).read_bytes()
|
|
base = int(args.base, 0)
|
|
try:
|
|
if args.cmd == "disasm":
|
|
print(json.dumps(disasm(data, args.arch, base), indent=2))
|
|
else:
|
|
print(json.dumps(emulate(data, args.arch, base), indent=2))
|
|
except ImportError as exc:
|
|
print(f"missing dependency: {exc} (pip install capstone unicorn)", file=sys.stderr)
|
|
return 2
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|