Vector35 / binaryninja-api

Public API, examples, documentation and issues for Binary Ninja
https://binary.ninja/
MIT License
927 stars 209 forks source link

MIPS PLT entries are broken. #3267

Closed plafosse closed 1 year ago

plafosse commented 2 years ago

Version and Platform (required):

Bug Description: image

PLT entries in the included binary are broken in some fundamental way.

libhdk.bndb.zip

lwerdna commented 1 year ago

This one is kind of tough. First a quick review on what happens when this binary actually executes, here for an example call to memset().

Register $gp gets address GOT + 0x7FF0. When the actual call does lw $t9, -0x7d28($gp) it resolves to 0x2C6C8, the address of the GOT entry. The entry initially holds the address of the stub. Every stub does lw $t9, -0x7ff0($gp), loading the 0'th entry and calling it. While the binary has bytes 00 00 00 00 there, the dynamic loader at runtime replaces it with a pointer to its lazy loading routine:

image

The routine reads $t8 to see which symbol table index it's resolving (notice the addiu $t8, ... in the stub's branch slot) and then overwrites the GOT entry with the address of the called function's implementation, which is probably in some shared object that RTLD also was responsible for placing:

image

The stub never executes again. The initial call has four steps, but every subsequent call only has two steps. What does BinaryNinja do? Well it imitates the loader a bit, by replacing the initial GOT entry (pointing to the stub) with the address of a "resolved" function. Of course BinaryNinja doesn't actually load shared objects dependencies and resolve them, so a fake section ".extern" acts as a stand-in:

image

When the first instruction of each stub loads the first entry of the GOT, it really does get 0x0 because this is a mostly static look at a binary and RTLD hasn't executed and replaced the zero with its lazy loading routine:

00018b30  1080998f   lw      $t9, -0x7ff0($gp)  {0x0}  {_GLOBAL_OFFSET_TABLE_}

It does end up calling 0x0 through $t9, and thus decompiling to nullptr() isn't incorrect.

The analysis / function finder continues downward and thinks the next function is fall through execution, which is why you get memset() calling qsort() below it, qsort() calling XML_ErrorString() below it, etc. User agency is limited without a way to mark the end of a function, or set some analysis barrier.

To see all callers to a function in this situation, the recommendation is to navigate to the .got version of it and look at code references, or the .extern version of it which will have one data references from the .got version:

image

In other words, the single-use stub for some function foo() is indeed ugly, but it can be safely ignored in favor of foo()'s GOT entry.

lwerdna commented 1 year ago

This finally closes due to:

Current appearance: image