AngusKing 发表于 2017-9-1 16:13

反调试与绕过的奇淫技巧 ios

本帖最后由 AngusKing 于 2017-9-1 16:18 编辑

前言
对于应用安全甲方一般会在这三个方面做防御.

按逻辑分类的话应该应该分为这几类, 但如果从实现原理的话, 应该分为两类, 用API实现的 和 不用API实现的(这说的不用 API 实现, 不是指换成 inine 函数就行) . 首先使用 API 实现基本统统沦陷. 直接通过指令实现的机制还有一丝存活的可能. 逻辑的话应该分为, 反调试, 反注入, 越狱检测, hook 检测.

本文所有相关仅仅针对 aarch64.

假设读者对下知识有了解

arm64 相关知识
macho 文件结构以及加载相关知识
dyld 链接 dylib 相关函数等知识
如何 hook 不定参数函数?

技巧在于伪造原栈的副本. 具体参考下文.

通常来说必备手册


// 指令格式等细节
ARM Architecture Reference Manual(ARMv8, for ARMv8-A architecture profile)
https://static.docs.arm.com/ddi0487/b/DDI0487B_a_armv8_arm.pdf

ARM Cortex -A Series Programmer’s Guide for ARMv8-A
http://infocenter.arm.com/help/topic/com.arm.doc.den0024a/DEN0024A_v8_architecture_PG.pdf

Calling conventions for different C++ compilers and operating systems
http://www.agner.org/optimize/calling_conventions.pdf

Procedure Call Standard for the ARM 64-bit Architecture (AArch64)
http://infocenter.arm.com/help/topic/com.arm.doc.ihi0055b/IHI0055B_aapcs64.pdf

通常来说必备源码

// dyld
https://opensource.apple.com/tarballs/dyld/

// xnu
https://opensource.apple.com/tarballs/xnu/

// objc
https://opensource.apple.com/tarballs/objc4/
https://github.com/RetVal/objc-runtime (可编译)

// cctools
https://opensource.apple.com/tarballs/cctools (很全的头文件)
反调试
反调试从逻辑上分大概分为, 一种是直接屏蔽调试器挂载, 另一种就是根据特征手动检测调试器挂载. 当然也分为使用函数实现 和 直接使用内联 asm 实现.

ptrace 反调试

ptrace 反调试可以使用四种方法实现.

1. 直接使用 ptrace 函数

这里使用的是 dlopen + dysym.

typedef int (*PTRACE_T)(int request, pid_t pid, caddr_t addr, int data);
static void AntiDebug_001() {
    void *handle = dlopen(NULL, RTLD_GLOBAL | RTLD_NOW);
    PTRACE_T ptrace_ptr = dlsym(handle, "ptrace");
    ptrace_ptr(PT_DENY_ATTACH, 0, 0, 0);
}
当然也可以基于 runtime 符号查找.

// runtime to get symbol address, but must link with `
// -Wl,-undefined,dynamic_lookup` or you can use `dlopen` and `dlsym`
extern int ptrace(int request, pid_t pid, caddr_t addr, int data);
static void AntiDebug_002() { ptrace(PT_DENY_ATTACH, 0, 0, 0); }
2. 使用 syscall 实现

void AntiDebug_005() { syscall(SYS_ptrace, PT_DENY_ATTACH, 0, 0, 0); }
3. 内联 svc + ptrace 实现

其实这种方法等同于直接使用 ptrace, 此时系统调用号是 SYS_ptrace

static __attribute__((always_inline)) void AntiDebug_003() {
#ifdef __arm64__
    __asm__("mov X0, #31\n"
            "mov X1, #0\n"
            "mov X2, #0\n"
            "mov X3, #0\n"
            "mov w16, #26\n"
            "svc #0x80");
#endif
}
4. 内联 svc + syscall + ptrace 实现

其实这种方法等同于使用 syscall(SYS_ptrace, PT_DENY_ATTACH, 0, 0, 0), 这里需要注意, 此时的系统调用号是 0, 也就是 SYS_syscall

static __attribute__((always_inline)) void AntiDebug_004() {
#ifdef __arm64__
    __asm__("mov X0, #26\n"
            "mov X1, #31\n"
            "mov X2, #0\n"
            "mov X3, #0\n"
            "mov X4, #0\n"
            "mov w16, #0\n"
            "svc #0x80");
#endif
}
简单整理下系统调用流程, 只能以 xnu-3789.41.3 源码举例.

