Skip to content

Latest commit

 

History

History
601 lines (384 loc) · 18.4 KB

File metadata and controls

601 lines (384 loc) · 18.4 KB

Breaking Decompilers

...or, How To Make Jordan's Life Hard


Note: I'm Jordan!


Note: I'm one of the developers behind Binary Ninja. I also believe the best talks have the shortest biography sections.


You?

Who has...

  • written C?
  • used a debugger?
  • used a decompiler?
  • written a plugin for a decompiler?
  • written a decompiler?

Note: I have the coveted "after lunch" spot where everyone is usually very sleepy so let's see if we can wake up by moving a little bit. I'd like some audience participation. By a show of hands, please show...


Goals

After this talk, you should:

  • understand more about how decompilers work and thus,
  • have lots of ideas on how to break them
  • know a bunch of concrete examples that will break all tools

Note: Goals for this talk.


Anti-Goals

  • Not breaking debuggers
  • Not an exhaustive list of all possible techniques

Note: While this talk isn't explicitly about breaking debuggers and we're not intentionally targeting them, it's worth noting that most debuggers are also disassemblers and have to parse files so several of these techniques will still apply. However, there are other techniques that are only applicable in dynamic analysis situations.


Outline

  • Why Decompilers Are Impossible
  • How Decompilers Work At All
  • (╯°□°)╯︵ ┻━┻

Why Decompilers Are Impossible


Informally

  • Comments
  • Symbol Names (in a stripped binary without debug info)

Note: these are clearly and obviously lost but it's more than that, for example:


Informally

#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
  int fd = open(argv[1], O_RDONLY);
  size_t size = lseek(fd, 0, SEEK_END);
  int prot = PROT_EXEC | PROT_READ;
  void *mem = mmap(NULL, size, prot, MAP_PRIVATE, fd, 0);
  ((void(*)())mem)();
  return 0;
}

Note: Can you tell me all possible things this program will do?


Formally

https://en.wikipedia.org/wiki/Rice's_theorem

Note: Rice's Theorem is an extension to the halting problem that basically proves that not only is it impossible to always be able to tell whether a program terminates, but you can't tell ANY non-trivial property of the program. So by this logic, we should never be able to perfectly analyze any program.


Code? Data?








Note: One of the hardest problems in computer science is "what is code" and "what is data". This isn't maybe super obvious since we're all used to just seeing analysis tools scan and find the code but for a whole bunch of reasons I'm not going to go into now, this is actually a very difficult problem.

Definitely some folks who have researched this but there's plenty of folks who also just blindly assume it's solved:


But...

Note: You all saw the outline, and you are almost certainly aware that decompilers exist. So how do I reconcile my two statements?

Thankfully, we don't have to be perfect, we just have to work most of the time. Most decompilers are written to operate best on standard compiler code which is much better formed than "all possible code" and therefore many shortcuts/heuristics can be taken. Often, these assumptions are the very things we'll be abusing.


How Decompilers Work At All


How Decompilers Work At All

  • Parsing
  • Lifting
  • Optimizing

Note: Sound familiar? No, really, anybody? Does this sound like any other programming technology?


How Decompilers Work

  • Parsing
  • Lifting Lowering
  • Optimizing

Note: What about now?


Note: Yup, that's exactly how a compiler works. It turns out, that there's a ton of similarity between building a compiler and a decompiler. Which makes sense since they're both having to translate from one representation into another.


Parsing

Universal:

  • Memory Mappings
  • Entry Point
  • Exports / Imports
  • Control Flow / Code Discovery

Note: So let's look at some opportunities of what parsing, lifting, and optimizing look like in terms of attack surface to break decompilers. Someone actually asked me yesterday if this was more about breaking decompilation or disassembly, and it was a good question. We'll be focusing on both as you can tell from this list!

Control Flow / Code Discovery could be considered partially a lifting problem, partially a parsing problem, so we'll list it in both places.


Parsing

Specific:

  • Mach-O / PE / ELF
  • Section Information
  • Metadata
  • Relocations

Lifting

Note: Way more than I can cover here, check out one of my previous talks for more info. Another phrasing for this might have been "translating", there's a translation step from native architecture to some intermediate representation. "P-Code" origin?


Optimizing

  • Applying type information
  • Matching signatures
  • Dead Code Elimination
  • Resolve Indirect Control Flow
  • Higher Level Control Flow Structures

Note: and several other topics worthy of an entire semester worth of advanced CS classes.

This is in particular where we're targeting the decompiler aspect specifically even though the previous steps are also pre-requisites for decompilation they will also break disassemblers.


Evaluation Criteria

Note: Before we actually break things, let's talk about the different properties we might care about regarding obfuscation.


Effective

How much does it prevent analysis/understanding?


Effective

Strength of Effect on understanding/analysis:

  1. Ineffective
  2. Somewhat Effective
  3. Moderately effective
  4. Very effective
  5. Extremely effective

Note: higher is better, so the higher the number, the more effective the obfuscation is at breaking analysis


Evident

How obvious is it?


Evident

  1. Blatant
  2. Clear
  3. Noticeable
  4. Subtle
  5. Hidden

Note: This is one of the most important attributes as too often people just want to make it hard to reverse something, but what is far more valuable is subtly breaking a decompiler. That, used judiciously, can cause far more trouble than the world's most opaque VM implementation.


Effort

