5

I'm trying to write a proof of concept in C that demonstrates code execution from a memory buffer in the stack on an ARM Cortex-M3. This will be useful to demonstrate that using the ARM MPU correctly can prevent such an attack. I figured a quick and dirty way to get some code into the stack is to copy it from a regular function and then use a goto to jump to it like so:

static void loopit(void)
{
    printf("loopit\n");
    while (1);
}

void attack(void)
{
    uint8_t buffer[64] __attribute__((aligned(4)));
    memcpy(buffer, loopit, sizeof(buffer));
    goto *((void *) (int) buffer);
}

I would expect that when I call the attack function it will copy code into the stack, jumps to it, print the message and go into an infinite loop. However instead I get an exception with the following values in the fault registers:

HFSR = 0x40000000
CFSR = 0x00020000
PSR  = 0x60000000

This seems to be the INVSTATE bit in the UFSR which indicates "illegal use of the EPSR", which I read is usually due to the BX instruction attempting to jump to an address with the LSB set to 0 which the processor interprets as a function with non-Thumb code in it, but Cortex-M processors only allow Thumb code. I see that the memcpy is being given an odd address for the loopit function since I assume the compiler is ORing the real memory address with 1. So the fix I would think would be to rewrite my attack function like so:

void attack(void)
{
    uint8_t buffer[64] __attribute__((aligned(4)));
    memcpy(buffer, ((int) loopit) & ~1, sizeof(buffer));
    goto *((void *) ((int) buffer) | 1);
}

However after doing that I get a different exception with fault registers:

HFSR = 0x40000000
CFSR = 0x00080000
PSR  = 0x81000000

This doesn't seem to make any sense the UFSR bit 3 set means "the processor has attempted to access a coprocessor". Looking at the PC this time it appears the jump succeeded which is great but then something went off the rails and the CPU appears to be executing strange instructions and not going into the infinite loop. I tried turning off interrupts before the goto and commenting out the printf as well but no luck. Any clue what is going wrong and how to make it work?

satur9nine
  • 13,927
  • 5
  • 80
  • 123
  • Did you try to see the assembly code generated by the C compiler, of the loopit() function? That might give a hint as why the copy or its execution fail to work as expected. – Déjà vu Nov 11 '17 at 01:23
  • To make sure, you are running bare metal - no OS or MMU automatically preventing you from running code in data space? Running in a debugger will probably answer a lot of questions, including what exactly you are jumping to. – Michael Dorgan Nov 11 '17 at 01:31
  • 1
    Why the `(int)` cast in `(void *) ((int) buffer)`, depending on the size of `int` and the size of pointers, that `int` cast might alter the address. Would `goto *((void *) buffer);` work better? – Déjà vu Nov 11 '17 at 01:43
  • It works for me after I fix compilation errors in your code. No faults, looping inside the buffer. What compiler do you use? – A.K. Nov 11 '17 at 04:28
  • Call to printf() or any other function causes a fault because the generated BL instruction uses relative addressing, jumping to a random address in RAM. – A.K. Nov 11 '17 at 04:39
  • When I compile with -O3, the compiler eliminates the memcpy() call because it figures `buffer` is not used at all! – A.K. Nov 11 '17 at 04:54

3 Answers3

1

Sorry for abusing the answer form, I have adapted your code a little and it blinks a LED right from the stack:

void (*_delay_ms)(uint32_t) = delay_ms;

static void loopit(void)
{
    while (1)
    {
        GPIOC->ODR ^= 1 << 13;
        _delay_ms(125);
    }
}

void attack(void)
{
    volatile uint8_t buffer[64] __attribute__((aligned(4)));
    memcpy(buffer, (void *)((uint32_t) loopit & ~1), sizeof(buffer));
    goto *(void *)((uint32_t) buffer | 1);
}

I wonder how soon I get complaints about UB.

A.K.
  • 839
  • 6
  • 13
  • 1
    I'd complain more if it needed to be cross platform compatible. Running self generated assembly already violates just about every rule that an OS would have, and the code is only going to run a very specific assembler, therefore, it kind of screams for a hack type answer. That is until the next compiler release and everything breaks... – Michael Dorgan Nov 13 '17 at 18:04
0

I ended up not using goto and not trying to execute any functions from the function copied into stack memory. Also be sure to compile the stack function with noinline and O0.

I used the following code to cast the stack address into a function pointer:

// Needed a big buffer and copied to the middle of it
#define FUNC_SIZE 256
#define BUF_SIZE (FUNC_SIZE * 3)

uint8_t mybuf[BUF_SIZE] __attribute__((aligned(8)));
uintptr_t stackfunc = (uintptr_t) mybuf;
stackfunc += FUNC_SIZE;

memcpy((void *) stackfunc, (void *) (((uintptr_t) &flashfunc) & ~1), FUNC_SIZE);

void (*jump_to_stack)(void) = (void (*)(void)) ((uintptr_t) stackfunc | 1);
jump_to_stack();

Not sure why I had to make the buffer so big. I copied the function to the middle of the buffer.

satur9nine
  • 13,927
  • 5
  • 80
  • 123
0
void attack(void)
{
    uint16_t buffer[64];
    goto *((void *) (((unsigned int)(buffer)) | 1));
}

you asked it to do a branch, it does not need the lsbit set for a branch, a branch exchange sure. In this case let the tool do its job. Or if there is a concern use assembly language to perform the branch so that you can specifically control the instruction used and thus the address.

00000000 <attack>:
   0:   b0a0        sub sp, #128    ; 0x80
   2:   2301        movs    r3, #1
   4:   466a        mov r2, sp
   6:   4313        orrs    r3, r2
   8:   469f        mov pc, r3
   a:   46c0        nop         ; (mov r8, r8)

Not even a branch in this case but a mov pc (functionally the same). Which is definitely not on the list of interworked instructions. See the architectural reference manual.

old_timer
  • 69,149
  • 8
  • 89
  • 168
  • But you didn't put anything inside the buffer, need some code in there to demonstrate it works. – satur9nine Sep 18 '18 at 18:45
  • I did in the answer that copied this one. You said I ended up not using goto. Although an interesting solution the goto wasnt the issue, messing with the address in the goto was an issue. Then of course you have to put code in the buffer. – old_timer Sep 19 '18 at 03:39