Supervisor Call causes a Supervisor Call exception. svc 切换 Exception Levels 从 EL0(Unprivileged) 到 EL1(Privileged)



上面说的是指令层相关, 再说系统层相关, 使用 svc 进行系统中断调用需要明确 3 个点: 中断号, 系统调用号, 以及参数. 下面以 x86-64 举例.

中断向量表

// xnu-3789.41.3/osfmk/x86_64/idt_table.h
USER_TRAP_SPC(0x80,idt64_unix_scall)
USER_TRAP_SPC(0x81,idt64_mach_scall)
USER_TRAP_SPC(0x82,idt64_mdep_scall)
中断处理函数

// xnu-3789.41.3/osfmk/x86_64/idt64.s
/*
* System call handlers.
* These are entered via a syscall interrupt. The system call number in %rax
* is saved to the error code slot in the stack frame. We then branch to the
* common state saving code.
*/
               
#ifndef UNIX_INT
#error NO UNIX INT!!!
#endif
Entry(idt64_unix_scall)
        swapgs                                /* switch to kernel gs (cpu_data) */
        pushq        %rax                        /* save system call number */
        PUSH_FUNCTION(HNDL_UNIX_SCALL)
        pushq        $(UNIX_INT)
        jmp        L_32bit_entry_check
// xnu-3789.41.3/bsd/dev/i386/systemcalls.c
__attribute__((noreturn))
void
unix_syscall64(x86_saved_state_t *state)
{
        thread_t        thread;
        void                        *vt;
        unsigned int        code;
        struct sysent        *callp;
        int                args_in_regs;
        boolean_t        args_start_at_rdi;
        int                error;
        struct proc        *p;
        struct uthread        *uthread;
        x86_saved_state64_t *regs;
        pid_t                pid;

        assert(is_saved_state64(state));
        regs = saved_state64(state);
#if        DEBUG
        if (regs->rax == 0x2000800)
                thread_exception_return();
#endif
        thread = current_thread();
        uthread = get_bsdthread_info(thread);

#if PROC_REF_DEBUG
        uthread_reset_proc_refcount(uthread);
#endif

        /* Get the approriate proc; may be different from task's for vfork() */
        if (__probable(!(uthread->uu_flag & UT_VFORK)))
                p = (struct proc *)get_bsdtask_info(current_task());
        else
                p = current_proc();

        /* Verify that we are not being called from a task without a proc */
        if (__improbable(p == NULL)) {
                regs->rax = EPERM;
                regs->isf.rflags |= EFL_CF;
                task_terminate_internal(current_task());
                thread_exception_return();
                /* NOTREACHED */
        }

        code = regs->rax & SYSCALL_NUMBER_MASK;
        DEBUG_KPRINT_SYSCALL_UNIX(
                "unix_syscall64: code=%d(%s) rip=%llx\n",
                code, syscallnames, regs->isf.rip);
        callp = (code >= nsysent) ? &sysent : &sysent;
系统调用表

xnu-3789.41.3/bsd/kern/syscall.h
#define        SYS_setuid         23
#define        SYS_getuid         24
#define        SYS_geteuid      25
#define        SYS_ptrace         26
#define        SYS_recvmsg      27
#define        SYS_sendmsg      28
反调试检测
这里主要是调试器的检测手段, 很多检测到调试器后使用 exit(-1) 退出程序. 这里很容易让 cracker 断点到 exit 函数上. 其实有一个 trick 就是利用利用系统异常造成 crash. 比如: 覆盖/重写 __TEXT 内容(debugmode 模式下可以对 rx- 内存进行操作).

或者利用内联汇编实现退出, 并清除堆栈(防止暴力 svc patch with nop).

static __attribute__((always_inline)) void asm_exit() {
#ifdef __arm64__
    __asm__("mov X0, #0\n"
            "mov w16, #1\n"
            "svc #0x80\n"

            "mov x1, #0\n"
            "mov sp, x1\n"
            "mov x29, x1\n"
            "mov x30, x1\n"
            "ret");
#endif
}
使用 sysctl 检测

这里在检测时也可以通过 svc 实现.

static int DetectDebug_sysctl() __attribute__((always_inline));
int DetectDebug_sysctl() {
    size_t size = sizeof(struct kinfo_proc);
    struct kinfo_proc info;
    int ret, name;
   
    memset(&info, 0, sizeof(struct kinfo_proc));
   
    name = CTL_KERN;
    name = KERN_PROC;
    name = KERN_PROC_PID;
    name = getpid();
   
#if 0
    if ((ret = (sysctl(name, 4, &info, &size, NULL, 0)))) {
      return ret; // sysctl() failed for some reason
    }
#else
    // or change as `AntiDebug_003` and `AntiDebug_004`
    // https://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html
    __asm__ volatile("mov x0, %\n"
                     "mov x1, #4\n"
                     "mov x2, %\n"
                     "mov x3, %\n"
                     "mov x4, #0\n"
                     "mov x5, #0\n"
                     "mov w16, #202\n"
                     "svc #0x80"
                     :
                     : "r"(name), "r"(&info),
                      "r"(&size));
#endif
   
    return (info.kp_proc.p_flag & P_TRACED) ? 1 : 0;
}

void AntiDebug_006() {
    if (DetectDebug_sysctl()) {
      asm_exit();
    }
}
使用 isatty 检测

#include <unistd.h>
void AntiDebug_isatty() {
if (isatty(1)) {
    exit(1);
} else {
}
}
使用 ioctl 检测

#include <sys/ioctl.h>
void AntiDebug_ioctl() {
if (!ioctl(1, TIOCGWINSZ)) {
    exit(1);
} else {
}
}
svc 完整性检测

上述的 svc 反调试手段, 可以通过 patch svc #0x80 with nop 轻松绕过. 所以需要校验 svc #0x80 是否被 patch, 一个想当然的方法是在正常的代码中使用 svc 进行 coding, 仔细想想并不合适.

所以另一个想法就是, 使用 svc 实现一个小功能, 之后检测 x0 返回值. 这里使用的是 getpid().

tips: longjmp 本来是用在异常时恢复状态, 这里由于未保存状态. 所以可以让攻击者不能对退出进行断点.

这里使用, 下面一小段内联汇编可以达到相同的目的.

"mov x1, #0\n"
"mov sp, x1\n"
"mov x29, x1\n"
"mov x30, x1\n"
"ret\n"
整体的 svc 完整检测原型如下, 仅做抛砖引玉.

static __attribute__((always_inline)) void check_svc_integrity() {
    int pid;
    static jmp_buf protectionJMP;
#ifdef __arm64__
    __asm__("mov x0, #0\n"
            "mov w16, #20\n"
            "svc #0x80\n"
            "cmp x0, #0\n"
            "b.ne #24\n"
            
            "mov x1, #0\n"
            "mov sp, x1\n"
            "mov x29, x1\n"
            "mov x30, x1\n"
            "ret\n"
            
            "mov %, x0\n"
            : "=r" (pid)
            :
            :
            );
   
    if(pid == 0) {
      longjmp(protectionJMP, 1);
    }
#endif
}
绕过
对于使用函数进行反调试可以使用 hook 轻松绕过, 具体的实现, 直接看代码.

syscall 反调试绕过

因为 syscall 反调试有些特殊, 这里需要介绍下如何绕过 syscall 反调试, 使用的是 va_list 进行传递参数. http://infocenter.arm.com/help/topic/com.arm.doc.ihi0055b/IHI0055B_aapcs64.pdf 参考阅读 va_list 相关.

借助 HookZz18 有两种方法可以进行绕过

1. 使用 replace_call 绕过

这里的 syscall 使用的是 va_list 传递参数. 所以这里问题在于如何 hook 不定参数函数. 因为在 hook 之后不确定原函数的参数个数. 所以没有办法调用原函数.

所以这里有一个 trick, 在 orig_syscall(number, stack, stack, stack, stack, stack, stack, stack, stack); 时伪造了一个栈, 这个栈的内容和原栈相同(应该是大于等于原栈的参数内容). 虽然传递了很多参数, 如果理解 function call 的原理的话, 即使传递了很多参数, 但是只要栈的内容不变, 准确的说的是从低地址到高地址的栈里的内容不变(这里可能多压了很多无用的内容到栈里), 函数调用就不会变.

这里不要使用 large structure, 编译时会使用隐含的 memcpy 最终传入的其实是地址. 大部分注释请参考下文





// get the origin stack args copy.(must >= origin stack args)
    memcpy(stack, args, 8 * 8);
   
    if (number == SYS_ptrace) {
      request = va_arg(args, int);
      pid = va_arg(args, pid_t);
      addr = va_arg(args, caddr_t);
      data = va_arg(args, int);
      va_end(args);
      if (request == PT_DENY_ATTACH) {
            NSLog(@" catch 'syscall(SYS_ptrace, PT_DENY_ATTACH, 0, "
                  @"0, 0)' and bypass.");
            return 0;
      }
    } else {
      va_end(args);
    }
   
    // must understand the principle of `function call`. `parameter pass` is
    // before `switch to target` so, pass the whole `stack`, it just actually
    // faked an original stack. Do not pass a large structure,will be replace with
    // a `hidden memcpy`.
    int x = orig_syscall(number, stack, stack, stack, stack, stack,
                         stack, stack, stack);
    return x;
}
2. 使用 pre_call 绕过

