Repository navigation
A stripped bzip2 build on Linux: what REA returned, checked against the build and the docs #2254
CassieFox23
started this conversation in
Show and tell
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
TL;DR: I asked REA which function prints one message in a stripped, optimized bzip2 build. REA's function, one caller and 24 callees all matched an unstripped build and the source. It also refused two bad requests with typed errors, and below I list what this does not show.
Here is what I saw on Linux. The steps are folded below so you can repeat them.
The test
The program is bzip2 1.0.8, under the bzip2 licence (a BSD-style licence). I built it with gcc -O2 and ran
strip. The file has no function names, and the compiler has inlined some functions.The question: which function prints "Can't guess original name for %s -- using %s", who calls it, and what does it call?
REA found the string at 0x413220. It traced the string to the function at 0x402ed0, listed one caller, and listed 24 callees. Each reply carried an Evidence id. A label on each id says "observed" (Ghidra saw it) or "derived" (REA worked it out from other records).
I then checked the answers against an unstripped build of the same source. REA never saw that build.
uncompressat 0x402ed0uncompressmainat 0x400600objdumpTwo refusals
uncompress. The stripped file has no such name. REA replied with the codeinvalid_request. The message said the name is unknown. Aprocedure_infocall formaingot the same code.inspect_macho, on this Linux file. REA replied with the codecapability_unavailable. The reason read: "Native macOS utilities require macOS."Plain Ghidra
Plain Ghidra headless, run on the same file, printed its analyzer list and then "Import succeeded". It printed no function list, no string hit and no caller list.
What the docs say
Here is where REA's docs describe what this demo shows. I read them on today's main, f941968. The demo ran on 83a4445.
Evidence ids. From mcp-contracts.md, lines 11 to 13:
Observed and derived. From tool-design.md, lines 57 to 59:
- Keep observed, derived, and inferred relationships distinct. Preserve unresolved edges and partial provider facts instead of requiring every provider to supply the same metadata.The two refusals. From mcp-contracts.md, lines 24 to 26:
Callee lists. From tool-design.md, lines 60 to 64:
So the 24 callees above are the direct calls Ghidra saw in the function. Here they matched
objdump. The docs do not promise that for every program.Not run
This demo does not show these things.
paniccalls inuncompressmatches which source string. The count of two matches.Two more things I saw
BZ2_bzCompress, in both builds. REA reported five changed parts, one unchanged and two unknown. It would not call them the same function, because the names were only addresses.Build commands
I computed the tarball sha256 below on my box. The script checks it before it unpacks.
My builds came out as follows. The -O2 file is an x86-64 ELF, dynamically linked and stripped.
The
.textbytes are identical in the stripped and unstripped files (checked withobjcopy -O binary --only-section=.text). So addresses fromnmon the unstripped file apply to the stripped one.Exact MCP calls and replies (trimmed)
A small Node script of mine (not included here) started
node scripts/rea.mjs mcpover stdio and made these calls in order. The server reportedrea6.4.0 with 173 tools. Evidence ids are shortened.Question and answer:
Of the 24 callees, 5 are inside the program. With the unstripped build, I named them as follows.
The other 19 are import-table names that Ghidra gave itself, such as
fprintf,fopen64,strerrorandutime.The body of the function at 0x402ed0 has 19 separate ranges. The helpers
mapSuffix,containsDubiousCharsandfileExistshave no function of their own in the unstripped -O2 build. The compiler inlined them.Each reply also carries
limitations. For callers and callees, the coverage note says Ghidra returns only calls it resolved. Forprocedure_info, two notes say that Ghidra marks two calls to 0x4014c0 (at 0x403630 and 0x4036f6) as no-return calls.Refusals, with the reply fields trimmed:
{"error":{"code":"invalid_request","category":"invalid_input","retryable":true, "details":{"operation":"analyze_function","issues":[{"path":[],"reason":"invalid_value", "message":"Unknown Ghidra procedure name or address: uncompress"}]}}} {"error":{"code":"capability_unavailable","category":"unsupported_provider","retryable":false, "details":{"provider_id":"ghidra","operation":"inspect_macho", "reason":"Native macOS utilities require macOS."}}}Three other calls showed limits.
xrefswith no arguments gaveinvalid_request, with the issue{"path":["address"],"reason":"missing_argument","expected":"string"}.search_stringscall for text that is not in the file gave 0 matches. The note said an empty search does not show that the text is absent.open_binarywith a relative path failed input validation, and the message said the path must be absolute.Other calls:
trace_call_path {"start":"0x400600","goal":"0x402310"}gavegoal_status: reachedwithtraversal_path["0x400600","0x402ed0","0x402310"]. That ismain,uncompress,uncompressStreambynm. It covered 55 nodes and 101 edges, with no failures and no unresolved items.export_evidence_bundlewrote 440,392 bytes. The file holds the path of the binary, so its sha256 depends on that path. Two runs at the first path gave the first hash below. Two reruns at a second path gave the second hash.The -O2 versus -O0 compare output
I ran
analyze_functiononBZ2_bzCompressin each build (0x40e460 in -O2, 0x414031 in -O0, both fromnm). Then I passed both full replies tocompare_functions.REA knew them only by address. The result was
function_match.status: mismatched, confidencederived, authorityanalyst-inference, overall statusunknown. The summary was 1 unchanged, 5 changed and 2 unknown. The counts are the numbers REA printed. I did not check their units.REA gave these limits:
Then
find_changed_behaviortook that comparison. It gavebehavior_status: unknown, with 0 observed changes, 5 static candidates, 0 contradictions and 2 unresolved.Commands to repeat it
Build the two bzip2 binaries with the commands in the "Build commands" fold.
Put Node 24 first on your PATH (I used 24.18.0). Build REA at the commit below. I first built from a
git archiveof that commit. I then ran this clone and checkout as written. It gave the same commit, and the tracked files matched thegit archivefiles.Set these environment variables for the REA process. My first run also set
REA_PWNTOOLS_PYTHONto a Python with pwntools 4.15.0. My rerun without it gave the same results, so this demo does not need it.Ask the main question with the REA command line. This is the same call as
trace_feature. It gave the same Evidence id (ev_a6833b81...ec0a99cd) as the MCP call, even from a new build at a new path.node rea/scripts/rea.mjs trace /absolute/path/to/bzip2-O2 "guess original name" --provider ghidra --jsonFor the rest, connect any MCP client to
node rea/scripts/rea.mjs mcpand make the calls listed in the "Exact MCP calls" fold.Check the answers with the unstripped build:
Run plain Ghidra on the same file. Make an empty project folder with no dot-folder in its path. Ghidra does not make it for you. One try failed with "Path element starting with '.' is not permitted" because the folder was under
.cache. Another failed with "Directory not found" because the folder did not exist.I ran the whole demo twice at first. Then I rebuilt everything in a new folder and ran it twice more, with a copy of my script that pointed at the new folder. Every Evidence id in the saved replies matched the first runs and matched between the two new runs. The ids do not depend on the path. The console log matched the first run line for line, apart from call times and the bundle hash.
Test setup
🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊
All reactions