iOS ARM64系统调用

3
我正在学习如何在iOS设备上使用arm64架构的shellcode并进行系统调用。我正在测试的设备是iPhone 6S。
我从这个链接(https://github.com/radare/radare2/blob/master/libr/include/sflib/darwin-arm-64/ios-syscalls.txt)中获取了系统调用列表。
我从这里了解到x8用于在arm64架构中放置系统调用号码(http://arm.ninja/2016/03/07/decoding-syscalls-in-arm64/)。
我觉得用于传递arm64参数的各种寄存器应该与arm相同,因此我参考了这个链接(https://w3challs.com/syscalls/?arch=arm_strong),该链接来源于https://azeria-labs.com/writing-arm-shellcode/
我在Xcode中编写了内联汇编代码,以下是一些片段。
//exit syscall
__asm__ volatile("mov x8, #1");
__asm__ volatile("mov x0, #0");
__asm__ volatile("svc 0x80");

然而,当我跨过这些代码时,应用程序并没有终止。
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"
                 );

如果此系统调用有效,则应在Xcode的控制台输出屏幕中打印出一些文本。然而,没有任何内容被打印出来。
有许多关于ARM汇编的在线文章,有些使用svc 0x80,有些使用svc 0等,因此可能会有一些变化。我尝试了各种方法,但我无法使这两个代码片段起作用。
有人能提供一些指导吗?
编辑: 当我编写一个C函数syscall int return_value=syscall(1,0);时,Xcode在其汇编视图中显示的内容如下:
    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

我不确定为什么这段代码被省略了。

为什么要打上shellcode标签?你是计划使用编译器+内联汇编来生成攻击载荷,而不是手写汇编代码吗? - Peter Cordes
如果您使用优化编译第二个代码片段,则write_buffer[]中的存储可能会被优化掉,因为您没有使用“memory”占位符或虚拟内存源输入。寄存器中的指针并不意味着所指向的内存也是asm语句的输入或输出。此外,您可以使用register char *buf asm("x1")来让编译器选择x1作为“r”约束条件。这样可以将asm语句减少到仅包含系统调用指令而没有mov指令。(但是您必须记得指定一个x0输出。) - Peter Cordes
我认为传递arm64参数所使用的各种寄存器应该与arm相同 - 这听起来像是一个非常危险的假设。你尝试过单步执行libc的write()系统调用包装函数来查看它的行为吗? - Peter Cordes
嗨@PeterCordes我知道它们不是同一个,但我没有找到一个特定于iOS ARM64的链接,但这个链接https://gist.github.com/yamnikov-oleg/454f48c3c45b735631f2似乎表明x0-x5用于Arm64系统调用。我不太理解您对第二个代码片段的建议。所以,如果我只想使用write将某些内容打印到控制台屏幕上,您有一个简单的示例吗?即使我的退出系统调用代码片段也无法正常工作。这些代码片段在Xcode中不会引起任何错误,它们似乎只是不能按照我的期望执行。 - localacct
我对iOS特别的东西一无所知,否则我就会直接发布一个答案了。我建议使用调试器单步执行libc write()函数,以查看现有库代码如何进行系统调用。或者,如果您能找到write包装函数,只需反汇编C库即可。我假设iOS与普通Unix类似,具有包含系统调用包装函数的libc.so或类似的东西,您可以从C中调用它们。 - Peter Cordes
嗨@PeterCordes感谢你的建议。我会继续努力。 - localacct
2个回答

16

用于系统调用的寄存器是任意的,而您选择的资源对于XNU来说肯定是错误的。

据我所知,arm64的XNU系统调用ABI是私有的,并且可能随时更改,因此没有公布的标准遵循它,但是您可以通过查看XNU源代码(在线查看下载tarball)来了解其工作原理,特别是handle_svc函数
我不会详细介绍您在哪里找到哪些位,但最终结果是:

  • svc 接收的立即数会被忽略,但标准库使用 svc 0x80(请参见这里这里)。
  • x16 保存系统调用号。
  • x0x8 最多保存9个参数*。
  • 栈上没有参数。
  • x0x1 最多保存2个返回值(例如在 fork 的情况下)。
  • 进位位用于报告错误,在这种情况下,x0 保存错误代码。

* 这仅在具有8个参数的间接系统调用(x16 = 0)的情况下使用。
* XNU源代码中的注释也提到了x9,但似乎编写该注释的工程师应该加强对偏移量错误的理解。

然后就是实际可用的系统调用号:

  • "UNIX syscalls" 的规范来源是 XNU 源代码树中的文件 bsd/kern/syscalls.master。这些系统调用使用从 0 开始到最新 iOS 13 beta 中的约 540 的系统调用号。
  • "Mach syscalls" 的规范来源是 XNU 源代码树中的文件 osfmk/kern/syscall_sw.c。这些系统调用使用负数号码在 -10-100 之间调用(例如,-28 将是 task_self_trap)。
  • 与上一点无关,两个系统调用 mach_absolute_timemach_continuous_time 可以通过系统调用号码分别为 -3-4 进行调用。
  • 几个低级操作可以通过系统调用号码 0x80000000platform_syscall 完成。

嗨@Siguza,我尝试了你的方法,它起作用了,但我注意到了一些问题。有些系统调用可以按预期执行(如getpid、getppid等),但其他的则不行(如打开/写入文件、创建文件夹等)。我想这可能与Unix文件权限有关,对吧?当我运行getlogin系统调用时,它返回了“mobile”,所以我猜mobile只有受限的权限。感谢你的帮助。 - localacct
1
Unix文件权限起着一定的作用(是的,移动设备是“非特权”用户),但如果您在非越狱设备上从普通应用程序运行此文件时,就不仅仅是这些了。具体而言,AppleMobileFileIntegrity和Sandbox kexts会通过XNU的MACF框架钩入数百个函数,可以基于多种标准禁止某些操作。除其他事项外,这还用于将每个应用限制在其自己的容器中。在进行那些打开/创建目录调用之前尝试运行chdir([NSHomeDirectory() stringByAppendingPathComponent:@"Documents"].UTF8String),然后它们应该会成功。 - Siguza
1
只是补充一下,iOS和新的Apple Silicon版本的OSX都不使用SYSCALL_CLASS_MASK,它将BSD系统调用号设置为位24以区分其他Mach系统调用。只有x86_64版本的OSX才会这样做。 - Olsonist

6

下面的内容应该能帮助你了。正如 @Siguza 所提到的,你必须使用 x16 而不是 x8 用于系统调用号。

#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]));
    }
}

