4

Our team recently had to look at SPARC assembly specifications, the problem is that I do not have a SPARC processor to try things on it (I should set up a simulator or get one of these old Sparc station on my desk... I know).

Somehow, the ba (branch always) instruction puzzled me because it is a delayed branch execution instruction. This mean that the instruction located just after the ba get executed before the jump occurs.

One of my colleague raised a very interesting question, what does occur in this case:

0x804b38 ba 0x805a10
0x804b3c ba 0x806844
...
0x805a10 add %r3, %r2, %r5
...
0x806844 sub %r3, %r5, %r2
...

Our guess, following the specifications, is that the run should behave like this:

0x805a10 add %r3, %r2, %r5
0x806844 sub %r3, %r5, %r2
0x806848 ...

Which means that you can probably jump and pick up one instruction inside a block of others and run to the next ba... I wonder what the CFG would look like.

Whatever, it was the "simple" case, what if we have dynamic jumps (the jmp instruction is like a ba but based on the address stored in the given register):

0x804b38 jmp %r3
0x804b3c jmp %r0
...
(%r3)    change %r0
...

Would it be a good way to mislead a static-analyzer ? Or, is there a way to have an easy computation to guess what it is doing ?

  • do you have more code, e.g. a full binary? I have a SPARC to try it on ;) – 0xC0000022L Apr 29 '13 at 17:58
  • Sorry, I just speculate based on the specifications... :-/ I need to get a sparc and a complete build chain quickly. I have to try. – perror Apr 29 '13 at 18:01
  • @0xC0000022L I deleted my comment, I didn't read the question till the end... – Mellowcandle Apr 29 '13 at 18:35
8

I edited the whole example a little so that it would better match the question.

My SPARC assembly fu is weak, but what I did was write a little "Hello world" with a twist (or one could say with jumps/gotos) in C and use gcc -S to translate it to assembly. I have a SPARC on which I am running it, details:

$ isainfo -v
64-bit sparcv9 applications
        vis2 vis
32-bit sparc applications
        vis2 vis v8plus div32 mul32

NB: b is the same as jmp, it's just a different mnemonic for the same thing, really. One takes an immediate value (b), the other a register (jmp).

It turns out that what the link you gave is true for GCC:

Notice that the last instruction executes before the jump takes place, not after the subroutine returns. This first instruction after a jump is called a delay slot. It is common practice to fill the delay slot with a special operation that performs no task, called a no-operation, or nop.

Real life test

I reckon we need to do this with and without debugger, because it's not clear whether it might behave differently under a debugger. So the code should output something readable so we can see what kind of effect our tinkering has ;)

C code

#include <stdio.h>

int foo(int argc)
{
        switch(argc)
        {
        case 0:
        case 1:
                goto a1;
        case 2:
                return 3;
        case 4:
                goto a2;
        case 5:
                return -1;
        default:
                goto a4;
        }
a1:     return 1;
a2:     return 2;
a4:     return 4;
}

int main(int argc, char** argv)
{
        printf("Hello world: %i\n", foo(argc));
        return foo(argc);
}

This gives me plenty of branch instructions to play around with the idea raised in the question.

Assembly created by gcc -S

Here's the assembly before I tinkered with it:

        .file   "test.c"
        .section        ".text"
        .align 4
        .global foo
        .type   foo, #function
        .proc   04
foo:
        !#PROLOGUE# 0
        save    %sp, -120, %sp
        !#PROLOGUE# 1
        st      %i0, [%fp+68]
        ld      [%fp+68], %g1
        cmp     %g1, 5
        bgu     .LL11
        nop
        ld      [%fp+68], %g1
        sll     %g1, 2, %i5
        sethi   %hi(.LL12), %g1
        or      %g1, %lo(.LL12), %g1
        ld      [%i5+%g1], %g1
        jmp     %g1
         nop
.LL6:
        mov     3, %g1
        st      %g1, [%fp-20]
        b       .LL1
         nop
.LL9:
        mov     -1, %g1
        st      %g1, [%fp-20]
        b       .LL1
         nop
.LL5:
        mov     1, %g1
        st      %g1, [%fp-20]
        b       .LL1
         nop
.LL8:
        mov     2, %g1
        st      %g1, [%fp-20]
        b       .LL1
         nop
