Working with Functions

Check out our latest YouTube Short, “Working with Functions!”


If you haven’t already seen, we started a new training playlist on YouTube! We’ll be posting new shorts and videos there from time to time, so make sure to subscribe and hit that bell!

Self-promo aside, let’s talk about functions in Binary Ninja.

Many functions, same bytes.

Functions contain bytes, but the real “owner” of those bytes is the BinaryView. This allows us to accommodate situations where the compiler might choose to optimize for size by reusing the same series of bytes across several functions, or where the same set of bytes form a valid function across multiple architectures. So while binary views have a default architecture, you can create functions of any architecture that Binary Ninja supports and read their decompilation side-by-side. This tends to be most useful when analyzing ARM binaries with Thumb functions in them. You can also leverage our multi-architecture support to analyze binaries with embedded virtual machines or emulators, like those that Xusheng alluded to in one of his blog posts.

To create a function in Binary Ninja, simply select the start address where you think a function should be and press p.

To create a function of a non-default architecture in a binary, first right-click the first byte of the function, then find Make Function at This Address, and choose the architecture you want.

If it turns out that the function isn’t valid, you can always ⌘-z/ctrl-z! Or, find the Undefine Current Function action in the command palette or right-click menu.

Function Properties

Right-click in a function and find the Edit Function Properties option. From here, you get a lot of control over different aspects of a function.

Edit Function Properties Dialogue

Calling Conventions

One neat trick most people don’t know is how to overload an argument’s register when the compiler decides to break calling convention.

For example, consider the function size_t myCustomStrlen(const char* str). Let’s say that the compiler decides to use RAX for the first argument instead of RDI. If you select the function and press y, you can change the type of the function.

In order to overload the register being used for the first argument to be RAX, simply change the type to size_t myCustomStrlen(const char* str @ RAX). Binary Ninja will understand the @ syntax to be a register override, and update the decompilation appropriately.

Additionally, if there’s a specific call of a function that doesn’t look right, you can use the Override Call Type action. This will allow you to override the type of a function at a specific call site, which tends to be especially useful when working with variadic functions that don’t want to follow the calling convention. Simply change the type of the function to whatever it should be for that specific call (including any necessary register overrides!), and Binary Ninja will handle the rest.

Function size

Now you might’ve wondered how large a function is before. This is apparently a highly contested topic that I’ll let Jordan cover for you.

API

We pride ourselves on our extremely ergonomic API, but not everyone knows how to use it! I think that’s a shame, so let me help you write your first script.

You can access the Python console by pressing “`” on your keyboard or going to ViewPython Console. As I mentioned earlier, your BinaryView is what owns the actual bytes of the binary, and Functions are a sort of container that display code in the UI, as well as provide a number of other helper functions. Thankfully, when you have a file open in the UI, we provide the magic console variable of bv which maps to the binary view for the file you currently have open (it’s contextual and tracks the current tab you have open as well!).

Using this magic variable, you can iterate through the functions in your binary using the following snippet:

for func in bv.functions:
  func

That will print all the functions in your binary to the console. If we want to make this do just a little more for us, we can find all the functions in our binary that have no side effects:

for func in bv.functions:
  if func.is_pure:
    func

From here, the sky’s the limit! You can check out another one of our YouTube videos where we delve further by inspecting the decompilation of each function, looking for a specific vulnerability pattern in the Windows kernel.

Training

If you like these sort of tips and tricks, consider checking out one of our upcoming trainings! We cover a ton of topics just like this in our beginner and intermediate classes, as well as much more complex topics from recovering classes and inheritance patterns to modeling vulnerabilities for automated detection with hands-on, real-world examples.

We’re excited to have just announced a ton of trainings for 2024! If you don’t want to miss any of our upcoming class announcements, consider joining our training newsletter!

Article Link: Binary Ninja - Working with Functions