这种方法需要查看 syscall 的汇编实现, 来确定 PT_DENY_ATTACH 放在哪一个寄存器.

libsystem_kernel.dylib`__syscall:
    0x1815c0900 <+0>:ldp    x1, x2,
    0x1815c0904 <+4>:ldp    x3, x4,
    0x1815c0908 <+8>:ldp    x5, x6,
    0x1815c090c <+12>: ldr    x7,
    0x1815c0910 <+16>: mov    x16, #0x0
    0x1815c0914 <+20>: svc    #0x80
    0x1815c0918 <+24>: b.lo   0x1815c0930               ; <+48>
    0x1815c091c <+28>: stp    x29, x30, !
    0x1815c0920 <+32>: mov    x29, sp
    0x1815c0924 <+36>: bl   0x1815a6dc0               ; cerror
    0x1815c0928 <+40>: mov    sp, x29
    0x1815c092c <+44>: ldp    x29, x30, , #0x10
    0x1815c0930 <+48>: ret
可以看到调用如果 x0 是 SYS_ptrace, 那么 PT_DENY_ATTACH 存放在 .

void syscall_pre_call(RegState *rs, ThreadStack *threadstack, CallStack *callstack) {
    int num_syscall;
    int request;
    zpointer sp;
    num_syscall = (int)(uint64_t)(rs->general.regs.x0);
    if (num_syscall == SYS_ptrace) {
      sp = (zpointer)(rs->sp);
      request = *(int *)sp;
      if (request == PT_DENY_ATTACH) {
            *(long *)sp = 10;
            NSLog(@" catch 'syscall(SYS_ptrace, PT_DENY_ATTACH, 0, "
                  @"0, 0)' and bypass.");
      }
    }
}
__attribute__((constructor)) void patch_syscall_by_pre_call() {
    zpointer syscall_ptr = (void *)syscall;
    #if 0
    ZzBuildHook((void *)syscall_ptr, NULL, NULL, syscall_pre_call, NULL);
    ZzEnableHook((void *)syscall_ptr);
    #endif
}
// --- end ---
svc #0x80 反调试绕过

这里介绍关键是介绍如何对 svc 反调试的绕过.

上面已经对 svc 进行了简单的介绍. 所以理所当然想到的是希望通过 syscall hook, 劫持 system call table(sysent) . 这里相当于实现 syscall hook. 但是难点之一是需要找到 system call table(sysent), 这一步可以通过 joker, 对于 IOS 10.x 可以参考 http://ioshackerwiki.com/syscalls/, 难点之二是作为 kext 加载. 可以参考 附录, 对于具体的 kernel patch 没有做过深入研究, 应该可以参考 comex 的 datautils0

ok, 接下来使用另一种思路对绕过, 其实也就是 code patch + hook address. 对 __TEXT 扫描 svc #0x80 指令, 对于 cracker 来说, 在 __TEXT 段使用 svc #0x80 具有一定的反调试可能, 所以需要对 svc #0x80 进行 hook addres, 这里并不直接对 svc #0x80 进行覆盖操作.

以下代码依赖于 HookZz18).

大致原理就是先搜索到 svc #0x80 指令后, 对该指令地址进行 hook, 之后使用 pre_call 修改寄存器的值.

void hook_svc_pre_call(RegState *rs, ThreadStack *threadstack, CallStack *callstack) {
    int num_syscall;
    int request;
    num_syscall = (int)(uint64_t)(rs->general.regs.x16);
    request = (int)(uint64_t)(rs->general.regs.x0);
   
    if (num_syscall == SYS_syscall) {
      int arg1 = (int)(uint64_t)(rs->general.regs.x1);
      if (request == SYS_ptrace && arg1 == PT_DENY_ATTACH) {
            *(unsigned long *)(&rs->general.regs.x1) = 10;
            NSLog(@" catch 'SVC #0x80; syscall(ptrace)' and bypass");
      }
      
    } else if (num_syscall == SYS_ptrace) {
      request = (int)(uint64_t)(rs->general.regs.x0);
      if (request == PT_DENY_ATTACH) {
            *(unsigned long *)(&rs->general.regs.x0) = 10;
            NSLog(@" catch 'SVC-0x80; ptrace' and bypass");
      }
    } else if(num_syscall == SYS_sysctl) {
      STACK_SET(callstack, (char *)"num_syscall", num_syscall, int);
      STACK_SET(callstack, (char *)"info_ptr", rs->general.regs.x2, zpointer);
    }
}

void hook_svc_half_call(RegState *rs, ThreadStack *threadstack, CallStack *callstack) {
    // emmm... little long...
    if(STACK_CHECK_KEY(callstack, (char *)"num_syscall")) {
      int num_syscall = STACK_GET(callstack, (char *)"num_syscall", int);
      struct kinfo_proc *info = STACK_GET(callstack, (char *)"info_ptr", struct kinfo_proc *);
      if (num_syscall == SYS_sysctl)
      {
            NSLog(@" catch 'SVC-0x80; sysctl' and bypass");
            info->kp_proc.p_flag &= ~(P_TRACED);
      }
    }
}

__attribute__((constructor)) void hook_svc_x80() {
    zaddr svc_x80_addr;
    zaddr curr_addr, text_start_addr, text_end_addr;
    uint32_t svc_x80_byte = 0xd4001001;
   
    const struct mach_header *header = _dyld_get_image_header(0);
    struct segment_command_64 *seg_cmd_64 = zz_macho_get_segment_64_via_name((struct mach_header_64 *)header, (char *)"__TEXT");
    zsize slide = (zaddr)header - (zaddr)seg_cmd_64->vmaddr;
   
    struct section_64 *sect_64 = zz_macho_get_section_64_via_name((struct mach_header_64 *)header, (char *)"__text");
   
    text_start_addr = slide + (zaddr)sect_64->addr;
    text_end_addr = text_start_addr + sect_64->size;
    curr_addr = text_start_addr;
   
    while (curr_addr < text_end_addr) {
      svc_x80_addr = (zaddr)zz_vm_search_data((zpointer)curr_addr, (zpointer)text_end_addr, (zbyte *)&svc_x80_byte, 4);
      if (svc_x80_addr) {
            NSLog(@"hook svc #0x80 at %p with aslr (%p without aslr)",
                  (void *)svc_x80_addr, (void *)(svc_x80_addr - slide));
            ZzBuildHookAddress((void *)svc_x80_addr, (void *)(svc_x80_addr + 4),
                               hook_svc_pre_call, hook_svc_half_call);
            ZzEnableHook((void *)svc_x80_addr);
            curr_addr = svc_x80_addr + 4;
      } else {
            break;
      }
    }
}

justcause 发表于 2017-9-1 17:24

没太看明白,不过还是要谢谢分享

wzy41718682 发表于 2017-9-1 17:49

弄得这是一坨什么啊 连原贴地址都不贴上。。。万一人家说事咋办

草石榴vip 发表于 2017-9-1 18:00

学习下了啊

gongyong728125 发表于 2017-9-1 21:11

好文章,谢谢楼主

xiaofengzi 发表于 2017-9-1 21:47

这是啥东西 没看懂,是不是涉及到内核了

chenjingyes 发表于 2017-9-2 00:13

总结的挺好 谢谢楼主分享

cql1499 发表于 2017-9-2 14:28

谢谢楼主分享,感谢你

su0081 发表于 2017-9-2 17:02

不懂,不过还是帮顶!

xjq8888 发表于 2017-9-2 22:35

支持楼主。。。
页: [1] 2
查看完整版本: 反调试与绕过的奇淫技巧 ios