.LL11:
        mov     4, %g1
        st      %g1, [%fp-20]
.LL1:
        ld      [%fp-20], %i0
        ret
        restore
        .align 4
        .align 4
.LL12:
        .word   .LL5
        .word   .LL5
        .word   .LL6
        .word   .LL11
        .word   .LL8
        .word   .LL9
        .size   foo, .-foo
        .section        ".rodata"
        .align 8
.LLC0:
        .asciz  "Hello world: %i\n"
        .section        ".text"
        .align 4
        .global main
        .type   main, #function
        .proc   04
main:
        !#PROLOGUE# 0
        save    %sp, -112, %sp
        !#PROLOGUE# 1
        st      %i0, [%fp+68]
        st      %i1, [%fp+72]
        ld      [%fp+68], %o0
        call    foo, 0
         nop
        mov     %o0, %o5
        sethi   %hi(.LLC0), %g1
        or      %g1, %lo(.LLC0), %o0
        mov     %o5, %o1
        call    printf, 0
         nop
        ld      [%fp+68], %o0
        call    foo, 0
         nop
        mov     %o0, %g1
        mov     %g1, %i0
        ret
        restore
        .size   main, .-main
        .ident  "GCC: (GNU) 3.4.3 (csl-sol210-3_4-branch+sol_rpath)"

I'll concentrate on modifying the result of foo(), so I won't repeat all of the assembly code again but instead only bits and pieces.

btw: GCC created the extra indentation for the nop instructions, but it makes it easy to spot them, of course.

Steps to get from C to executable with tinkering involved

Here are the steps to get to the modified program.

  • use gcc -S test.c to get a test.s file
  • modify the test.s
  • Assemble it with gas -o test.o test.s
  • Link with GCC using gcc -o test test.o

Modifications to the assembly code

First, I felt compelled to "optimize" the instructions in LL6, LL9, LL5, LL8, LL11 and LL1 like this:

.LL6:
        mov     3, %i0
        b       .LL1
         nop
.LL9:
        mov     -1, %i0
        b       .LL1
         nop
.LL5:
        mov     1, %i0
        b       .LL1
         nop
.LL8:
        mov     2, %i0
        b       .LL1
         nop
.LL11:
        mov     4, %i0
.LL1:
        ret
        restore

It should be clear that if your colleague is right, we should be able to substitute the nop instructions for a mov ..., %i0 to see something other than the expected value.

I called my modified assembly file modified.s so as to not confuse myself ;)

Verifying my "optimizations"

First test is with my "optimizations only". I wrote a little test script:

#!/usr/bin/env bash
for i in optimized test; do
        echo -n "$i: "; ./$i
        echo -n "$i: "; ./$i a1
        echo -n "$i: "; ./$i a1 a2
        echo -n "$i: "; ./$i a1 a2 a3
done

The binaries are called optimized (my "optimizations" from above) and test (plain assembly created by GCC from C code).

Results:

$ ./runtest
optimized: Hello world: 1
optimized: Hello world: 3
optimized: Hello world: 4
optimized: Hello world: 2
test: Hello world: 1
test: Hello world: 3
test: Hello world: 4
test: Hello world: 2

So my "optimizations" seem to be just fine. Now let's tinker a little.

Tinkering with the instructions which modify the program counter

The claim is that anything past a jmp (i.e. b) will get executed before the jump itself. We have several labels with jumps, so let's replace the nop in each with something that changes the value inside %i0 and thus the return value of foo().

The changes:

.LL6:
        mov     3, %i0
        b       .LL1
        mov     30, %i0
.LL9:
        mov     -1, %i0
        b       .LL1
        mov     42, %i0
.LL5:
        mov     1, %i0
        b       .LL1
        mov     10, %i0
.LL8:
        mov     2, %i0
        b       .LL1
        mov     20, %i0
.LL11:
        mov     4, %i0
.LL1:
        ret
        restore

So except for return code -1 (which becomes 42) and 4 (which stays the same) everything should now return the original value times ten.

Let's see the results (I added modified to the list of items in my for loop):

$ ./runtest
optimized: Hello world: 1
optimized: Hello world: 3
optimized: Hello world: 4
optimized: Hello world: 2
test: Hello world: 1
test: Hello world: 3
test: Hello world: 4
test: Hello world: 2
modified: Hello world: 10
modified: Hello world: 30
modified: Hello world: 4
modified: Hello world: 20

