Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
265 lines (218 sloc) 16.3 KB

The Return of Disassembly Desynchronization


Introduction

Disassembly desynchronization is a well-known anti-disassembly technique used to induce incorrect disassembly. Newer disassembler like Binary Ninja is still vulnerable to it. More established disassembler like IDA has a fix for it, but the fix inadvertently makes another anti-disassembly technique more stealthy and can also be used to hide instructions. In summary, that's what this post will be about. And without further ado, let's start.

Disclaimer: CFG (control-flow graph) recovery and functions identification are still unsolved problems. Obviously, adding anti-disassembly to the mix only makes them even more of a headache. So in no way is this post trying to poke fun at the current state of reverse engineering tools for not handling this particular edge case correctly.

How It Works

Disassembly desynchronization causes incorrect disassembly by placing data bytes at locations that a disassembler will expect to contain instruction bytes. Such magical locations can be found following control-flow altering instructions (e.g. CALL and JCC). For example, execution doesn't necessary have to return to the instruction following a CALL after the subroutine finishes; the subroutine's return address can be purposely altered during subroutine's execution, which grants us the freedom to place data bytes following CALL to disrupt disassembly since execution will never flow there. Same idea with unconditional JMP that is disguised as a JCC instruction, as seen in this assembly code snippet:

...
xor eax, eax
jz always_jump
db 0xeb
always_jump:
...

Although JZ (jump if zero) is a conditional jump instruction, the XOR that precedes it makes sure the branch is always taken. We can insert a data byte after JZ and since genuine instructions should follow either true or false branch, a disassembler might try to disassemble starting from that data byte, causing some numbers of the instructions it disassembles from that point on to be fake.

This technique is well-documented in academic papers and books, most notably Practical Malware Analysis, Ch.15 and The IDA Pro Book 2nd Edition, Ch.21.

Disassembly Desynchronization In Binary Ninja

Below is a simple program utilizing disassembly desynchronization:

section .text
global _start

culprit: 
xor eax, eax
jz always_jmp
db 0xeb ; <---- data byte to throw off disassembly
always_jmp:
ret

_start:
    mov ebx, 1
    call culprit
    xor ebx, ebx
    mov eax, 0x1
    int 0x80

Example Code #1
nasm -f elf32 -o [object] [assembly] && ld -m elf_i386 -o [binary] [object]

And this is how the culprit function looks in Binary Ninja:

Figure 1: Binary Ninja's disassembly of culprit function from Example Code #1

The purposely placed 0xeb data byte (also opcode for JMP) is disassembled as part of jmp __elf_header.header_size+1. The non-existent jump destination causes more data bytes to be misinterpreted as instructions (0x8048029 to 0x8048061) and the _start function to spill into the culprit function (0x8048066 to 0x8048077).

We can hide the _start function from spilling into culprit function by adding another 0xeb data byte right before _start. This results in the disassembly to be even more incorrect as none of the basic blocks steaming from the false branch contains a genuine instruction:

Figure 2: Disassembly of culprit function with added data byte before _start

Back in October of last year, the Binary Ninja team published a blog post called "Automated Opaque Predicate Removal". Opaque predicate is a boolean conditional that always evaluate to the same value; it is the backbone of disassembly desynchronization since we need a disguised conditional statement to cause execution to always jump over data byte. In that post, it talks about how Binary Ninja is able to identify opaque predicate through its dataflow analysis, which can determine whether if the condition for a conditional statement is constant. If we run the automatic opaque predicate patcher that comes with the post, we now see that the disassembly correctly reflects how the code will be executed:

Figure 3: Disassembly of culprit function after running automatic opaque predicate patcher

As wonderful as it is, Binary Ninja's dataflow analysis is not perfect. Toward the end of the post, it lists out some limitations that will cause its dataflow flow analysis to fail to recognize opaque predicate. One limitation is that dataflow analysis will not work on writable segments. So if the predicate is based on value on the stack, Binary Ninja won't try to determine that value. Common C calling conventions like CDECL and STDCALL push function arguments on the stack. To change our previous code so that the automatic opaque predicate patcher won't work properly, we just need to pass in the value of EAX as a function argument:

