3

I am learning more about shellcode and making syscalls in arm64 on iOS devices. The device I am testing on is iPhone 6S.

I got the list of syscalls from this link (https://github.com/radare/radare2/blob/master/libr/include/sflib/darwin-arm-64/ios-syscalls.txt).

I learnt that x8 is used for putting the syscall number for arm64 from here (http://arm.ninja/2016/03/07/decoding-syscalls-in-arm64/).

I figured the various registers used to pass in parameters for arm64 should be the same as arm so I referred to this link (https://w3challs.com/syscalls/?arch=arm_strong), taken from https://azeria-labs.com/writing-arm-shellcode/.

I wrote inline assembly in Xcode and here are some snippets

//exit syscall
__asm__ volatile("mov x8, #1");
__asm__ volatile("mov x0, #0");
__asm__ volatile("svc 0x80");

However, the application does not terminate when I stepped over these codes.

char write_buffer[]="console_text";
int write_buffer_size = sizeof(write_buffer);

__asm__ volatile("mov x8,#4;"     //arm64 uses x8 for syscall number
                 "mov x0,#1;"     //1 for stdout file descriptor
                 "mov x1,%0;"    //the buffer to display
                 "mov x2,%1;"    //buffer size
                 "svc 0x80;"
                 :
                 :"r"(write_buffer),"r"(write_buffer_size)
                 :"x0","x1","x2","x8"
                 );

If this syscall works, it should print out some text in Xcode's console output screen. However, nothing gets printed.

There are many online articles for ARM assembly, some use svc 0x80 and some use svc 0 etc and so there can be a few variations. I tried various methods but I could not get the two code snippets to work.

Can someone provide some guidance?

EDIT: This is what Xcode shows in its Assembly view when I wrote a C function syscall int return_value=syscall(1,0);

    mov x1, sp
    mov x30, #0
    str x30, [x1]
    orr w8, wzr, #0x1
    stur    x0, [x29, #-32]         ; 8-byte Folded Spill
    mov x0, x8
    bl  _syscall

I am not sure why this code was emitted.

localacct
  • 611
  • 5
  • 13
  • Why is this tagged shellcode? Are you planning to use a compiler + inline asm to generate an exploit payload, instead of just writing it by hand in asm? – Peter Cordes Jul 11 '19 at 10:02
  • If you compiled your 2nd snippet with optimization, the stores into `write_buffer[]` probably get optimized away as dead because you don't use a `"memory"` clobber or a dummy memory-source input. A pointer in a register does not imply that the pointed-to memory is also an input or output to the asm statement. Also, you can use `register char *buf asm("x1")` to make the compiler pick `x1` for an `"r"` constraint. This lets you reduce the `asm` statement to just the system call instruction with no `mov` instructions. (But then you have to remember to specify an `x0` output.) – Peter Cordes Jul 11 '19 at 10:06
  • *I figured the various registers used to pass in parameters for arm64 should be the same as arm* - That sounds like a very dangerous assumption. Have you tried single-stepping into a libc `write()` system call wrapper function to see what it does? – Peter Cordes Jul 11 '19 at 10:07
  • Hi @PeterCordes I know they are not the same but I did not find one link specific for iOS ARM64 but this link https://gist.github.com/yamnikov-oleg/454f48c3c45b735631f2 seems to indicate that x0-x5 are used for Arm64 syscalls. I do not quite understand your suggestion for the second snippet. So, if I just want to use write to print something to console screen, would you have a simple example on how I can do it? Even my exit syscall snippet does not work. The code snippets do not cause any error in Xcode, they just do not seem to perform as I expected. – localacct Jul 11 '19 at 10:59
  • I don't know anything about iOS specifically or I would have just posted an answer. I was suggesting single-stepping with a debugger into a libc `write()` function to see how the existing library code makes the system call. Or just disassemble the C library if you can find the `write` wrapper function. I'm assuming that iOS is similar to normal Unix in having a `libc.so` or something containing system-call wrapper functions you can call from C. – Peter Cordes Jul 11 '19 at 11:29
  • Hi @PeterCordes Thanks for the suggestions. I will keep trying. – localacct Jul 11 '19 at 11:49

2 Answers2

16

The registers used for syscalls are arbitrary, and the resources you've picked are certainly wrong for XNU.

As far as I'm aware, the XNU syscall ABI for arm64 is private and subject to change without notice so there's no published standard that it follows, but you can see how it works by looking at the XNU source (view it online or download a tarball), specifically the handle_svc function.
I'm not gonna go into detail on where exactly you find which bits, but the end result is:

  • The immediate passed to svc is ignored, but the standard library uses svc 0x80 (see here and here).
  • x16 holds the syscall number
  • x0 through x8 hold up to 9 arguments*
  • There are no arguments on the stack
  • x0 and x1 hold up to 2 return values (e.g. in the case of fork)
  • The carry bit is used to report an error, in which case x0 holds the error code

* This is used only in the case of an indirect syscall (x16 = 0) with 8 arguments.
* Comments in the XNU source also mention x9, but it seems the engineer who wrote that should brush up on off-by-one errors.

And then it comes to the actual syscall numbers available:

  • The canonical source for "UNIX syscalls" is the file bsd/kern/syscalls.master in the XNU source tree. Those take syscall numbers from 0 up to about 540 in the latest iOS 13 beta.
  • The canonical source for "Mach syscalls" is the file osfmk/kern/syscall_sw.c in the XNU source tree. Those syscalls are invoked with negative numbers between -10 and -100 (e.g. -28 would be task_self_trap).
  • Unrelated to the last point, two syscalls mach_absolute_time and mach_continuous_time can be invoked with syscall numbers -3 and -4 respectively.
  • A few low-level operations are available through platform_syscall with the syscall number 0x80000000.
Boris Verkhovskiy
  • 14,854
  • 11
  • 100
  • 103
Siguza
  • 21,155
  • 6
  • 52
  • 89
  • Hi @Siguza I tried your method and it worked but I noticed something. Some of the syscalls can execute as expected (getpid, getppid etc) but others do not (open/write a file, folder creation etc). I suppose this has to do with Unix file permissions right? When I ran the getlogin syscall, it returned "mobile" so I am guessing that mobile has only restricted rights. Thanks for the help – localacct Jul 12 '19 at 04:01
  • 1
    Unix file permissions play a role (and yes, mobile is the "unprivileged" user), but if you're running this from a normal app on a non-jailbroken device, there is much more at play than just that. Specifically the AppleMobileFileIntegrity and Sandbox kexts hook into hundreds of functions via XNU's MACF framework and can prohibit actions based on a multitude of criteria. Among other things, this is used to confine every app to its own container. Try running `chdir([NSHomeDirectory() stringByAppendingPathComponent:@"Documents"].UTF8String)` before those open/mkdir calls, then they should succeed. – Siguza Jul 12 '19 at 13:57
  • 1
    Just to add, neither iOS nor the new Apple Silicon version of OSX use SYSCALL_CLASS_MASK which takes the BSD syscall number and sets bit 24 to distinguish it from other Mach system calls. Only the x86_64 version of OSX does this. – Olsonist Mar 27 '22 at 18:23
5

This should get you going. As @Siguza mentioned you must use x16 , not x8 for the syscall number.

#import <sys/syscall.h>
char testStringGlobal[] = "helloWorld from global variable\n";
int main(int argc, char * argv[]) {
    char testStringOnStack[] = "helloWorld from stack variable\n";
#if TARGET_CPU_ARM64

    //VARIANT 1 suggested by @PeterCordes
    //an an input it's a file descriptor set to STD_OUT 1 so the syscall write output appears in Xcode debug output
    //as an output this will be used for returning syscall return value;
    register long x0 asm("x0") = 1;
    //as an input string to write
    //as an output this will be used for returning syscall return value higher half (in this particular case 0)
    register char *x1 asm("x1") = testStringOnStack;
    //string length
    register long x2 asm("x2") = strlen(testStringOnStack);
    //syscall write is 4
    register long x16 asm("x16") = SYS_write; //syscall write definition - see my footnote below

    //full variant using stack local variables for register x0,x1,x2,x16 input
    //syscall result collected in x0 & x1 using "semi" intrinsic assembler
    asm volatile(//all args prepared, make the syscall
                 "svc #0x80"
                 :"=r"(x0),"=r"(x1) //mark x0 & x1 as syscall outputs
                 :"r"(x0), "r"(x1), "r"(x2), "r"(x16): //mark the inputs
                 //inform the compiler we read the memory
                 "memory",
                 //inform the compiler we clobber carry flag (during the syscall itself)
                 "cc");

    //VARIANT 2
    //syscall write for globals variable using "semi" intrinsic assembler
    //args hardcoded
    //output of syscall is ignored
    asm volatile(//prepare x1 with the help of x8 register
                 "mov x1, %0 \t\n"
                 //set file descriptor to STD_OUT 1 so it appears in Xcode debug output
                 "mov x0, #1 \t\n"
                 //hardcoded length
                 "mov x2, #32 \t\n"
                 //syscall write is 4
                 "mov x16, #0x4 \t\n"
                 //all args prepared, make the syscall
                 "svc #0x80"
                 ::"r"(testStringGlobal):
                 //clobbered registers list
                 "x1","x0","x2","x16",
                 //inform the compiler we read the memory
                 "memory",
                 //inform the compiler we clobber carry flag (during the syscall itself)
                 "cc");

    //VARIANT 3 - only applicable to global variables using "page" address
    //which is  PC-relative addressing to load addresses at a fixed offset from the current location (PIC code).
    //syscall write for global variable using "semi" intrinsic assembler
    asm volatile(//set x1 on proper PAGE
                 "adrp x1,_testStringGlobal@PAGE \t\n" //notice the underscore preceding variable name by convention
                 //add the offset of the testStringGlobal variable
                 "add x1,x1,_testStringGlobal@PAGEOFF \t\n"
                 //set file descriptor to STD_OUT 1 so it appears in Xcode debug output
                 "mov x0, #1 \t\n"
                 //hardcoded length
                 "mov x2, #32 \t\n"
                 //syscall write is 4
                 "mov x16, #0x4 \t\n"
                 //all args prepared, make the syscall
                 "svc #0x80"
                 :::
                 //clobbered registers list
                 "x1","x0","x2","x16",
                 //inform the compiler we read the memory
                 "memory",
                 //inform the compiler we clobber carry flag (during the syscall itself)
                 "cc");
#endif
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

EDIT

To @PeterCordes excellent comment, yes there is a syscall numbers definition header <sys/syscall.h> which I included in the above snippet^ in Variant 1. But it's important to mention inside it's defined by Apple like this:

#ifdef __APPLE_API_PRIVATE
#define SYS_syscall        0
#define SYS_exit           1
#define SYS_fork           2
#define SYS_read           3
#define SYS_write          4

I haven't heard of a case yet of an iOS app AppStore rejection due to using a system call directly through svc 0x80 nonetheless it's definitely not public API.

As for the suggested "=@ccc" by @PeterCordes i.e. carry flag (set by syscall upon error) as an output constraint that's not supported as of latest XCode11 beta / LLVM 8.0.0 even for x86 and definitely not for ARM.

Kamil.S
  • 5,205
  • 2
  • 22
  • 51
  • 1
    You need GNU C *Extended* asm to declare clobbers on registers you modify. Plus, you can't assume that register values survive between two asm statements. Use `asm volatile("svc #0x80" : "=r"( retval) : "r" (callnum), ... : "memory" );` for each system call, with appropriate input constraints using `register long x0 asm("x0")` local register vars to tell the compiler where the inputs and outputs are. – Peter Cordes Jul 18 '19 at 13:43
  • @PeterCordes thanks you for feedback as always. I attempted to correct the 1st one for now. If the values for registers inputs are hardcoded do I still need the `"r"` part? Same for `retval`, since I only care about the side effect of the `syscall` do I still need to mark the outputs if I don't need them? – Kamil.S Jul 18 '19 at 15:41
  • 1
    That's close to correct for the first system call. You're still missing a `"memory"` clobber to tell the compiler you read the memory pointed-to by the input register. You could make it more efficient by using input and/or output constraints for `x0 .. 2` and `x16` instead of putting `mov` instructions inside the template. `mov x1, %0` is particularly useless because the compiler already needs to generate the pointer in some other register. This is what `register char * x1 asm("x1");` is for. – Peter Cordes Jul 18 '19 at 15:44
  • 1
    You generally want an `"=r"` output with a register variable for `x0` so you can check the return value if you want to, instead of just declaring a clobber on `x0`. See [ARM inline asm: exit system call with value read from memory](//stackoverflow.com/a/37363860) for a 32-bit ARM example of using register-asm locals to make `"r"` and `"=r"` constraints pick specific registers. – Peter Cordes Jul 18 '19 at 15:46
  • Notice that by using input operands, you can easily do `x2 = strlen(testStringOnStack);` instead of hardcoding 31. (It's still a compile-time constant after constant-propagation, because it's a string literal). Also, `testStringOnStack` stores the *pointer* on the stack, but it's still just pointing to a string literal (normally in a read-only page in section .rodata). If you want the string data itself on the stack, you need `char str[] = "foo";`. Then you can use `sizeof(str) - 1` as the number of characters to write. – Peter Cordes Jul 18 '19 at 18:40
  • the other answer on this question says `x1` can be a return value as well; if system calls always clobber it even when they have narrow return values, make sure to declare an output operand for it. (Or use a `"+r"` operand to use the same var for input and output). Also note that error / no-error status is in the Carry flag. I'm not sure if [GCC6 flag-output operands](//stackoverflow.com/q/30314907) work on AArch64, or only on x86. Otherwise if you actually want to check for error you'd need a flag->integer instruction or a branch to C labels with asm-goto. – Peter Cordes Jul 18 '19 at 18:45
  • Oh also you probably don't have to hard-code the system call numbers. On Linux you can `#include ` and then `x4 = __NR_write`. Presumably iOS/XNU has a similar header with `SYS_write` or some kind of named constants for call numbers. – Peter Cordes Jul 18 '19 at 18:55
  • Oh also, you need to declare a `"cc"` clobber because a system call does step on the carry flag. Forgot about that at first because GNU C for x86 implicitly has a `"cc"` clobber in every asm statement. But not for ARM. – Peter Cordes Jul 18 '19 at 18:57
  • If you made `testStringGlobal` an array instead of a pointer, you would only need `adrp/add` to put the string address in a register. The extra level of indirection from storing a pointer in memory is pointless. Also then you wouldn't need a `"memory"` clobber on that one because you can assume that static constant storage is already in sync at program startup. (Not that it really matters; basically no reason to ever use anything other than the first variant). – Peter Cordes Jul 18 '19 at 19:07
  • 1
    @PeterCordes you were right on the header with syscall definitions, updated the answer – Kamil.S Jul 19 '19 at 08:01
  • @Kamil.S Thanks for providing a sample code. I noticed that when using "r" and "=r" for the output and input operands in inline assembly, Xcode would state that I am not using the registers with the correct size or something like that. I suspect this is due to the fact that I am writing for 64 bit platform but using 32 bit registers to hold the values? – localacct Jul 19 '19 at 11:35
  • @localacct make sure you wrap everything in `#if TARGET_CPU_ARM64 #endif` if targetting ARM64. Also your backing variables for the register must match their size. Perhaps start with a fresh obj-c iOS xCode project and copy my example to `main.c` and treat is as your sandbox. Last but not least, make sure you have a 64bit device connected and selected as target so that you compile only for ARM64. – Kamil.S Jul 19 '19 at 11:40
  • @localacct: if you're making a "generic" syscall wrapper macro, probably cast everything to `uint64_t` when using the `register uint64_t x0 asm("x0");` method. The kernel should ignore high bits for `int` syscall args so it's fine to have them sign or zero-extended into 64-bit regs. If you're using `mov` instructions *inside* an asm statement, obviously it has to be `mov w0, w8` or `mov x0, x8` so you need the template to use the right width, or use `%k0` or whatever to make the compiler print the register name with your choice of width. Or better, don't do that and use register asm locals. – Peter Cordes Jul 19 '19 at 13:19
  • @Kamil.S: interesting. The whole "not public" thing presumably means "subject to change without notice", not that the call numbers or ABI on any given version of the kernel it at all hard to find. Or possibly that some details of semantics could change in ways that a libc wrapper function could compensate for. (e.g. by passing `0` for a newly-introduced `flags` arg to a system call that had another arg added). IDK if that's the kind of thing iOS is likely to do, but by declaring the ABI "private" they keep that option open. – Peter Cordes Jul 19 '19 at 13:23
  • But it's not like MS Windows system calls where the numbers are truly undocumented, and only found by reverse-engineering the DLLs. And also unlike Linux where the syscall ABI is public and stable (don't break userspace is the rule for kernel development) and any new behaviour (like a new `flags` arg) would be added as a new system call, with the original callnumber becoming a wrapper inside the kernel. e.g. [Linux `mlock2` is `mlock`](http://man7.org/linux/man-pages/man2/mlock.2.html) with an extra flag and both have separate call numbers. – Peter Cordes Jul 19 '19 at 13:26