Thomas Piacentini
← Workshop

Chapter

inspect-binary-layout and the load image

Original text · English

On macOS, inspect-binary-layout refuses with capability_unavailable, and inspect-macho is the engine-free reader of segments, sections, load commands, imports, and exports.

You ran rea inspect-binary-layout fixtures/ledger-x86_64-O1.o --format json on the capture Mac. It exited 1 and printed a JSON error instead of a layout. Nothing is broken. rea 6.7.0 has two engine-free layout readers, and this one runs on Linux x64 only.

When you finish this section, you can read that error envelope and name the host rule behind it. You can also read the load image of ledger-arm64-O1 from inspect-macho: four segments, 17 load commands, six imports, and six exports.

The error envelope

inspect-binary-layout on macOSjsonFull source
{
  "code": "capability_unavailable",
  "category": "unsupported_provider",
  "message": "The initial real-verified pwntools artifact profile supports Linux x64 only.",
  "retryable": false,
  "remediation": {
    "action": "The initial real-verified pwntools artifact profile supports Linux x64 only."
  },
  "details": {
    "provider_id": "pwntools-elf",
    "operation": "inspect_binary_layout",
    "reason": "The initial real-verified pwntools artifact profile supports Linux x64 only."
  }
}
The whole file. The CLI exit status was 1.
Original source
title: inspect-binary-layout on macOS
source: capture/out/04-inspect-binary-layout.json
lang: json
note: The whole file. The CLI exit status was 1.
---
{
  "code": "capability_unavailable",
  "category": "unsupported_provider",
  "message": "The initial real-verified pwntools artifact profile supports Linux x64 only.",
  "retryable": false,
  "remediation": {
    "action": "The initial real-verified pwntools artifact profile supports Linux x64 only."
  },
  "details": {
    "provider_id": "pwntools-elf",
    "operation": "inspect_binary_layout",
    "reason": "The initial real-verified pwntools artifact profile supports Linux x64 only."
  }
}

Every failure in rea has this shape: code, category, message, retryable, remediation.action, and an optional details object. The provider is pwntools-elf. Its decoder throws AnalysisCapabilityUnavailableError when process.platform is not linux or process.arch is not x64, before it looks for Python. The message is a constant in the same file. The MCP server lists the tool as unavailable unless the host is Linux x64 and REA_PWNTOOLS_PYTHON is an absolute path. The exit status follows the CLI rule for an operation that could not finish {{cli §Output and exit status}}.

The capture kit gave the tool the ELF object because inspect_binary_layout is written for ELF. On a Linux x64 host with pwntools 4.15.0 it returns sections, segments, symbols, and relocations. Section 2.3 shows how each operation declares its availability before you call it, and the reference lists every error code the captures produced.

inspect-macho and the tools it runs

inspect-macho is the macOS reader, from the provider native-macos, "macOS native inspection utilities". It runs seven host commands and records each one as provenance.

the provenance of one inspect-macho runjsonFull source
"provenance": [
  {
    "tool": "lipo",
    "command": [
      "/Library/Developer/CommandLineTools/usr/bin/lipo",
      "-detailed_info",
      "$ARTIFACT"
    ],
    …
  },
  {
    "tool": "otool",
    "command": [
      "/Library/Developer/CommandLineTools/usr/bin/llvm-otool",
      "-h",
      "-l",
      "$ARTIFACT"
    ],
    …
  },
  {
    "tool": "nm",
    …
  },
  {
    "tool": "dyld_info",
    "command": [
      "/Library/Developer/CommandLineTools/usr/bin/dyld_info",
      "-imports",
      "$ARTIFACT"
    ],
    …
  },
  {
    "tool": "dyld_info",
    …
  },
  {
    "tool": "dwarfdump",
    …
  },
  {
    "tool": "vtool",
    …
  }
],
Cut to the tool names and three of the commands. Each entry also records the executable SHA-256, the exit code, and the output sizes.
Original source
title: the provenance of one inspect-macho run
source: capture/out/04-inspect-macho.json
lang: json
note: Cut to the tool names and three of the commands. Each entry also records the executable SHA-256, the exit code, and the output sizes.
---
"provenance": [
  {
    "tool": "lipo",
    "command": [
      "/Library/Developer/CommandLineTools/usr/bin/lipo",
      "-detailed_info",
      "$ARTIFACT"
    ],
    …
  },
  {
    "tool": "otool",
    "command": [
      "/Library/Developer/CommandLineTools/usr/bin/llvm-otool",
      "-h",
      "-l",
      "$ARTIFACT"
    ],
    …
  },
  {
    "tool": "nm",
    …
  },
  {
    "tool": "dyld_info",
    "command": [
      "/Library/Developer/CommandLineTools/usr/bin/dyld_info",
      "-imports",
      "$ARTIFACT"
    ],
    …
  },
  {
    "tool": "dyld_info",
    …
  },
  {
    "tool": "dwarfdump",
    …
  },
  {
    "tool": "vtool",
    …
  }
],