A change that is as close to your example as I can get it

        mov     39, %i0
        jmp     %g1
        b       .LL11
        b       .LL1
.LL6:
        mov     37, %i0
        b       .LL1
        mov     30, %i0
[...]
.LL11:
        mov     4, %i0
.LL1:
        ret
        restore

Amending the test script, here's the output:

$ ./runtest
optimized: Hello world: 1
optimized: Hello world: 3
optimized: Hello world: 4
optimized: Hello world: 2
test: Hello world: 1
test: Hello world: 3
test: Hello world: 4
test: Hello world: 2
modified: Hello world: 10
modified: Hello world: 30
modified: Hello world: 4
modified: Hello world: 20
question: Hello world: 4
question: Hello world: 4
question: Hello world: 4
question: Hello world: 4

Baffling!

Result

You can play tricks on the reverse engineer's mind with this - no doubt. I learned something new and that alone was worth it.

Here's the situation

jmp     %g1
b       .LL11 ; <-- this is the branch taken
b       .LL1
mov     37, %i0 ; <-- but this gets executed first (at least in GDB)

Now I don't know whether this is true for all SPARC machines, but certainly for the one I was using for my tests (specs at the top)

Conclusion

Yes, this can certainly be used to trick the unwitting reverse engineer and perhaps the disassembler (static analysis tool). It's basically an opaque predicate. I.e. the outcome is clear at compile time, but it looks like it's dynamic.

It's difficult to see how good different disassemblers cope, given that I only have IDA Pro and objdump available here. My educated guess would be that they cope the same as with other opaque predicates, i.e. sometimes they'll get fooled, sometimes they'll be surprisingly smart. So whether or not this is a suitable obfuscation method remains unsolved.

Bonus information

As opposed to prior to the edit, IDA seems to be mildly confused by the new code, watch this graph view:

IDA slightly confused click here for full size image (previous version)

Little GDB session

0x106CC is the mov 39, %i0 instruction, found via IDA.

$ gdb -q ./question
(no debugging symbols found)
(gdb) b *0x106CC
Breakpoint 1 at 0x106cc
(gdb) run a1
Starting program: /export/home/builder/test/question a1
[New LWP 1]
[New LWP 2]
[LWP 2 exited]
[New LWP 2]
(no debugging symbols found)
(no debugging symbols found)

Breakpoint 1, 0x000106cc in foo ()
(gdb) disp/i $pc
1: x/i $pc
0x106cc <foo+44>:       mov  0x27, %i0
(gdb) si
0x000106d0 in foo ()
1: x/i $pc
0x106d0 <foo+48>:       jmp  %g1
0x106d4 <foo+52>:       b  0x1070c <foo+108>
0x106d8 <foo+56>:       b  0x10710 <foo+112>
0x106dc <foo+60>:       mov  0x25, %i0
(gdb)
0x000106d4 in foo ()
1: x/i $pc
0x106d4 <foo+52>:       b  0x1070c <foo+108>
0x106d8 <foo+56>:       b  0x10710 <foo+112>
0x106dc <foo+60>:       mov  0x25, %i0
(gdb)
0x000106dc in foo ()
1: x/i $pc
0x106dc <foo+60>:       mov  0x25, %i0
(gdb)
0x0001070c in foo ()
1: x/i $pc
0x1070c <foo+108>:      mov  4, %i0
(gdb)
0x00010710 in foo ()
1: x/i $pc
0x10710 <foo+112>:      ret
0x10714 <foo+116>:      restore
(gdb)

So according to GDB we are executing the mov 37, %i0 before the branching. This seems to suggest to me that even when you chain multiple branch instructions, the first thing to be executed is whatever comes after the last one in the chain.

  • @perror: concerning your edit (thanks!), have a look at this user script, I've been using it for a while now. – 0xC0000022L Apr 30 '13 at 12:00
  • You compare this to an opaque predicate, but it only fuzz the structure of the CFG. I don't think it breaks the analyzer at a semantic level (except if you do not know what a jmp is suppose to do in SPARC). Or maybe I did miss something ? – perror May 2 '13 at 13:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.