任何带特征的检测都是不安全的 & 隐而不发(@Ouroboros)
Move to AntiDebugBypass on github
代码依赖于 HookZz, 一个 hook 框架
前言
对于应用安全甲方一般会在这三个方面做防御.
按逻辑分类的话应该应该分为这几类, 但如果从实现原理的话, 应该分为两类, 用API实现的
和 不用API实现的
(这说的不用 API 实现, 不是指换成 inine 函数就行) . 首先使用 API 实现基本统统沦陷. 直接通过指令实现的机制还有一丝存活的可能. 逻辑的话应该分为, 反调试, 反注入, 越狱检测, hook 检测.
本文所有相关仅仅针对 aarch64.
假设读者对下知识有了解
- arm64 相关知识
- macho 文件结构以及加载相关知识
- dyld 链接 dylib 相关函数等知识
如何 hook 不定参数函数?
技巧在于伪造原栈的副本. 具体参考下文.
通常来说必备手册
|
|
通常来说必备源码
|
|
反调试
反调试从逻辑上分大概分为, 一种是直接屏蔽调试器挂载, 另一种就是根据特征手动检测调试器挂载. 当然也分为使用函数实现 和 直接使用内联 asm 实现.
ptrace 反调试
ptrace 反调试可以使用四种方法实现.
1. 直接使用 ptrace 函数
这里使用的是 dlopen
+ dysym
.
|
|
当然也可以基于 runtime 符号查找.
|
|
2. 使用 syscall 实现
|
|
3. 内联 svc + ptrace 实现
其实这种方法等同于直接使用 ptrace, 此时系统调用号是 SYS_ptrace
|
|
4. 内联 svc + syscall + ptrace 实现
其实这种方法等同于使用 syscall(SYS_ptrace, PT_DENY_ATTACH, 0, 0, 0)
, 这里需要注意, 此时的系统调用号是 0, 也就是 SYS_syscall
|
|
简单整理下系统调用流程, 只能以 xnu-3789.41.3
源码举例.
Supervisor Call causes a Supervisor Call exception. svc 切换 Exception Levels
从 EL0(Unprivileged)
到 EL1(Privileged)
上面说的是指令层相关, 再说系统层相关, 使用 svc 进行系统中断调用需要明确 3 个点: 中断号, 系统调用号, 以及参数. 下面以 x86-64 举例.
中断向量表
|
|
中断处理函数
|
|
|
|
系统调用表
|
|
反调试检测
这里主要是调试器的检测手段, 很多检测到调试器后使用 exit(-1)
退出程序. 这里很容易让 cracker 断点到 exit
函数上. 其实有一个 trick 就是利用利用系统异常造成 crash. 比如: 覆盖/重写 __TEXT
内容(debugmode 模式下可以对 rx-
内存进行操作).
或者利用内联汇编实现退出, 并清除堆栈(防止暴力 svc patch with nop
).
|
|
使用 sysctl 检测
这里在检测时也可以通过 svc 实现.
|
|
使用 isatty 检测
|
|
使用 ioctl 检测
|
|
svc 完整性检测
上述的 svc 反调试手段, 可以通过 patch svc #0x80
with nop
轻松绕过. 所以需要校验 svc #0x80
是否被 patch, 一个想当然的方法是在正常的代码中使用 svc 进行 coding, 仔细想想并不合适.
所以另一个想法就是, 使用 svc 实现一个小功能, 之后检测 x0
返回值. 这里使用的是 getpid()
.
tips: longjmp
本来是用在异常时恢复状态, 这里由于未保存状态. 所以可以让攻击者不能对退出进行断点.
这里使用, 下面一小段内联汇编可以达到相同的目的.
|
|
整体的 svc 完整检测原型如下, 仅做抛砖引玉.
|
|
绕过
对于使用函数进行反调试可以使用 hook 轻松绕过, 具体的实现, 直接看代码.
syscall 反调试绕过
因为 syscall
反调试有些特殊, 这里需要介绍下如何绕过 syscall
反调试, 使用的是 va_list
进行传递参数. http://infocenter.arm.com/help/topic/com.arm.doc.ihi0055b/IHI0055B_aapcs64.pdf
参考阅读 va_list
相关.
借助 HookZz 有两种方法可以进行绕过
1. 使用 replace_call
绕过
这里的 syscall
使用的是 va_list
传递参数. 所以这里问题在于如何 hook 不定参数函数. 因为在 hook 之后不确定原函数的参数个数. 所以没有办法调用原函数.
所以这里有一个 trick, 在 orig_syscall(number, stack[0], stack[1], stack[2], stack[3], stack[4], stack[5], stack[6], stack[7]);
时伪造了一个栈, 这个栈的内容和原栈相同(应该是大于等于原栈的参数内容). 虽然传递了很多参数, 如果理解 function call
的原理的话, 即使传递了很多参数, 但是只要栈的内容不变, 准确的说的是从低地址到高地址的栈里的内容不变(这里可能多压了很多无用的内容到栈里), 函数调用就不会变.
这里不要使用 large structure
, 编译时会使用隐含的 memcpy
最终传入的其实是地址. 大部分注释请参考下文.
|
|
2. 使用 pre_call
绕过
这种方法需要查看 syscall
的汇编实现, 来确定 PT_DENY_ATTACH
放在哪一个寄存器.
|
|
可以看到调用如果 x0
是 SYS_ptrace
, 那么 PT_DENY_ATTACH
存放在 [sp]
.
|
|
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
进行覆盖操作.
以下代码依赖于 HookZz).
大致原理就是先搜索到 svc #0x80
指令后, 对该指令地址进行 hook, 之后使用 pre_call
修改寄存器的值.
|
|
总结
上文对很多的反调试原理做了总结, 也有一些没有讲到原理. 读者可以自行研究.
附录
|
|
ios kext load
|
|