The command list is fixed in the provider. It runs lipo -detailed_info to discover the slices. For each slice it runs otool -h -l, nm -gjU --no-dyldinfo, dyld_info -imports, dyld_info -exports, and dwarfdump --uuid. It runs vtool -show-build as an optional extra. The run starts no Java and takes a few seconds. Keep one limitation of the record in mind when a name you expect is missing: "Imports and exports combine dyld_info and nm; stripped or toolchain-hidden symbols may be absent."

Fig. 3.2two layout commands on the capture Maccomparison
The same host refuses one layout reader on platform and answers the other with 17 load commands, because each reader names its own prerequisites.
The same host refuses one layout reader on platform and answers the other with 17 load commands, because each reader names its own prerequisites. Left to right in each column. Solid arrows are calls, the dashed rose arrow is the failure, the dashed ink arrow is a reply. From capture/out/04-inspect-binary-layout.json and 04-inspect-macho.json.

Original SVG

Original source
id: fig-3-2
kind: comparison
title: two layout commands on the capture Mac
claim: The same host refuses one layout reader on platform and answers the other with 17 load commands, because each reader names its own prerequisites.
caption: Left to right in each column. Solid arrows are calls, the dashed rose arrow is the failure, the dashed ink arrow is a reply. From capture/out/04-inspect-binary-layout.json and 04-inspect-macho.json.

The load image