How much work is it to implement?


Effort

  1. Trivial
  2. Light
  3. Moderate
  4. Demanding
  5. Grueling

(╯°□°)╯︵ ┻━┻

Note: Ok, with that out of the way, let's actually go break some decompilers.


Base Challenge

#include <stdio.h>
#include <string.h>

int main() {
    char input[100];
    printf("Enter the password: ");
    fgets(input, sizeof(input), stdin);
    if (strcmp(input, "correct") == 0) {
        printf("Access granted!\n");
    } else {
        printf("Access denied!\n");
    }
    return 0;
}

Note: not quite exactly what we're using, but close enough and fits on a slide. We're going to take this same challenge and apply a bunch of our techniques to it and see how it looks after each.


Examples

  • Break Parsing
    • Relocations
    • Sections/Segments
  • Break Lifting
    • Alignment
    • Vectorization
  • Break Optimizations
    • STOP 🛑
    • Packers
    • SCC
    • Dataflow

Break the Parsing


Relocations

Relocations are the worst

Effective5
Evident4
Effort1

Note: A pain to implement, I'm not even going to make any examples, but this is probably one of the most under-appreciated tools to use to break decompilers, in part because every architecture and platform implements them differently and there are way way more than you think.


Section Shenanigans

Effective4
Evident4
Effort3

Note: Many folks have created variants by this. It was pointed out to my by zetatwo who worked with bluec0re for the example documented in PagedOut 5, and it was also documented in an earlier PoC||GTFO


Demo!

./examples/sections


Build Your Own!

  1. Fuzz the file, run it.
  2. If it still works, dump the decompilation and pattern match.
  3. GOTO 1

Note: There are an infinite number of discrepencies between loaders and decompilers. All it takes is playing around with new features and you can find a new break here pretty easily.


Break the Lifting


Alignment

eb ff c3
eb ff c3
jmp $-1 ret
eb ff c3
inc ebx

Note: This example shows how the same byte sequence can be interpreted in multiple ways. The bytes "eb ff c3" can be read as two different instruction sequences depending on the disassembly technique.


Alignment

Effective3
Evident3
Effort5

Note: oldest trick in the book, still breaks IDA super easily! At one point they "fixed" it by matching the exact byte pattern match (turns out, they only did it on x86, x64 is still vulnerable to the same thing)

Ghidra used to be vulnerable but fixed it at some point.


Demo!

./examples/alignment

🐶⚡️ Link

Note: I have no idea what IDA is doing here. It simply refuses to decompile it at all.


Vectorized

Just use an instruction that is rare and not implemented, or is incorrectly lifted.

Effective5
Evident1
Effort3

Note: similar in terms of effectiveness to mis-aligned instructions, depends on the tool and how it gets the lifting wrong. More work than the mis-aligned instructions because you have to find the instructions first, but you can probably just go trolling through libraries or bug reports for Binja or Ghidra. Or just use consensus evaluation and disassembly a single instruction at a time in LOTS of tools. Does require normalization though which can be a headache. That said, relatively easy to fix on the architecture/parsing side.


Demo!

./examples/vectorized

🐶⚡️ Link


Break the Optimizations


STOP 🛑

Effective3
Evident2
Effort2

Note: The particular optimization being abused here is in the "no-return" property as well as the fact that IDA's heuristic for when to apply the type is permissive. Of course, on the flip-side, there are potentially binaries where static signatures don't apply where's IDA's heuristic might result in better analysis. Most of these are simply choices that the tools make to decide what they think is the best default case but they can easily be abused.


Demo!

./examples/stop

🐶⚡️ Link


UPX

Effective4
Evident4
Effort5

./examples/upx


Demo!


SCC

Our newly open-sourced Shellcode Compiler supports many built in obfuscations.

Effective4
Evident2
Effort5

Note: Has many built-in obfuscations. But really this is just a substitute for any obfuscating compiler. In this case I'm just using a single option to use a different register for the stack pointer. That's it, but because this is a non-standard compiler technique, any decompilers that assume the stack pointer must be EBP/RSP will have lots of trouble.


SCC

https://scc.binary.ninja/scc.html


Demo!

🐶⚡️ Link


Dataflow Propagation/Memory Permissions

Effective4
Evident4
Effort4

Demo!

./examples/dataflow

🐶⚡️ Link


Bonus: Breaking LLMs

"Multi Level Marketing Model"

Note: Time permitting, share example of MLMM and how it turned out it wasn't even needed!


Summary of Techniques

<style> .reveal table { font-size: 0.7em; } </style>
Technique Effectiveness Evident Effort
Section Shenanigans 4 4 3
Relocations 5 4 1
Alignment 3 3 5
Vectorized 5 1 3
STOP 3 2 2
UPX 4 4 5
SCC 4 2 5
Dataflow/Permissions 4 4 4

Note: Higher scores are better in all categories.


Conclusion

  • What are your goals?
  • Do you care more about increased difficulty or subtle breakage?
  • Decompilers are easy to break, hard to make.

Credits / Acknowledgements

Note: We all stand on the shoulders of giants, I'm sure none of these techniques are new, thanks to everyone else who has done similar research in the past, sorry for not running down each instance, if I had I was afraid I would miss some other earlier example!


Questions?

Note: Please note that I'd love feedback if you have other examples of things that break binary ninja besides this. As a side-note, we do treat