///|
/// `moonpe` — inspect a Windows PE image.
///
/// The library is deliberately IO-free; this command is the only place that
/// touches the filesystem, so the parser stays testable in memory.
///|
fn main {
run()
}
///|
fn print_usage() -> Unit {
println("moonpe - inspect Windows PE images")
println("")
println(
"usage: moonpe [--json] [--summary-only] ",
)
println("")
println("commands:")
println(" info headers, machine, subsystem, dependency counts")
println(" sections section table with permissions")
println(" imports imported DLLs, including delay-loaded ones")
println(" iat import address slots, symbols and raw values")
println(" exports exported symbols")
println(" debug debug directory entries and PDB paths")
println(" relocations relocation blocks by fixup type")
println(" resources resource tree and VERSIONINFO")
println(" dotnet CLR metadata streams and decoded tables")
println(" loadconfig load-configuration directory (SafeSEH, CFG, cookie)")
println(" check release and triage findings with a verdict")
println(
" scan recursively check .exe/.dll/.sys files in a directory",
)
println(" all everything")
println("")
println("global options:")
println(" --json machine-readable JSON output")
println(" --summary-only omit per-file rows during scan")
}
///|
fn fail(message : String) -> Unit {
println("moonpe: " + message)
}
///|
fn run() -> Unit {
let args = @env.args()
// Under the JS target the runtime hands over node and the script path
// first, so the real arguments start two slots in.
let mut base = 1
if args.length() >= 2 && args[1].contains(".js") {
base = 2
}
if args.length() <= base {
print_usage()
return
}
let mut json_mode = false
let mut summary_only = false
let mut reading_options = true
while reading_options && args.length() > base {
let option = args[base]
if option == "--json" {
json_mode = true
base = base + 1
} else if option == "--summary-only" {
summary_only = true
base = base + 1
} else {
reading_options = false
}
}
if args.length() <= base {
if json_mode {
fail_json("missing command")
} else {
print_usage()
}
return
}
let command = args[base]
if command == "-h" || command == "--help" || command == "help" {
print_usage()
return
}
if args.length() <= base + 1 {
if json_mode {
fail_json("missing file or directory argument")
} else {
fail("missing file argument")
print_usage()
}
return
}
let path = args[base + 1]
if command == "scan" {
run_scan(path, json_mode, summary_only)
return
}
if summary_only {
if json_mode {
fail_json("--summary-only is only valid with scan")
} else {
fail("--summary-only is only valid with scan")
}
return
}
let known = command == "info" ||
command == "sections" ||
command == "imports" ||
command == "iat" ||
command == "exports" ||
command == "debug" ||
command == "relocations" ||
command == "resources" ||
command == "dotnet" ||
command == "loadconfig" ||
command == "check" ||
command == "all"
if !known {
if json_mode {
fail_json("unknown command '" + command + "'")
} else {
fail("unknown command '" + command + "'")
print_usage()
}
return
}
let bytes = @fs.read_file_to_bytes(path) catch {
_ => {
if json_mode {
fail_json("cannot read " + path)
} else {
fail("cannot read " + path)
}
return
}
}
let pe = @pe.Pe::parse(bytes) catch {
e => {
if json_mode {
fail_json(e.message())
} else {
fail(e.message())
}
return
}
}
if json_mode {
print_json(json_for_command(path, command, pe))
return
}
if command == "info" {
print_info(path, pe)
} else if command == "sections" {
print_sections(pe)
} else if command == "imports" {
print_imports(pe)
} else if command == "iat" {
print_iat(pe)
} else if command == "exports" {
print_exports(pe)
} else if command == "debug" {
print_debug(pe)
} else if command == "relocations" {
print_relocations(pe)
} else if command == "resources" {
print_resources(pe)
} else if command == "dotnet" {
print_dotnet(pe)
} else if command == "loadconfig" {
print_load_config(pe)
} else if command == "check" {
print_check(pe)
} else if command == "all" {
print_info(path, pe)
println("")
print_sections(pe)
println("")
print_imports(pe)
println("")
print_iat(pe)
println("")
print_exports(pe)
println("")
print_debug(pe)
println("")
print_relocations(pe)
println("")
print_resources(pe)
println("")
print_dotnet(pe)
println("")
print_check(pe)
}
}
///|
fn pad_right(text : String, width : Int32) -> String {
let sb = StringBuilder()
sb.write_string(text)
let mut i = text.length()
while i < width {
sb.write_char(' ')
i = i + 1
}
sb.to_string()
}
///|
fn pad_left(text : String, width : Int32) -> String {
let sb = StringBuilder()
let mut i = text.length()
while i < width {
sb.write_char(' ')
i = i + 1
}
sb.write_string(text)
sb.to_string()
}
///|
fn print_info(path : String, pe : @pe.Pe) -> Unit {
let libraries = pe.imports().length() catch { _ => 0 }
let delay_libraries = pe.delay_imports().length() catch { _ => 0 }
let exports = try {
match pe.exports() {
Some(table) => table.exports.length()
None => 0
}
} catch {
_ => 0
}
println("file: " + path)
println("format: PE" + pe.bits().to_string())
println("machine: " + pe.machine())
println("subsystem: " + pe.subsystem())
println("image: base 0x" + @pe.hex64(pe.optional.image_base))
println(" size 0x" + @pe.hex(pe.optional.size_of_image))
println("entry: 0x" + @pe.hex(pe.optional.entry_point))
println("sections: " + pe.sections.length().to_string())
println("imports: " + libraries.to_string() + " libraries")
println("delay: " + delay_libraries.to_string() + " libraries")
println("exports: " + exports.to_string() + " symbols")
print_pdb_line(pe)
print_version_line(pe)
print_extra_lines(pe)
}
///|
fn print_sections(pe : @pe.Pe) -> Unit {
println(
pad_right("NAME", 12) +
pad_left("VADDR", 10) +
pad_left("VSIZE", 10) +
pad_left("RAWPTR", 10) +
pad_left("RAWSIZE", 10) +
" PERM",
)
for section in pe.sections {
println(
pad_right(section.name, 12) +
pad_left("0x" + @pe.hex(section.virtual_address), 10) +
pad_left("0x" + @pe.hex(section.virtual_size), 10) +
pad_left("0x" + @pe.hex(section.raw_pointer), 10) +
pad_left("0x" + @pe.hex(section.raw_size), 10) +
" " +
section.permissions(),
)
}
}
///|
fn print_imports(pe : @pe.Pe) -> Unit {
let libraries = Some(pe.imports()) catch {
e => {
fail(e.message())
None
}
}
match libraries {
None => ()
Some(libs) =>
if libs.length() == 0 {
println("(no imports)")
} else {
for library in libs {
println(library.name)
for entry in library.entries {
println(" " + entry_label(entry))
}
}
}
}
let delayed = Some(pe.delay_imports()) catch {
e => {
fail(e.message())
None
}
}
match delayed {
None => ()
Some(libs) =>
if libs.length() > 0 {
println("delay-loaded:")
for library in libs {
println(library.name)
for entry in library.entries {
println(" " + entry_label(entry))
}
}
}
}
}
///|
fn entry_label(entry : @pe.ImportEntry) -> String {
match entry.name {
Some(name) => name
None =>
match entry.ordinal {
Some(ordinal) => "#" + ordinal.to_string() + " (ordinal)"
None => "?"
}
}
}
///|
fn print_iat(pe : @pe.Pe) -> Unit {
let slots = Some(pe.iat()) catch {
e => {
fail(e.message())
None
}
}
match slots {
None => ()
Some(entries) =>
if entries.length() == 0 {
println("(no IAT entries)")
} else {
println(
pad_right("KIND", 7) +
pad_right("LIBRARY", 24) +
pad_left("SLOT", 10) +
pad_left("RAW", 20) +
" SYMBOL",
)
for entry in entries {
println(
pad_right(if entry.delayed { "delay" } else { "static" }, 7) +
pad_right(entry.library, 24) +
pad_left("0x" + @pe.hex(entry.slot_rva), 10) +
pad_left("0x" + @pe.hex64(entry.raw_value), 20) +
" " +
entry_label(entry.symbol),
)
}
}
}
}
///|
fn print_exports(pe : @pe.Pe) -> Unit {
let table = Some(pe.exports()) catch {
e => {
fail(e.message())
None
}
}
match table {
None => ()
Some(value) =>
match value {
None => println("(no exports)")
Some(exports) => {
println(exports.dll_name)
for entry in exports.exports {
let label = match entry.name {
Some(name) => name
None => "-"
}
println(
" " +
pad_right(label, 32) +
" ordinal " +
pad_left(entry.ordinal.to_string(), 5) +
" rva 0x" +
@pe.hex(entry.rva),
)
}
}
}
}
}
///|
fn print_pdb_line(pe : @pe.Pe) -> Unit {
let entries = Some(pe.debug_entries()) catch { _ => None }
match entries {
None => ()
Some(list) => {
let mut found = false
for entry in list {
match entry.pdb_path {
Some(path) =>
if !found {
println("pdb: " + path)
found = true
}
None => ()
}
}
if !found {
println("pdb: (none)")
}
}
}
}
///|
fn print_version_line(pe : @pe.Pe) -> Unit {
let info = Some(pe.version_info()) catch { _ => None }
match info {
None => ()
Some(value) =>
match value {
None => println("version: (no version resource)")
Some(version) => {
let product = match version.product_name {
Some(name) => name
None => "-"
}
println("version: " + product + " " + version.file_version)
}
}
}
}
///|
fn print_debug(pe : @pe.Pe) -> Unit {
let entries = Some(pe.debug_entries()) catch {
e => {
fail(e.message())
None
}
}
match entries {
None => ()
Some(list) =>
if list.length() == 0 {
println("(no debug directory)")
} else {
println(pad_right("TYPE", 24) + pad_left("AGE", 6) + " PDB")
for entry in list {
let age = match entry.pdb_age {
Some(value) => value.to_string()
None => "-"
}
let path = match entry.pdb_path {
Some(value) => value
None => "-"
}
println(
pad_right(entry.kind_name, 24) + pad_left(age, 6) + " " + path,
)
}
}
}
}
///|
fn print_relocations(pe : @pe.Pe) -> Unit {
let summary = Some(pe.relocations()) catch {
e => {
fail(e.message())
None
}
}
match summary {
None => ()
Some(value) =>
if value.blocks.length() == 0 {
println("(no relocations)")
} else {
println(
pad_left("PAGE", 12) +
pad_left("ENTRIES", 9) +
pad_left("DIR64", 7) +
pad_left("HIGHLOW", 9) +
pad_left("OTHER", 7),
)
for block in value.blocks {
println(
pad_left("0x" + @pe.hex(block.page_rva), 12) +
pad_left(block.entries.to_string(), 9) +
pad_left(block.dir64.to_string(), 7) +
pad_left(block.highlow.to_string(), 9) +
pad_left((block.other + block.high + block.low).to_string(), 7),
)
}
println(
"total: " +
value.total_entries.to_string() +
" entries (DIR64 " +
value.dir64.to_string() +
", HIGHLOW " +
value.highlow.to_string() +
")",
)
}
}
}
///|
fn print_resources(pe : @pe.Pe) -> Unit {
let entries = Some(pe.resources()) catch {
e => {
fail(e.message())
None
}
}
match entries {
None => ()
Some(list) =>
if list.length() == 0 {
println("(no resources)")
} else {
println(
pad_right("TYPE", 18) +
pad_right("NAME", 10) +
pad_left("LANG", 8) +
" RVA/SIZE DETAIL",
)
for entry in list {
let kind = match entry.type_id {
Some(id) => @pe.resource_type_name(id)
None =>
match entry.type_name {
Some(name) => name
None => "?"
}
}
let name = match entry.name_id {
Some(id) => id.to_string()
None =>
match entry.name_name {
Some(value) => value
None => "?"
}
}
let detail = pe.decode_resource(entry).summary catch {
e => "decode error: " + e.message()
}
println(
pad_right(kind, 18) +
pad_right(name, 10) +
pad_left("0x" + @pe.hex(entry.language), 8) +
" 0x" +
@pe.hex(entry.rva) +
"/" +
entry.size.to_string() +
" " +
detail,
)
}
}
}
}
///|
fn print_dotnet(pe : @pe.Pe) -> Unit {
let metadata = pe.clr_metadata() catch {
e => {
fail(e.message())
None
}
}
match metadata {
None => println("(native image; no CLR metadata)")
Some(info) => {
let assembly = match info.assembly_name {
Some(name) => name
None => "(none)"
}
let version = match info.assembly_version {
Some(value) => value
None => "-"
}
println("assembly: " + assembly + " " + version)
let module_name = match info.module_name {
Some(name) => name
None => "-"
}
println("module: " + module_name)
println("streams: " + info.streams.length().to_string())
for stream in info.streams {
println(
" 0x" +
@pe.hex(stream.offset) +
" " +
pad_left(stream.size.to_string(), 8) +
" " +
stream.name,
)
}
println("tables:")
for table in info.tables {
println(
" " + pad_right(table.name, 28) + pad_left(table.rows.to_string(), 8),
)
}
println(
"rows: " +
info.type_definitions.length().to_string() +
" type definitions, " +
info.type_references.length().to_string() +
" type references, " +
info.method_definitions.length().to_string() +
" methods, " +
info.member_reference_names.length().to_string() +
" member references",
)
for reference in info.assembly_references {
println("ref: " + reference.name + " " + reference.version)
}
}
}
}
///|
fn print_check(pe : @pe.Pe) -> Unit {
let report = pe.check()
println("verdict: " + report.verdict)
println(
"max entropy: " +
(report.max_section_entropy * 100.0).to_int().to_string() +
" / 800",
)
if report.findings.length() == 0 {
println(" (no findings)")
} else {
for finding in report.findings {
let tag = match finding.severity {
@pe.Info => "INFO"
@pe.Warning => "WARN"
@pe.High => "HIGH"
}
println(
" [" +
pad_right(tag, 4) +
"] " +
pad_right(finding.code, 18) +
finding.message,
)
}
}
}
///|
fn print_extra_lines(pe : @pe.Pe) -> Unit {
let clr = pe.clr() catch { _ => None }
match clr {
Some(info) => println("runtime: .NET " + info.runtime_version)
None => ()
}
let level = pe.manifest_execution_level() catch { _ => None }
match level {
Some(value) => println("manifest: requestedExecutionLevel=" + value)
None => ()
}
let certificates = pe.certificates() catch { _ => [] }
if certificates.length() > 0 {
println(
"signed: " +
certificates.length().to_string() +
" certificate entry(ies)",
)
}
let tls = pe.tls() catch { _ => None }
match tls {
Some(info) =>
println("tls: " + info.callbacks.to_string() + " callback(s)")
None => ()
}
}
///|
fn print_load_config(pe : @pe.Pe) -> Unit {
let config = pe.load_config() catch {
e => {
fail(e.message())
return
}
}
match config {
None => println("(no load config)")
Some(info) => {
println("size: 0x" + @pe.hex(info.size))
println("security cookie: 0x" + @pe.hex64(info.security_cookie))
let handlers = pe.safe_seh_handlers() catch { _ => None }
match handlers {
None => println("safeseh: not applicable (not x86)")
Some(list) =>
if list.length() == 0 {
println("safeseh: no handlers recorded")
} else {
println(
"safeseh: " +
list.length().to_string() +
" handler(s), first 0x" +
@pe.hex(list[0]),
)
}
}
println(
"guard flags: 0x" +
@pe.hex(info.guard_flags) +
", table 0x" +
@pe.hex64(info.guard_cf_function_table) +
", count " +
info.guard_cf_function_count.to_string(),
)
println("heap flags: 0x" + @pe.hex(info.process_heap_flags))
}
}
}