Recovering symbols from log functions

Your stripped binary still remembers where it came from

Stripping a binary throws away the symbol table: function names become
sub_140001a30, the directory layout the developer worked in is gone, and you
start your analysis staring at anonymous blobs. Except a stripped binary is
rarely as silent as it looks. If the program uses assert, a custom logger, or
any BUG()-style macro, the compiler has very likely baked its own source
metadata straight into .rodata, and that metadata hands you back names and
structure for free.

This post walks through the trick on a tiny example, then shows two Binary
Ninja plugins that automate it: logrn (recovers names) and arborist
(recovers the source tree).

Where the leak comes from

The C preprocessor defines a handful of magic tokens that expand to string (or
integer) literals at the call site:

  • __FILE__ → the source path as passed to the compiler, e.g. "src/net/socket.c"
  • __LINE__ → the line number
  • __func__ / __FUNCTION__ → the enclosing function’s name, e.g. "socket_open"

Logging and assertion macros almost always fold these in so a message can say
where it came from. A typical custom logger looks like this:

1
2
void log_msg(const char *file, int line, const char *func, const char *msg);
#define LOG(msg) log_msg(__FILE__, __LINE__, __func__, msg)

Every time LOG("...") is used, the compiler emits a call to log_msg with the
current file path, line, and function name as constant arguments. Those
constants survive stripping, stripping removes symbols, not string data. So a
single logging function becomes a central collection point: walk its callers and
you can read, per caller, the original file and function name.

That one line tells you the caller is _ConfigManagerFilter_GetConfig and that it lived in Src/RPCApp/Filter/ConfigManager.c.
There are as many such lines as there are callers. Doing this by hand across thousands of callers is the part that logrn plugin automate.

Recovering names - logrn

logrn (https://github.com/sum-catnip/logrn) takes the name argument. You select
the logging function, tell it which parameter carries the function name (func
here), and it walks every caller, reads the string that caller passes, and
renames the caller to it.

1
2
[Default] processing 2213 callers
[Default] renaming done

A caveat worth stating in the post: logrn takes the first matching call per
caller. Most of the time a function logs its own name consistently, but inlined
code can carry a foreign name, a few wrong labels are the price of recovering
thousands of correct ones.

Recovering structure - arborist

__func__ gives you names, __FILE__ gives you place. That’s what
arborist (https://github.com/9hozt/arborist) exploits. Same idea, select the
logging function, point it at the path parameter (file), but instead of
renaming, it reconstructs the directory layout the paths describe and writes it
out as a browsable tree.

Pick the tree + Pseudo C mode and arborist recreates the folders and fills
each file with the decompiled Pseudo C of the functions that belonged to it:

Combined with logrn, you’ve gone from a flat list of sub_* to named functions
sitting in their original folders, with pseudo-source inside, a large chunk of
the project’s shape rebuilt from a binary that was supposed to tell you nothing.

Limitations

This is reconstruction, not time travel. Worth being upfront about:

  • Inlining. A function inlined from another translation unit can carry that
    unit’s __FILE__, so it may be filed under the wrong path. (Recompile one
    file at -O2 to see this happen.)
  • Partial coverage. Only functions that actually reach a logging call leak
    anything. The tree reflects what the binary leaks, not the whole project.
  • Pseudo C is decompiler output, not the original source, and it won’t
    compile.
  • The leak can be suppressed. -ffile-prefix-map, reproducible-build flags,
    or a logger that doesn’t pass __FILE__/__func__ all shut the door. When
    it’s open, though, it’s wide open.

The end

logrn and arborist are both on GitHub and MIT-licensed. Found a bug, have an idea, or want to tackle a roadmap item? Open an issue or send a PR, contributions are more than welcome.