# Binary Ninja Workflows: Fixing branch obfuscation

If you've been reversing x86/x64 for a while then you will have definitely come across functions that end like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1698142698577/c5bac9cd-87d6-48cf-b4fc-4c16c2719a5f.png align="center")

We know two things here:

* The `RET`/`RETN` opcode in x86/x64 pops the stack and jumps to that address
    
* The `PUSH` before the return here is actually the address we're about to jump to
    

It's normal for a disassembler explore a function, traversing all branches, and terminate each when it gets to a `RET` opcode. For a decompiler/lifter it's also generally important to trust `RET` opcodes and turn them into `return` statements, and this is why it's using `PUSH/RET` combinations is a good obfuscation tool.

## A sample app

We'll start with a simple app that we can load up into Binary Ninja, [source code is here](https://github.com/samrussell/pushret/blob/master/pushret.asm). As we can see, it picks up the initial function right through to the `RET` opcode and then stops, and it doesn't find the rest of the function:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1698222536586/60375cff-0053-420f-82a5-5c7383da3021.png align="center")

This is quite confusing from the high level IL, as it looks like it's calling a syscall and returning the response. There's a var declared that isn't used either, this is kind of weird. The low level IL makes it a bit more clear what is happening here:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1698222615342/c448abee-d6ca-4ab2-909c-21385230231b.png align="center")

I initially thought that the LLIL had actually interpreted the push/pop/jump correctly, but this is actually just the notation for printing a return statement. In practice it's not being clever here, it literally just has a return statement on line 6.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1698223829286/c55dfc21-0789-4c5c-aa2c-57801f7f42bb.png align="center")

What we want to do is hook the LLIL and convert this `LowLevelILRet` instruction into something that the higher level ILs can handle properly. We'll look into 2 potential options, but first, let's take a look at how to build a basic workflow to hook into the analysis and make our own changes.

## Binary Ninja workflows for hooking LLIL

The way that we hook into the Binary Ninja lifters is by using their [Workflow API](https://docs.binary.ninja/dev/workflows.html). The BN team recommended that I do try this in C++ and definitely don't try this in Python. They are almost certainly correct, but I also wanted to see how the Python API works and it seems to be okay. You have been warned though.

### Here be dragons

!["The Dragon Book": Compilers, Principles, Techniques and Tools](https://images-na.ssl-images-amazon.com/images/S/compressed.photo.goodreads.com/books/1387666736i/703102.jpg align="center")

Workflows are disabled by default, and the BN team describe it as an "Early Feature Preview", so expect things to change in future versions. I'm doing this in Binary Ninja version 3.5.4526 so if you're on a different version the there's a good chance these examples might break.

### Replacing fake returns with tailcalls

```python
pwf = Workflow().clone("PopRetTailcallWorkflow")
def interpret_popret(analysis_context):
	# iterate over llil basic blocks
	updated = False
	for block in analysis_context.llil.basic_blocks:
		# check if we have push;ret
		if len(block) >= 2 and isinstance(block[-1], LowLevelILRet) and isinstance(block[-2], LowLevelILPush):
			# replace push with a tailcall
			analysis_context.llil.replace_expr(block[-1], analysis_context.llil.tailcall(block[-2].operands[0].expr_index))
			analysis_context.llil.replace_expr(block[-2], analysis_context.llil.nop())
			updated = True
	# we need to redo the ssa then
	if updated:
		analysis_context.llil.generate_ssa_form()

pwf.register_activity(Activity("extension.popretworkflow.interpretpopret", action=interpret_popret))
pwf.insert("core.function.generateMediumLevelIL", ["extension.popretworkflow.interpretpopret"])
pwf.register()
```

Most of this is copied from the [sample python workflow](https://docs.binary.ninja/dev/workflows.html#python) and takes inspiration from [c++ tailcall example](https://github.com/Vector35/binaryninja-api/blob/7763b7ab173151aa01b017e1902e1de716f2a1ee/examples/workflows/tailcall/tailcall.cpp). We'll go through step by step to see what it does:

```python
pwf = Workflow().clone("PopRetTailcallWorkflow")
def interpret_popret(analysis_context):
	# ...

pwf.register_activity(Activity("extension.popretworkflow.interpretpopret", action=interpret_popret))
pwf.insert("core.function.generateMediumLevelIL", ["extension.popretworkflow.interpretpopret"])
pwf.register()
```

Here we create a new `Workflow` object called `PopRetTailcallWorkflow`. We register an `Activity` called `extension.popretworkflow.interpretpopret` and point it at our new `interpret_popret` function. Once this is set up, we need to insert it into the current workflow. We can use `pwf.show_topology()` to see what the base workflow looks like:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1698388703202/d8db0929-2f80-4bcd-890f-9ad47b5cc6f9.png align="center")

If we zoom in on the lower half we see this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1698388736109/cb76eb17-03ed-4b5d-be55-371b5ec922b5.png align="center")

I am guessing here but I'm assuming we want our workflow to happen before the MLIL gets generated, so I've set up my workflow to be inserted just before this:

```python
pwf.insert("core.function.generateMediumLevelIL", ["extension.popretworkflow.interpretpopret"])
```

Our function for the workflow is fairly straightfoward:

Iterate over very LLIL basic block

```python
for block in analysis_context.llil.basic_blocks:
```

Check if the final two instructions are a `PUSH` followed by a `RET`:

```python
if len(block) >= 2 and isinstance(block[-1], LowLevelILRet) and isinstance(block[-2], LowLevelILPush):
```

We want to replace this with a LLIL tailcall, which means replacing one of the instructions with this and replacing the other with a `NOP` (I couldn't find a way to delete existing LLIL instructions)

```python
analysis_context.llil.replace_expr(block[-1], analysis_context.llil.tailcall(block[-2].operands[0].expr_index))
analysis_context.llil.replace_expr(block[-2], analysis_context.llil.nop())
updated = True
```

The nice thing with this is that we don't care if the `PUSH` is a constant or a register, we just take the expression index out of the `PUSH` instruction and put it in the tailcall and Binary Ninja does the rest for us.

## Testing out the tailcall replacement

You can load this as a plugin, but you can also just paste this in the python console and it will register the workflow:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1698663720631/88c096cf-26eb-4288-9ceb-86d9f930660e.png align="center")

We also need to make sure workflows are turned on in settings (Edit-&gt;Preferences-&gt;Settings) and tick the checkbox for "Enable the analysis orchestration framework":

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1698663777338/901aeb00-bb65-48b3-a939-14a700a0fdac.png align="center")