section .text
global main

culprit: 
    push ebp 
    mov ebp, esp 
    mov eax, dword[ebp+0x8]
    test eax, eax 
    jz always_jmp
    db 0xeb
    always_jmp:
    mov esp, ebp 
    pop ebp 
    ret 

main:
    push ebp 
    mov ebp, esp 
    xor eax, eax 
    push eax 
    call culprit
    mov esp, ebp 
    pop ebp 
    ret 

Example Code #2
nasm -f elf32 -o [object] [assembly] && gcc -m32 -o [binary] [object]

Opaque predicate like the one above is resilient against Binary Ninja's dataflow analysis but it is not stealthy against human detection. A human glancing over the disassembly can still easily tell that EAX will always be zero. To make it stealthy, we need a non-trivial opaque predicate, which can be achieved using algebraic predicate like x(x+1) % 2 == 0.

We can replace our old opaque predicate xor eax, eax with this new one while passing the value for the unknown variable 'x' as function argument to satisfy both resiliency and stealthiness:

section .text
global main

culprit: 
    push ebp
    mov ebp, esp
    mov eax, dword[ebp+0x8]
    add eax, 0x1
    imul eax, dword[ebp+0x8]
    and eax, 0x1
    test eax, eax
    jz always_jmp
    db 0xeb
    always_jmp:
    mov esp, ebp
    pop ebp
    ret

main:
    push ebp
    mov ebp, esp
    xor eax, eax
    push eax
    call culprit
    mov esp, ebp
    pop ebp
    ret

Example Code #3
nasm -f elf32 -o [object] [assembly] && gcc -m32 -o [binary] [object]

Disassembly Desynchronization In IDA

Older versions of IDA are vulnerable to disassembly desynchronization too, but it seems like the newest free version of IDA (v7.0) has the problem fixed:

Figure 4: IDA is able to automatically recognize the purposely misplaced data byte

How about let's make sure the false branch is always taken? Will IDA try to disassemble the data byte then? Let's find out:

Figure 5: IDA doesn't try to disassemble data byte even though it will be reached during execution

Hmm it doesn't... It seems to me that IDA isn't using dataflow analysis like Binary Ninja to identify that only one of the branches can be taken; rather, it is probably using some forms of pattern matching against typical disassembly desynchronization code pattern and making the assumption that if the pattern matches then disassembly desynchronization must be at play so it only disassembles from the destination of the true branch.

Can we exploit this newfound assumption made by IDA? Yes we can.

We can use this assumption to make IDA not disassemble genuine instruction by placing genuine instruction at destination of the false branch. To accomplish this, we also need to overlap instructions at both true and false branches:

section .text
global _start

culprit: 
xor eax, eax
jnz not_jmp
db 0xb8 ; <----- if disassembled from here inserted data bytes become: mov eax, 0xc8d1c031
not_jmp:
db 0x31 ; <----- if disassembled from here inserted data bytes become: xor eax, eax; ror eax, 0x1
db 0xc0
db 0xd1
db 0xc8
ret

print_result: 
mov ebx, 0x1
mov eax, 0x4 ; sys_write
int 0x80
ret

_start:
    call culprit
    test eax, eax
    jnz not_zero
    ; "EAX is zero"
    mov ecx, zero
    mov edx, zeroLen
    jmp time_to_print
not_zero:
    ; "EAX is nonzero"
    mov ecx, nonzero
    mov edx, nonzeroLen
time_to_print:
    ; print "EAX is zero" if culprit's return value is zero
    ; print "EAX is nonzero" otherwise
    ; IDA's disassembly of culprit will make it look like "EAX is zero" will be printed
    ; But when executed, "EAX is nonzero" is the one that's printed
    call print_result
    xor ebx, ebx
    mov eax, 0x1 ; sys_exit
    int 0x80

