Binary Ninja offers multiple views of the code, each with an API that gives you the same access that the GUI has. The different views vary in how much they are like assembly or C. Only that last step, real C code, is still missing. Those other views are quite good if your goal is to understand things, but less good if you were hoping to throw the results into a C compiler.
Anybody use SEH or MSVCRT exceptions on x86? Well, there are non-inlined functions that adjust the stack pointer dynamically there. Binary Ninja can't capture that. To be fair, it's unlikely IDA can either- but IDA has a heuristic (read- hack) that treats those functions specially. Result? SP-analysis for all callers generally fails, and Binja becomes convinced that arguments are being passed in eax and ebp.
Ah, but you can just patch the LLIL for calls to those functions to adjust the stack. Oh, no, you actually can't patch LLIL that way- it's immutable after the lifter creates it. Now, you can write your own architecture hook, and there you can be your own lifter- you can call the real lifter, see if it emits a LLIL_CALL to a function you recognize, and if so just emit the stack adjustment LLIL instead. Ah, heh, but you can't- you can't call the real lifter, because it doesn't emit LLIL, it adds LLIL to an existing function, and you can't remove that IL later- it's append-only. And you can't recognize functions easily, because the things passed into your GetInstructionLowLevelIL callback don't include a BinaryView pointer- the thing you'd need to find out anything at all about other functions. You can sort of, kind of, hack around this by calling about five other functions... for every CALL instruction in every function in the binary. This is, ah, less than performant.
Ever reversed a Win32 binary that uses the Win32 API a lot? I hope you like defining structs by hand, because OH BOY are you going to be defining a lot of structs to do anything useful. And you also get to define DWORD, LPDWORD, LPVOID, and every other annoying Windows typedef by hand. (You can be clever and use libclang hackery on the Windows SDK and automate some of this. But you'll have to do it yourself.)
Then there's stuff like type propagation only going forwards inside functions- sometimes. The GUI occasionally deciding that all basic blocks should be laid out in one small square, on top of each other. (You have to reanalyze the function to fix this.)
Mind you, I love Binary Ninja- I bought my own dang commercial license, and renewed it! It's getting better, fast... but it's got its warts.
If you're on the dev branch of binja (which, at least until recently, was miles ahead of stable), you get to do this again in a few days when binja updates and throws out all its old cached information.
Also, saving and loading massive databases can easily be a 5-minutes-or-more process. Again- this does provide you with ample time to explore the area around your office building, but still.
(Mind, this isn't a problem if you mostly see small binaries- for malware it's probably entirely fine.)
For instance, one of the most useful aspects of a decompiler for me is the ability to recover high-level control flow, which Binary Ninja apparently doesn't support. Instead it gives you an IDA-like graph view (but with IL instead of assembly in the graph nodes); but at least in my experience using IDA without a decompiler, even moderately large functions tend to result in a spiderweb of a graph, and recovering the control flow by hand ends up feeling like a pointless brain teaser. (This condition being true short-circuits this set of comparisons, so it must be an ||... but it still ends up doing this other set of comparisons, which you can also get to from... wait, where was I again?)
It also doesn't seem to allow eliding temporary assignments. Here's a short sample from some random function (retyped by hand since I don't see a way to copy and paste):
int32_t edx = arg4
int32_t eax = esi
eax_1, ecx_2 = sub_3c670(eax, edx)
It does do a sort of SSA transformation and assign unique variable names (like eax_1 instaed of eax) to the same register based on the location in the program, so that's nice. But what I really want is eax_1, ecx_2 = sub_3c670(esi, arg4)
I may be missing some option to do this manually, but it should be automatic.Yeah, there's no copy propagation for MLIL yet. I think they're saving that for HLIL, for some reason. It's exactly as obnoxious as you think it is, though. (For example- click on a variable name. Now other uses are highlighted. Ah, but when 80% of the other uses are just the right hand of assignments, which are then used... you get to trace through that fun chain by yourself!)
There's a community plugin to kind of try to fix this, by actually renaming the intermediate variables to match the RHS's name. This works, sometimes, but is written in Python, which means it's single-threaded and slow, and occasionally it will get stuck in a loop, and sometimes it decides that it wants to rename everything to "ecx_1" or something, in which case you become very grateful that undo exists.