header facts and the entry pointjsonFull source
"format": "mach-o",
"selected_architecture": {
  "name": "arm64",
  "container_file_offset": 0
},
"endian": "little",
"word_size": 64,
"file_type": "2",
"flags": [
  "0x00200085"
],
"uuid": "59A1CCA5-97D1-3661-99DA-666BC2BB38D7",
"entrypoints": {
  "items": [
    {
      "file_offset": 1488
    }
  ],
  …
},
…
"build_metadata": {
  "items": [
    {
      "platform": "MACOS",
      "minimum_os": "26.0",
      "sdk": "26.2",
      …
Cut from normalized_result to the header, the entry point, and the build metadata. rea names the build platform and tool codes from loader.h.
Original source
title: header facts and the entry point
source: capture/out/04-inspect-macho.json
lang: json
note: Cut from normalized_result to the header, the entry point, and the build metadata. rea names the build platform and tool codes from loader.h.
---
"format": "mach-o",
"selected_architecture": {
  "name": "arm64",
  "container_file_offset": 0
},
"endian": "little",
"word_size": 64,
"file_type": "2",
"flags": [
  "0x00200085"
],
"uuid": "59A1CCA5-97D1-3661-99DA-666BC2BB38D7",
"entrypoints": {
  "items": [
    {
      "file_offset": 1488
    }
  ],
  …
},
…
"build_metadata": {
  "items": [
    {
      "platform": "MACOS",
      "minimum_os": "26.0",
      "sdk": "26.2",
      …

Load command: one record in the list that follows the Mach-O header, with a cmd type and a cmdsize. Apple publishes no prose specification, so the loader.h header in the XNU source is the reference. file_type 2 is MH_EXECUTE. The flags 0x00200085 are MH_NOUNDEFS 0x1, MH_DYLDLINK 0x4, MH_TWOLEVEL 0x80, and MH_PIE 0x200000 added together. The entry point comes from LC_MAIN, whose entryoff is 1488, or 0x5d0. Add it to the __TEXT address below and you get 0x1000005d0, the _main export.

The four LC_SEGMENT_64 commands are the load image. Each segment_command_64 names a vmaddr, a vmsize, a fileoff, a filesize, two protection fields, and nsects, and its section_64 records follow inside the same command.

Segment vmaddr vmsize fileoff filesize initprot nsects
__PAGEZERO 0x0 4294967296 0 0 0 0
__TEXT 0x100000000 16384 0 16384 5, read and execute 5
__DATA_CONST 0x100004000 16384 16384 16384 3, read and write 2
__LINKEDIT 0x100008000 16384 32768 1104 1, read 0
the __TEXT segment and its five sectionsjsonFull source
{
  "name": "__TEXT",
  "vm_address": "0x100000000",
  "vm_size": 16384,
  "file_offset": 0,
  "file_size": 16384,
  …
  "sections": {
    "items": [
      {
        "segment": "__TEXT",
        "name": "__text",
        "address": "0x100000500",
        "size": 576,
        "file_offset": 1280,
        …
      },
      {
        "segment": "__TEXT",
        "name": "__stubs",
        "address": "0x100000740",
        "size": 48,
        "file_offset": 1856,
        …
      },
      {
        "segment": "__TEXT",
        "name": "__cstring",
        "address": "0x100000770",
        "size": 95,
        "file_offset": 1904,
        …
      },
      {
        "segment": "__TEXT",
        "name": "__const",
        "address": "0x1000007d0",
        "size": 64,
        "file_offset": 2000,
        …
      },
      {
        "segment": "__TEXT",
        "name": "__unwind_info",
        "address": "0x100000810",
        "size": 96,
        "file_offset": 2064,
        …
      }
    ],
Cut from the segments collection. Permissions, alignment, and flags are cut from each section.
Original source
title: the __TEXT segment and its five sections
source: capture/out/04-inspect-macho.json
lang: json
note: Cut from the segments collection. Permissions, alignment, and flags are cut from each section.
---
{
  "name": "__TEXT",
  "vm_address": "0x100000000",
  "vm_size": 16384,
  "file_offset": 0,
  "file_size": 16384,
  …
  "sections": {
    "items": [
      {
        "segment": "__TEXT",
        "name": "__text",
        "address": "0x100000500",
        "size": 576,
        "file_offset": 1280,
        …
      },
      {
        "segment": "__TEXT",
        "name": "__stubs",
        "address": "0x100000740",
        "size": 48,
        "file_offset": 1856,
        …
      },
      {
        "segment": "__TEXT",
        "name": "__cstring",
        "address": "0x100000770",
        "size": 95,
        "file_offset": 1904,
        …
      },
      {
        "segment": "__TEXT",
        "name": "__const",
        "address": "0x1000007d0",
        "size": 64,
        "file_offset": 2000,
        …
      },
      {
        "segment": "__TEXT",
        "name": "__unwind_info",
        "address": "0x100000810",
        "size": 96,
        "file_offset": 2064,
        …
      }
    ],

__PAGEZERO reserves the first 4 GiB of the address space with no file bytes, so a null pointer never reaches mapped memory. __TEXT starts at 0x100000000 and maps the first 16384 bytes of the file. Its code section __text holds 576 bytes at 0x100000500, and Part 4 accounts for every one of them. __cstring holds the format strings and the five labels, and __const holds the two lookup tables that the jump table section reads.

The __LINKEDIT segment has no sections. Other load commands point into it by file offset.

Command Offset in the file Size
LC_DYLD_CHAINED_FIXUPS 32768 176
LC_DYLD_EXPORTS_TRIE 32944 104
LC_FUNCTION_STARTS 33048 8
LC_SYMTAB, 13 symbols symoff 33056, stroff 33304 strsize 152
LC_CODE_SIGNATURE 33456 416

The last command ends at 33872, the file size. In ELF the loader reads a program header table and the linker reads a section header table, two tables that the ELF header points to. In Mach-O there is one list, and LC_SEGMENT_64 carries its sections inside the command. Lesson 32 builds a reader for both formats from the specifications and lines up each load command with its ELF idea.

Fig. 3.3the load image of ledger-arm64-O1layers
Four segments map 33872 file bytes into three address ranges, and every other load command points into the last one by file offset.
Four segments map 33872 file bytes into three address ranges, and every other load command points into the last one by file offset. Read from the lowest address at the top. The left column is the address, the right column is the file offset. The dashed box has no file bytes. From capture/out/04-inspect-macho.json.

Original SVG

Original source
id: fig-3-3
kind: layers
title: the load image of ledger-arm64-O1
claim: Four segments map 33872 file bytes into three address ranges, and every other load command points into the last one by file offset.
caption: Read from the lowest address at the top. The left column is the address, the right column is the file offset. The dashed box has no file bytes. From capture/out/04-inspect-macho.json.

Imports and exports

the exported functionsjsonFull source
"exports": {
  "items": [
    …
    {
      "name": "_grade",
      "address": "0x100000518",
      …
    },
    {
      "name": "_grade_label",
      "address": "0x100000588",
      …
    },
    {
      "name": "_main",
      "address": "0x1000005d0",
      …
    },
    {
      "name": "_percent_change",
      "address": "0x100000500",
      …
    },
    {
      "name": "_total",
      "address": "0x100000554",
      …
    }
  ],
  "total": null,
  "exhaustive": false,
  "limitations": [
    "Merged nm/dyld_info results may be incomplete for stripped binaries."
  ]
},
Cut from the exports collection. The __mh_execute_header entry and the weak and reexport fields are cut.
Original source
title: the exported functions
source: capture/out/04-inspect-macho.json
lang: json
note: Cut from the exports collection. The __mh_execute_header entry and the weak and reexport fields are cut.
---
"exports": {
  "items": [
    …
    {
      "name": "_grade",
      "address": "0x100000518",
      …
    },
    {
      "name": "_grade_label",
      "address": "0x100000588",
      …
    },
    {
      "name": "_main",
      "address": "0x1000005d0",
      …
    },
    {
      "name": "_percent_change",
      "address": "0x100000500",
      …
    },
    {
      "name": "_total",
      "address": "0x100000554",
      …
    }
  ],
  "total": null,
  "exhaustive": false,
  "limitations": [
    "Merged nm/dyld_info results may be incomplete for stripped binaries."
  ]
},

Six exports: the five functions and __mh_execute_header at 0x100000000. Six imports from libSystem: ___stack_chk_fail, ___stack_chk_guard, ___stderrp, _atoi, _fwrite, and _printf.

One dependency: /usr/lib/libSystem.B.dylib. rea lists only dylib load commands as dependencies. The dynamic linker /usr/lib/dyld stays in the LC_LOAD_DYLINKER row of the load commands.

Both the imports and the exports collections say exhaustive: false, because each merges the text of two tools. The record does not promise that the list is complete, and you should not promise it either. Section 4.2 shows the same names as Ghidra sees them, and the next section turns 0x100000518 into a file offset.

Original source
- Expect `capability_unavailable` from `inspect-binary-layout` on any host that is not Linux x64.
- Read `code`, `category`, and `details.provider_id` before you retry anything.
- On macOS run `inspect-macho` and read the four `LC_SEGMENT_64` rows first.
- Check `exhaustive` on each collection before you say a symbol is absent.

Sources: capture/out/00-commands.txt, capture/out/04-inspect-binary-layout.json, capture/out/04-inspect-macho.json; rea 6.7.0 at 27dc42d: src/contracts/errorSchemas.ts, src/native/pwntools/PwntoolsDecoder.ts, src/server/createServer.ts, src/native/NativeMachoInspection.ts, src/native/parsers/otool.ts; rea docs/cli.md

Code and artifacts 1
What you take away

Saved in your notebook on this device.

Sources and limits

Reading checkpoints are self-reported. Test results are recorded separately and apply to the executed cases.

AI Engineering from Scratch · 3f9715c · MIT

Download this file ↓