编辑

针对@PeterCordes的优秀评论,是的,有一个系统调用号定义头文件<sys/syscall.h>,我在Variant 1中包含了它。但重要的是要提到,在其中被苹果公司这样定义:

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

我还没有听说过因直接通过 svc 0x80 使用系统调用而导致 iOS 应用程序被 AppStore 拒绝的案例,尽管它明显不是公共 API。
至于 @PeterCordes 建议的 "=@ccc",即将进位标志(在发生错误时由系统调用设置)作为输出约束,目前最新的 XCode11 beta / LLVM 8.0.0 甚至对 x86 也不支持,对 ARM 更不可能。

1
你需要使用GNU C 扩展 asm来声明你修改的寄存器。此外,你不能假设寄存器值在两个asm语句之间保持不变。对于每个系统调用,请使用asm volatile("svc #0x80" : "=r"( retval) : "r" (callnum), ... : "memory" );,并使用register long x0 asm("x0")本地寄存器变量来告诉编译器输入和输出的位置。 - Peter Cordes
1
第一个系统调用已经接近正确。但您仍然缺少一个“内存”破坏来告诉编译器您读取了输入寄存器指向的内存。您可以使用x0..2x16的输入和/或输出约束,而不是将mov指令放在模板中,以使其更有效率。 mov x1,%0特别无用,因为编译器已经需要在某个其他寄存器中生成指针。这就是register char *x1 asm(“x1”);的用途。 - Peter Cordes
1
通常情况下,您希望使用寄存器变量为 x0 获得 "=r" 输出,以便在需要时可以检查返回值,而不只是在 x0 上声明 clobber。参见ARM inline asm: exit system call with value read from memory ,了解使用寄存器-asm局部变量的32位ARM示例,以使 "r""=r" 约束选择特定寄存器。 - Peter Cordes
请注意,通过使用输入操作数,您可以轻松地执行 x2 = strlen(testStringOnStack); 而不是硬编码31。(在常量传播之后仍然是编译时常量,因为它是一个字符串字面值)。此外,testStringOnStack 存储在堆栈上的 指针,但它仍然只是指向一个字符串字面值(通常在 .rodata 部分的只读页面中)。如果您想要将字符串数据本身存储在堆栈上,则需要 char str[] = "foo";。然后,您可以使用 sizeof(str) - 1 作为要写入的字符数。 - Peter Cordes
1
@PeterCordes,您关于系统调用定义的头文件的看法是正确的,已经更新了答案。 - Kamil.S
显示剩余10条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接