Reverse Defense

Reverse Defense

  本文将按阶段整理iOS逆向防御的知识,随着时间推移,内容将不断更新。

  攻防互为矛盾,防得了一时,防不了永远。防御的精髓就是逼疯攻的一方(XD)。

  本文内容以防重签作为抛砖引玉,慢慢深入细节看防护。

1. 防重签

  对于.app文件防重签的代码,网上有很多介绍且是同一套的代码,如下:

#define kIdentifier @"XXXXXXXXXX" // 10位的证书ID
- (void)checkCodesign {
    NSString *embeddedPath = [[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"];
    if ([[NSFileManager defaultManager] fileExistsAtPath:embeddedPath]) {
        NSString *embeddedProvisioning = [NSString stringWithContentsOfFile:embeddedPath encoding:NSASCIIStringEncoding error:nil];
        NSArray *embeddedProvisioningLines = [embeddedProvisioning componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
        for (int i = 0; i < [embeddedProvisioningLines count]; i++) {
            if ([[embeddedProvisioningLines objectAtIndex:i] rangeOfString:@"application-identifier"].location != NSNotFound) {
                NSInteger fromPosition = [[embeddedProvisioningLines objectAtIndex:i+1] rangeOfString:@"<string>"].location+8;
                NSInteger toPosition = [[embeddedProvisioningLines objectAtIndex:i+1] rangeOfString:@"</string>"].location;
                NSRange range;
                range.location = fromPosition;
                range.length = toPosition - fromPosition;
                NSString *fullIdentifier = [[embeddedProvisioningLines objectAtIndex:i+1] substringWithRange:range];
                NSArray *identifierComponents = [fullIdentifier componentsSeparatedByString:@"."];
                NSString *appIdentifier = [identifierComponents firstObject];
                if (![appIdentifier isEqual:kIdentifier]) {
                    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" message:@"codesign verification failed." delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil, nil];
                    [alert show];
                }
                break;
            }
        }
    }
}

  做防护一定不能不求甚解、生搬硬套,否则,防了跟没防的区别不大。

  就拿上面的防重签代码进行分析,结合App签名打包加壳到App Store下载的流程来思考上面这段防重签的代码,在本地编译无论debugrelease(包括archive)出来的.app文件内,都有embedded.mobileprovision文件,可被上面的代码用于验证。但是从App Store上下载下来的App解压(与砸壳无关)出来,这个文件都不在存在,代码中- (BOOL)fileExistsAtPath:(NSString *)path方法判断就不成立,也就不会进行验证了。在逆向的时候,从App Store安装App,砸壳,当需要重新打包运行时,.app文件就附加上embedded.mobileprovision文件(通过Provisioning Profile文件编译而成,开发者账号不一样,这个文件当然也不一样了),运行上设备上时当执行到上面的代码就会验证了。上面的代码写得很漂亮,还会友好提示”codesign verification failed“(XD),不过其实直接复制到AppDelegate中调用的话,效果就不怎么好了。

  • 首先,因为这一系列的代码是写在一个方法内的,所以是存在Hook了这个方法,让其防重签检验无效的可能。
  • 10位证书ID是通过宏来定义的,在预编译以后,就是直接作为参数,在编译成汇编后,它就是一个非常显眼的标识,通过IDAMach-O文件,顺着App的生命周期要找出来并不难,然后通过修改Mach-O文件中的数值,改成重签的证书的ID,防重签也就无效了。

  从这两点非常明显的切入点可以看出,上面这段代码防重签效果非常脆弱,要加强防重签效果要做的事情其实也不多也不难。

  要做到让IDA难以找到防重签代码,就要将上面的方法要不进行混淆(简单混淆其实就是利用.pch文件(Precompiled Header)通过加密算法算出字符串定义成方法名的宏,通过预编译替换方法名进行混淆),要不打散逻辑混进生命周期中(这种方法效果是非常好的,逆向时找这些逻辑会很痛苦,并且就算找到了也要想怎么去获得需要修改的对象,一旦走到这一步,除非死磕,不然通常逆向时就会改变思路,直接找证书ID来修改。但是代码维护成本变高,一不小心改了,出问题起来也会让人很懵逼,而且其中穿插的逻辑每一步的改动可能都要小心翼翼,毕竟越复杂的代码,越容易出漏洞)。下面是一个混淆方法名的例子:

2. 代码混淆

// SomeClass.h
#import <Foundation/Foundation.h>

@interface SomeClass : NSObject

- (void)someMethodWithParameter:(NSObject *)parameter;

@end
// SomeClass.m
#import "SomeClass.h"

@implementation SomeClass

- (void)someMethodWithParameter:(NSObject *)parameter { }

@end

  知道上面的这个类,希望混淆方法(注意:因为文件名不能混淆,所以注意源码文件的对应关系),可以新增一个.pch文件,并通过build setting中的prefix header添加.pch文件的路径(注意路径取当前项目的根目录作为基地址,不清楚的可以点.pch文件看File inspectorrelative to project下的路径),让这个方法名成为一个宏,在预编译后就会成了混淆后的名字了,后面就算是看restore symbol后的Mach-O文件,都只能看到混淆后的名字,可以让逆向一脸懵逼。

// Obfuscation.pch
#ifndef CodeObfuscation_pch
#define CodeObfuscation_pch

// 这里的ea6658e7b280dd39e077fe60016ce863是通过md5对someMethodWithParameter字符串加密生成的
// 当然也可以用其他办法生成加密字符串(甚至用脸滚键盘生成也没所谓)甚至使用混合方式加密
#define someMethodWithParameter		ea6658e7b280dd39e077fe60016ce863

#endif /* Obfuscation */

3. 关键字符串防护

  另外一点就是别将关键字符串直接当成参数进行传递,也别在关键地方添加容易让人定位的字符串。关键字符串,通常会使用CCCrypt函数进行对称加密(也有坑,例如用lldb符号断点断住这个函数,就能找到函数内的含加密的Key的参数了),或类似数组和位运算的方式进行保护,例子如下:

// 字符串加密
// 用脸滚出来的宏随便定义了一个16进制的数
#define DKASDLMNFWEFN  0xabcd
// 一个不显眼(混淆过)的静态函数,用于获取需要保护的字符串,在汇编中,看bl跳转的时候,难看的函数名要跟踪会很难受的
static NSString *vuhwiebasdweoqmc() {
	// 通过宏与被保护的字符串中的字符进行异或运算,防止字符串进入常量区,IDA就不能显示出被保护的字符串信息了
	unsigned char key[] = {
		(DKASDLMNFWEFN ^ 'w'),
		(DKASDLMNFWEFN ^ 'h'),
		(DKASDLMNFWEFN ^ 'a'),
		(DKASDLMNFWEFN ^ 't'),
		(DKASDLMNFWEFN ^ 'e'),
		(DKASDLMNFWEFN ^ 'v'),
		(DKASDLMNFWEFN ^ 'e'),
		(DKASDLMNFWEFN ^ 'r'),
		(DKASDLMNFWEFN ^ '\0'),
	};
	// 通过指针获得数组的地址,对其进行字符串还原(异或位运算做对称加密算法也是很方便)
	unsigned char *p = key;
	*p = (*p ^ DKASDLMNFWEFN);
	while (*p != '\0' ) {
		p++;
		*p = (*p ^ DKASDLMNFWEFN);
	}
	// 返回值就是被保护的字符串,在这里就是@"whatever"了
	return [NSString stringWithUTF8String:(const char *)key];
}

4. 防静态分析

  使用CCCrypt函数的参数加密了,还要注意防护函数本身,毕竟lldb符号断点断住了CCCrypt后,参数都是能拿到的,函数要是不防护,那前面做的防护效果也不怎么样了。所以想要做这一层的防护,就要用另外一种方式调用这个函数了(这种做法主要是为了防静态分析)。

  在不直接调用的情况下调用一个函数,这种操作就是C语言的精髓和魅力了,说白了就是指针。

  为了能让一个指针指向一个函数的入口,需要找到函数所在的库和函数的符号,符号是已知条件,而库想找也容易,先给函数打一个符号断点,当符号断点断住了之后bt,就能看到库了,例如这里的CCCrypt所在的库就是libcommonCrypto.dylib,然后通过image list查到库的加载地址(用otool查也行)。在条件都知道后,就可以进行调用了,例子如下:

// 因为要用上dlopen,所以要添加对应的头文件
#import <CommonCrypto/CommonCrypto.h>
#import "dlfcn.h"

void someFunction() {

	// 获取库
	void *handler = dlopen("PATH_TO_LIBRARY", RTLD_LAZY); 
	// 获取指向CCCrypt函数入口的指针
	CCCryptorStatus CCCryptPointer(
    CCOperation op,         /* kCCEncrypt, etc. */
    CCAlgorithm alg,        /* kCCAlgorithmAES128, etc. */
    CCOptions options,      /* kCCOptionPKCS7Padding, etc. */
    const void *key,
    size_t keyLength,
    const void *iv,         /* optional initialization vector */
    const void *dataIn,     /* optional per op and alg */
    size_t dataInLength,
    void *dataOut,          /* data RETURNED here */
    size_t dataOutAvailable,
    size_t *dataOutMoved)
    __OSX_AVAILABLE_STARTING(__MAC_10_4, __IPHONE_2_0) = dlsym(handler, "CCCrypt"); // 注意:这里的第二个参数的传值需要使用前面的字符串保护方式进行传值,例子这里就不加了,返回一个CCCrypt指针

    // 良好习惯
    if (!CCCryptPointer) {
    	return nil;
    }

    // 用函数指针调用函数并获得返回值,这里的调用就跟CCCrypt(...)一样
    CCCryptorStatus cryptorStatus = CCCryptPointer(...);
}

  通过函数指针调用函数可以去掉Mach-O文件中查相应符号的调用(防住了静态分析),可是这种方法还阻止不了lldb的符号断点(防不住动态调试),就是说用CCCrypt符号断点还是能断住,能获取到参数,结果是依旧没防住。

5. 防动态调试

  要防住动态调试,一个思路就是通过ptrace函数阻止进程依附。

/*
 * Copyright (c) 2000-2005 Apple Computer, Inc. All rights reserved.
 *
 * @APPLE_OSREFERENCE_LICENSE_HEADER_START@
 * 
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apple Public Source License
 * Version 2.0 (the 'License'). You may not use this file except in
 * compliance with the License. The rights granted to you under the License
 * may not be used to create, or enable the creation or redistribution of,
 * unlawful or unlicensed copies of an Apple operating system, or to
 * circumvent, violate, or enable the circumvention or violation of, any
 * terms of an Apple operating system software license agreement.
 * 
 * Please obtain a copy of the License at
 * http://www.opensource.apple.com/apsl/ and read it before using this file.
 * 
 * The Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 * Please see the License for the specific language governing rights and
 * limitations under the License.
 * 
 * @APPLE_OSREFERENCE_LICENSE_HEADER_END@
 */
/* Copyright (c) 1995 NeXT Computer, Inc. All Rights Reserved */
/*-
 * Copyright (c) 1984, 1993
 *	The Regents of the University of California.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. All advertising materials mentioning features or use of this software
 *    must display the following acknowledgement:
 *	This product includes software developed by the University of
 *	California, Berkeley and its contributors.
 * 4. Neither the name of the University nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 *	@(#)ptrace.h	8.2 (Berkeley) 1/4/94
 */

#ifndef	_SYS_PTRACE_H_
#define	_SYS_PTRACE_H_

#include <sys/appleapiopts.h>
#include <sys/cdefs.h>

enum {
	ePtAttachDeprecated __deprecated_enum_msg("PT_ATTACH is deprecated. See PT_ATTACHEXC") = 10
};


#define	PT_TRACE_ME	0	/* child declares it's being traced */
#define	PT_READ_I	1	/* read word in child's I space */
#define	PT_READ_D	2	/* read word in child's D space */
#define	PT_READ_U	3	/* read word in child's user structure */
#define	PT_WRITE_I	4	/* write word in child's I space */
#define	PT_WRITE_D	5	/* write word in child's D space */
#define	PT_WRITE_U	6	/* write word in child's user structure */
#define	PT_CONTINUE	7	/* continue the child */
#define	PT_KILL		8	/* kill the child process */
#define	PT_STEP		9	/* single step the child */
#define	PT_ATTACH	ePtAttachDeprecated	/* trace some running process */
#define	PT_DETACH	11	/* stop tracing a process */
#define	PT_SIGEXC	12	/* signals as exceptions for current_proc */
#define PT_THUPDATE	13	/* signal for thread# */
#define PT_ATTACHEXC	14	/* attach to running process with signal exception */

#define	PT_FORCEQUOTA	30	/* Enforce quota for root */
#define	PT_DENY_ATTACH	31

#define	PT_FIRSTMACH	32	/* for machine-specific requests */

__BEGIN_DECLS


int	ptrace(int _request, pid_t _pid, caddr_t _addr, int _data);


__END_DECLS

#endif	/* !_SYS_PTRACE_H_ */

  ptrace函数存放于user/include/sys/ptrace.h中,值得注意的是,它是一个用于MAC的头文件而不是iOS,所以为了能调用这个函数,需要手动把这个头文件复制到做防护的项目中,引入并调用。

  可以放心的是,这个头文件不是私有API,不会影响上架审核。其实注意到这个头文件中引入的2个头文件就知道它们本身就是iOS可用的系统头文件。

  利用ptrace可以防止进程依附,基于debugserverlldb直接对做了防护的App进行依附调试是会报错的,App也会闪退。进行防护时的调用如下:

/* parameter
	_request: 头文件中列出的一些宏对应的值,防止进程依附就是PT_DENY_ATTACH,即31
	_pid: 进程id,0表示自身进程
	_addr: ptrace可以访问第二个参数即指定进程内的寄存器和内存,根据第一个参数做操作,如读写内存,这个参数就是指定地址
	_data: 当ptrace对指定进程的指定地址进行操作时的数据
 */
ptrace(PT_DENY_ATTACH, 0, 0, 0);

  调用了ptrace函数就能做到最基本的反调试防护了,但是这样并不足够,因为还有反反调试这样的操作存在。。。

  反反调试,顾名思义,也就是不让ptrace生效,达到调试目的的操作。实现原理在之前的文章也说过,就是fishhook,毫无疑问ptrace是个C语言函数,在Mach-O文件中有它对应的符号,利用fishhook勾住ptrace替换成空操作,就能继续进行调试了。

  既然有了反反调试,要是还想防护要怎么做?那就反反反调试呗。。。

  反反反调试有3种方法

  • ptrace函数的调用放到调试顺序最前面,当ptrace成为最先执行的代码之一时,后面进行的fishhook已经无所谓了。关于代码的加载执行顺序,可以查看之前的文章中提及的代码加载执行顺序,这里的结论就是,若想做到最早调用自己的防护代码,就把防护代码打包成动态库Framework,放在项目依赖的最前面,同样的,这样的动态库最好有点耦合的功能代码,毕竟Mach-O文件中,是可以看到动态库的,不影响功能的话,直接删掉防护代码这种事情不要希望逆向开发时不去试。(实际上,QQ就是一个类似这样做法的App,QQ的Mach-O文件非常的小,大概只有一些涉及生命周期的的调用,它的主逻辑全都放在了其中一个Framework中)

  • sysctl函数在头文件usr/include/sys/sysctl.h内,头文件里定义了很多宏,这些宏是用来拼成控制码的,本文内容为防护,这里就以查询是否有进程依附这个信息来判断App是否正在被进程依附,例子如下:

首先来看一下这个函数:

/*
	这个函数就看声明真的很莫名其妙,让人一脸懵逼的。
	return value: int,整型类型。0,代表操作成功;1,代表操作失败;
	parameter 1: (int *),整型指针,指向存放控制码的地址,用于传递指定操作的信息
	parameter 2: u_int,无符号整型类型,用于传递第一个参数的大小
	parameter 3: (void *),指针类型,用于传递操作信息的旧值,对于获取信息时的情况来说,它就是指向查询信息的指针
	parameter 4: (size_t *),旧值的大小
	parameter 5: (void *), 指针类型,与parameter 3相似,但传递的是新值
	parameter 6: (size_t *), 新值的大小
 */
int	sysctl(int *, u_int, void *, size_t *, void *, size_t);
// 像这么优秀的函数还有两个,如下所示,他们是对sysctl的简化,直接用宏定义好的字符串映射控制码,方便调用
int	sysctlbyname(const char *, void *, size_t *, void *, size_t);
int	sysctlnametomib(const char *, int *, size_t *);

再来看下如何查询进程被依附:

// 以数组的方式填写控制码
int name[4];
name[0] = CTL_KERN;			// 内核查看
name[1] = KERN_PROC;		// 查询进程
name[2] = KERN_PROC_PID;	// 传递的参数是进程的id,即PID
name[3] = getpid();			// PID,通过调用getpid()来获得当前进程的id
struct kinfo_proc info;				// 接受进程查询结果信息的结构体,这个结构体的定义也在同一个头文件中,查看也方便
size_t info_size = sizeof(info);	// 结构体的大小
// 调用sysctl时,应注意的就是传参,第3个参数是获得信息的变量,传指针,函数内部赋值后,就能通过这个指针获得想要的信息,c语言很多函数都是这么传递信息的而不是通过返回值。
int error = sysctl(name, sizeof(name)/sizeof(*name), &info, &info_size, 0, 0);
assert(error == 0);			// 0,代表操作成功;1,代表操作失败;
// 最后可以通过位与操作判断当前进程是否依附了进程
// 查看kinfo_proc结构体中的extern_proc结构体(位与usr/include/sys/proc.h中)的p_flag值,与P_TRACED宏位与运算可知道进程有没有被依附
// P_TRACED宏的介绍是:#define	P_TRACED	0x00000800	/* Debugged process being traced */
// 位与运算时结果为0,表示没有被依附;结果非0,就是被进程依附了
(info.kp_proc.p_flag & P_TRACED) == 0

备注:sysctl函数与ptrace函数一样,是可以被fishhook的,所以会同样有ptrace函数的的尴尬情况,但不用担心,下面一种方法就是一个大杀器,利用内联汇编实现上面做的防护,使fishhook失效,但防动态调试却依然有效!

  • 要做动态调试的防护,基于C语言函数可以通过fishhook勾住这种情况,除了将防护代码做成Framework并放到开始加载的首批执行代码中外,还可以使用内联汇编的方式调用防护函数。关于系统的一些函数,可以在usr/include/sys/syscall.h内找到,内有定义了几百个的系统函数的宏,对应的是这些函数的序号。在知道了这些函数后,结合前面关于汇编语言提及到的知识,就可以直接用内联汇编来执行这些函数了(注意:内里的宏可以看到是被设置了为私有API的,所以不能使用里面定义的宏,而是直接使用宏对应的序号,至于用宏会不会出审核问题,本人没试过,有不怕碰壁的同学可以试试然后告诉一下呗,XD)。例子如下:
+ (void)load {
	// 这里的内联汇编执行的就是ptrace函数,对应26,它有4个参数,第一个参数是宏PT_DENY_ATTACH,即31,后3个参数上面提及过,都是0。调用效果同ptrace(PT_DENY_ATTACH, 0, 0, 0)
	asm(
	    "mov X0, #31\n"
	    "mov X1, #0\n"
	    "mov X2, #0\n"
	    "mov X3, #0\n"
	    "mov w16, #26\n"
	    "svc #0x80"
	);
	// 以下调用的内联汇编调用的是exit函数,对应1,只有一个参数,它的调用效果同exit(0)
	asm(
        "mov X0, #0\n"
        "mov w16, #1\n"
        "svc #0x80"
    );
}

总结:

  现在从新回到开始处提及的防重签代码,会发现这一段代码可以深入进行防护的地方不少,并且在检测到是重签时,可以直接用内联汇编调用exit函数闪退,而不是乖巧地提示“codesign verification failed.”(XD)。

  App的防护思路在本文中大概都提及了,全是细节,目的就是逼疯逆向开发人,消磨他们的意志,但世上不存在破不开的盾。毕竟连最新的闭源iOS也会被JB(即便是非完美的), 一个App真要死磕进行逆向,一行一行进行反汇编,终究是能攻破的,所以在防护这方面还存在另一层的做法,那就获知正在逆向的谁,然后进行限制,甚至是封号(微信就是这样子,一个不小心检测到逆向行为,先进行警告,再进行限制,最后进行封号)。

  这里提及到的防重签思路方法基本是通用的。重点是:灵活多变难寻找,生搬硬套死翘翘。


lZackx © 2022. All rights reserved.

Powered by Hydejack v9.1.6