Then you need to reload your binary to kick off the framework, go to File-&gt;Open with Options, load your binary, and then choose your new workflow in the next dialog that pops up:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1698663858645/3baee700-f142-4ba7-bf40-678245a29cc6.png align="center")

HLIL looks a bit different now:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1698663897984/ae2b1f13-9b28-4599-8742-fe3d5ce7292f.png align="center")

It doesn't automatically create a code section for us (exercise for the reader?) but we can also just follow the jump and convert it to a function (sometimes pressing P doesn't work and you need to actually change the type with Y and then it will decompile it correctly):

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1698664054965/34310dec-27f6-4164-9557-852d69112368.png align="center")

And if we look at the LLIL we can see where our new instructions took place:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1698664089449/ff5fc6f7-b57e-4ab1-97ef-6dd79ca980fa.png align="center")

I have a second binary, [stackops](https://github.com/samrussell/pushret/blob/master/stackops.asm), that looks like this with the default workflow:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1698664186347/faaaa4d1-27de-4ed7-b65d-ee0a8a1b3c04.png align="center")

But the HLIL comes out like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1698664205069/2b347b31-525b-4c27-8103-fbb98f64dd27.png align="center")

If we load with our new workflow it correctly catches the indirect jump:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1698664239017/a5dc334f-6256-49e3-ac90-decb2ad69cfb.png align="center")

As with the above example, we need to then convert these addresses to functions so we can analyse from there:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1698664295059/56e1c83d-09b4-4d2f-a68a-95e5f0edde2e.png align="center")

## Next steps

Solving this simple `PUSH;RET` case is a start, but this can easily be foiled by inserting junk instructions before the final `RET` instruction. There are some other issues too:

* The indirect jump might not be a separate function, but just an obfuscation inside an existing function. We might want to make a `JMP` instead of a tailcall
    
* If we're jumping to something that wasn't declared as code then we might want to mark it as code and kick off analysis there
    
* There are multiple ways of manipulating the stack before making a `RET`, so full stack analysis could tell us if we're actually making a fake `RET` or a real one
    

Let me know if you take this concept and extend it, this just scratches the surface and there are lots of options for taking it further.

Happy hacking everyone!