section .data
    zero db "EAX is zero", 0xa
    zeroLen equ $-zero
    nonzero db "EAX is nonzero", 0xa
    nonzeroLen equ $-nonzero

Example Code #4
nasm -f elf32 -o [object] [assembly] && ld -m elf_i386 -o [binary] [object]

The culprit function will always return a nonzero value even though IDA's disassembly of culprit makes it look like it will always return zero (obviously the xor eax, eax instruction is a dead giveaway that the disassembly is wrong but we can easily replace that with a non-trivial opaque predicate as discussed in the previous section):

Figure 6: IDA's disassembly of culprit shows that it will return 0 when in reality it returns a nonzero value

This is because the bytes right after the jnz not_jmp instruction in culprit, 0xb831c0d1c8, disassembles to the instruction mov eax, 0xc8d1c031. If we skip 0xb8 and disassemble starting from the not_jmp label then it disassembles to xor eax, eax; ror eax, 0x1 as displayed by IDA. But since the jump will never take place, mov eax, 0xc8d1c031 is the instruction that will be executed.

One thing I really liked about IDA is the ease of updating the disassembly with the c (code) and d (data) hotkeys that allows you to change a portion of bytes from code to data or vice versa. To fix the disassembly, we use the d hotkey to change the xor eax, eax; ror eax, 0x1 instructions to data and c hotkey to disassemble from the 0xb8 byte. The disassembly now correctly reflects execution:

Figure 7: IDA correctly shows culprit's disassembly after manually fixing it with the 'c' and 'd' hotkeys

Aside from hiding instruction, we can also use IDA's assumption to make genuine overlapped instructions more stealthy.

Normally IDA will not be able to display overlapped instructions. Instead, it will highlight it for the user in red to let user know that something funky is going on:

Figure 8: Overlapped instructions in IDA

The example above is taken from Practical Malware Analysis, Ch.15. The bytes in questions are 0xebffc048. 0xebff will translate to jmp -1 (shown as jmp short near ptr culprit+1 in IDA), meaning that the instructions that follows will be in the middle of the JMP: 0xffc048 or inc eax; dec eax. The problem, as shown in Figure 8, is that IDA can't displays this assembly sequence since 0xff is a part of both JMP and INC.

To make the use of overlapped instructions more stealthy, we can use IDA's fix for disassembly desynchronization to only display one part of the overlapped instructions while completely hiding the other one:

culprit: 
test eax, eax
jnz it_depends
db 0xb0 ; <----- if disassembled from here inserted data bytes become: mov al, 0x1; ret
it_depends:
db 0x01 ; <----- if disassembled from here inserted data bytes become: add ebx, ecx
db 0xc3 ; <----- also opcode for ret
ret

_start:
    ; assumes there's code here that sets eax in a non-trivial way...
    call culprit
    ; does something with the result
    xor ebx, ebx
    mov eax, 0x1 ; sys_exit
    int 0x80

Example Code #5
nasm -f elf32 -o [object] [assembly] && ld -m elf_i386 -o [binary] [object]

Figure 9: Although 'mov al, 0x1' can also be executed, it is not disassembled

Both the true and false branch in culprit can be taken. If the false branch is taken, 0xb001c3 or mov al, 0x1; ret will be executed and since conveniently 0xc3 is the opcode for ret it will just return to the caller right after the assignment. And if true branch is taken, 0x01c3 or add ebx, ecx will be executed instead, also follows by a ret. Yet, IDA can only show the branch leading to add ebx, ecx, without giving any hint that actually both branches can be taken.

Conclusion

Whether you are using disassembly desynchronization to mess with disassembly, to hide instructions, or to hide overlapped instructions, its resiliency and stealthiness are mainly dependent on the employed opaque predicate.

More on opaque predicate in the next post. Stay tuned!

You can’t perform that action at this time.