Skip to content

krystal1110/iOS-Security

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 

Repository files navigation

Hook 攻防

HooK原理 : 钩子,改变程序的执行流程的一种技术


**MethodSwizzle **

  • 利用OC的运行时(Runtime)特性修改 SELIMP(函数指针) 的关系,打到Hook OC方法的目的
  • method_exchangeIMP交换两个 IMP
  • class_replaceMethod替换某个 SELIMP (如果没有该方法就添加,相当于换掉这个方法)
  • method_getImplementationmethod_setImplementation 获取和设置某个方法的IMP (很多第三方框架都使用)

fishhook

  • Facebook提供的工具,利用MachO文件的加载原理,动态修改懒加载和非懒加载两个符号表!

  • 可以HOOK系统的函数,但是无法HOOK自定义的函数

** 原理:**

*  共享缓存
   * iOS系统有一块特殊的位置,存放公用动态库。动态库共享缓存(dyld shared Cache)
*  PIC技术
   * 由于外部的函数调用,在我们编译时是没有办法确定地址的。
   * 苹果就采用PIC技术(位置无关代码),在MachO文件 ` DATA段 ` ,建立两张表,懒加载和非懒加载表,里面存放执行外部函数指针
   * 首次调用懒加载函数,回去找桩执行代码,首次执行会调用 ` dyld_bingder 函数`
   
*  通过字符找到懒加载表
   * fishhook利用 
      - ` stirng Table 字符表` -> 
      - ` Symbols 符号表 ` -> 
      - ` indirect Symbols 间接符号表 ` -> 
      - ` Lazy Symbol 懒加载符号表中的地址值  ` 
   * 通过重绑定修改指针的值达到HOOK的目的

Cydia Substrate

  • 一个强大的框架
  • 越狱后基本都会有

MonKey Hook

Monkey中使用了 libsubstrate.dylib

  • method_setImplementation
  • method_getImplementation

MonKey已经替换的系统函数

  • dlsym
  • sysctl
  • ptrace

Dobby (修改静态函数 C 和 swift)

  • 实际上是替换 Text段
  • 动态修改 (加载到内存的时候修改)

防护 :lldb - ptrace

  • ptrace 是 命令行工程以及 Mac OS 工程里的 <sys/ptrace.h>提供的一个函数 , 可以用来来控制进程附加管理 , 它可以实现禁止应用程序进程被附加的效果 . 在 iOS 中并没有暴露出来 , 但是 iOS 是可以使用的 .
/**
 arg1: ptrace要做的事情: PT_DENY_ATTACH 表示要控制的是当前进程不允许被附加
 arg2: 要操作进程的PID , 0就代表自己
 arg3: 地址 取决于第一个参数要做的处理不同传递不同
 arg4: 数据 取决于第一个参数要做的处理不同传递不同
 */
ptrace(PT_DENY_ATTACH, 0, 0, 0);
  • 效果:

    • 运行工程 , 程序闪退 .
    • 从手机点开应用 , 应用正常 .
    • 使用Xcode 自带的 Debug - Attach to process 发现附加失败
  • 破解:

    • 通过符号断点检测
    • 使用 fishhook HOOK掉ptrace这个函数




防护: sysctl

  • sysctl ( system control ) 是由 <sys/sysctl.h> 提供的一个函数 , 它有很多作用 , 其中一个是可以监测当前进程有没有被附加 . 但是因为其特性 , 只是监测当前时刻应用有没有被附加 .
  • 因此正向开发中我们往往结合定时器一起使用 , 或者 定时 / 定期 / 在特定时期 去使用 .
#import "ViewController.h"
#import <sys/sysctl.h>
@interface ViewController ()
@end

@implementation ViewController
BOOL isDebug(){
    int name[4];             //里面放字节码。查询的信息
    name[0] = CTL_KERN;      //内核查询
    name[1] = KERN_PROC;     //查询进程
    name[2] = KERN_PROC_PID; //传递的参数是进程的ID
    name[3] = getpid();      //获取当前进程ID
    
    struct kinfo_proc info;  //接受查询结果的结构体
    size_t info_size = sizeof(info);  //结构体大小
    if(sysctl(name, 4, &info, &info_size, 0, 0)){
        NSLog(@"查询失败");
        return NO;
    }
    /**
    查询结果看info.kp_proc.p_flag 的第12位。如果为1,表示调试状态。
    (info.kp_proc.p_flag & P_TRACED) 就是0x800, 即可获取第12位
    */
    return ((info.kp_proc.p_flag & P_TRACED) != 0);
}

static dispatch_source_t timer;
void debugCheck(){
    timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(timer, ^{
        if (isDebug()) {//在这里写你检测到调试要做的操作
            NSLog(@"调试状态!");
        }else{
            NSLog(@"正常!");
        }
    });
    dispatch_resume(timer);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    debugCheck();
}
  • 效果:

    • 可以上报或者 exit
  • 破解:

    • 因为 sysctl函数也是系统函数,从而可以使用fishHook来交换
int  (*sysctl_ptr)(int *, u_int, void *, size_t *, void *, size_t);

int  my_sysctl(int * name, u_int namelen, void * info, size_t * infoSize, void * newInfo, size_t newInfoSize){
    if (name[0] == CTL_KERN && name[1] == KERN_PROC && name[2] == KERN_PROC_PID && *infoSize == sizeof(struct kinfo_proc)) {
        
        int old = sysctl_ptr(name,namelen,info,infoSize,newInfo,newInfoSize);
        //拿出info
        struct kinfo_proc * myinfo = (struct kinfo_proc *)info;
        
        if ((myinfo->kp_proc.p_flag & P_TRACED ) != 0 ) {
            //使用异或取反
            myinfo->kp_proc.p_flag ^= P_TRACED;
        }
        return old;
    }
    return sysctl_ptr(name,namelen,info,infoSize,newInfo,newInfoSize);
}

+(void)load{
  
    struct rebinding rebingSysctl;
    rebingSysctl.name = "sysctl";
    rebingSysctl.replacement = my_sysctl;
    rebingSysctl.replaced = (void *)&sysctl_ptr;
    struct rebinding rebs[1] = {rebingSysctl};
    rebind_symbols(rebs, 1);
}
  • 注意:
    • 需要慎用 exit 函数
    • 逆向中通过 exit 添加符号断点,就可以查看函数调用栈,从而可以看到调用 exit的函数地址,在减去首地址就可以拿到函数的偏移量,接着在 Hopper 当中就可以知道调用 exit 的地址了
    • 我们自己开发所使用的 framework 会比注入的动态库更早的执行,虽然还是会被 fishhook 替换掉,但是可以在此之前,禁用掉 fishhook 或者完成检测
    • 只不过如果破解人员找到这个 framework ,然后在 load 方法中直接 Return




防护: 通过函数地址直接调用 ptracesysctl

  • 在我工程开始我就获取 ptrace / sysctl 的地址 , 后面直接使用地址调用这个函数 . 实际上是可行的 , 利用 dlsym这个函数 .

    • 通过符号获取函数地址 ( dladdr 函数 )
    • 通过函数内部地址找到函数符号 ( dlsym 函数 )
#import "MyPtraceHeader.h"
#import <dlfcn.h>
  
void callFunAddres(void) {
    int name[4];             //里面放字节码。查询的信息
    name[0] = CTL_KERN;      //内核查询
    name[1] = KERN_PROC;     //查询进程
    name[2] = KERN_PROC_PID; //传递的参数是进程的ID
    name[3] = getpid();      //获取当前进程ID
    
    struct kinfo_proc info;  //接受查询结果的结构体
    size_t info_size = sizeof(info);  //结构体大小
    
    //这里做法是隐藏常量字符串
    unsigned char str[] = {
        ('a' ^ 's'),
        ('a' ^ 'y'),
        ('a' ^ 's'),
        ('a' ^ 'c'),
        ('a' ^ 't'),
        ('a' ^ 'l'),
        ('a' ^ '\0')
    };
    unsigned char * p = str;
    printf("%s", str);
    while (((*p) ^= 'a') != '\0') p++;
    
    void * handle = dlopen("/usr/lib/system/libsystem_c.dylib", RTLD_LAZY);

    int  (*sysctl_ptr)(int *, u_int, void *, size_t *, void *, size_t);
    //获取sysctl函数指针
    sysctl_ptr = dlsym(handle,(const char *)str);
    if (sysctl_ptr) {
        
        sysctl_ptr(name, 4, &info, &info_size, 0, 0);
      
        if ((info.kp_proc.p_flag & P_TRACED ) != 0 ){
            NSLog(@"调试状态");
        }else{
            NSLog(@"正常");
        }
    }
}
  • 破解
    • 使用 fishhook dlopendlsym 这两个系统函数干掉




防护 汇编

  • 使用汇编直接调用
- (void)viewDidLoad {
    [super viewDidLoad];
    //使用汇编调用syscall调起ptrace
    #ifdef __arm64__
    asm volatile(
                 "mov x0,#26\n"
                 "mov x1,#31\n"
                 "mov x2,#0\n"
                 "mov x3,#0\n"
                 "mov x16,#0\n"
                 "svc #0x80\n"//这条指令就是触发中断(系统级别的跳转!)
    );
    #endif
     
    //使用汇编直接调用 ptrace
    #ifdef __arm64__
    asm volatile(
                 "mov x0,#31\n"
                 "mov x1,#0\n"
                 "mov x2,#0\n"
                 "mov x16,#26\n"
                 "svc #0x80\n"
                 );
    #endif
}

x16 寄存器就放调用 syscall 需要调用的函数对应编号就可以 . 当然 , 不同架构寄存器指令不同 , 例如调用 exit 我们可以这么写

#ifdef __arm64__
    asm volatile(
                 "mov x0,#0\n"
                 "mov x16,#1\n"
                 "svc #0x80\n"
                 );
#endif
#ifdef __arm__//32位下
    asm volatile(
                 "mov r0,#0\n"
                 "mov r16,#1\n"
                 "svc #80\n"
                 );
#endif
  • 优点:
  • 可以防止系统函数被 fishhook 干掉
  • 添加符号断点并不能断住
  • 攻击者静态分析也比较难以查找



字符串常量隐藏

  • 例如在App内注册第三方APP的Key,SecretKey等,字符串常量隐藏
#define kWxAppID @"krystal69d7xxxxxx"  
 - (void)configureForWXSDK{
    [WXApi registerApp:kWxAppID
         universalLink:@"123123"];
}

利用Hopper打开MachO就可以看到

  • 解决办法
    • 在方法中返回这个字符串
#define KRYSTAL_ENCRYPT_KEY @"krystal_key"
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    //使用函数代替字符串
    [self uploadDataWithKey:AES_KEY()];  
}

- (void)uploadDataWithKey:(NSString *)key{
    NSLog(@"%@",key);
}

static NSString * AES_KEY(){
    unsigned char key[] = {
        'k','r','y','s','t','a','l','_','k','e','y','\0',
    };
    return [NSString stringWithUTF8String:(const char *)key];
}
@end
  • 破解:

    • 静态分析需要找到这个返回 Key 函数
  • 升级防护

    • 通过异或方式
    • 这些字符不会进入字符常量区 . 编译器直接换算成异或结果 .
#define STRING_ENCRYPT_KEY @"demo_AES_key"
#define ENCRYPT_KEY 0xAC
@interface ViewController ()
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
//    [self uploadDataWithKey:STRING_ENCRYPT_KEY]; //使用宏/常量字符串
    [self uploadDataWithKey:AES_KEY()]; //使用函数代替字符串
}

- (void)uploadDataWithKey:(NSString *)key{
    NSLog(@"%@",key);
}

static NSString * AES_KEY(){
    unsigned char key[] = {
        (ENCRYPT_KEY ^ 'd'),
        (ENCRYPT_KEY ^ 'e'),
        (ENCRYPT_KEY ^ 'm'),
        (ENCRYPT_KEY ^ 'o'),
        (ENCRYPT_KEY ^ '_'),
        (ENCRYPT_KEY ^ 'A'),
        (ENCRYPT_KEY ^ 'E'),
        (ENCRYPT_KEY ^ 'S'),
        (ENCRYPT_KEY ^ '_'),
        (ENCRYPT_KEY ^ '\0'),
    };
    unsigned char * p = key;
    while (((*p) ^= ENCRYPT_KEY) != '\0') {
        p++;
    }
    return [NSString stringWithUTF8String:(const char *)key];
}
@end
  • 效果:



动态库检测

  • 可以在服务器上存储一份 _dyld_image_name
  • 然后本地运行后获取到的上传服务器做比对
+ (BOOL)isExternalLibs{
    if(TARGET_IPHONE_SIMULATOR)return NO;
    int dyld_count = _dyld_image_count();
    for (int i = 0; i < dyld_count; i++) {
        const char * imageName = _dyld_get_image_name(i);
        NSString *res = [NSString stringWithUTF8String:imageName];
        if([res hasPrefix:@"/var/containers/Bundle/Application"]){
            if([res hasSuffix:@".dylib"]){
                //这边还需要过滤掉自己项目中本身有的动态库
                return YES;
            }
        }
    }
    return NO;
}
  • 破解:
    • 可以hook NSString的hasPrefix方法绕过检测



越狱检测

防护 NSFileManager

  • 使用NSFileManager通过检测一些越狱后的关键文件/路径是否可以访问来判断是否越狱 常见的文件/路径有
 static char *JailbrokenPathArr[] = {"/Applications/Cydia.app",
                                     "/usr/sbin/sshd",
                                     "/bin/bash",
                                     "/etc/apt",
                                     "/Library/MobileSubstrate",
                                     "/User/Applications/"}; 
      
      
+ (BOOL)isJailbroken1{
    if(TARGET_IPHONE_SIMULATOR)return NO;
    for (int i = 0;i < sizeof(JailbrokenPathArr) / sizeof(char *);i++) {
        if([[NSFileManager defaultManager] fileExistsAtPath:[NSString stringWithUTF8String:JailbrokenPathArr[i]]]){
            return YES;
        }
    }
    return NO;
}
  • 破解
    • 攻击者可以通过hook NSFileManager的fileExistsAtPath方法来绕过检测
//绕过使用NSFileManager判断特定文件是否存在的越狱检测,此时直接返回NO势必会影响程序中对这个方法的正常使用,因此可以先打印一下path,然后判断如果path是用来判断是否越狱则返回NO,否则按照正常逻辑返回
%hook NSFileManager
- (BOOL)fileExistsAtPath:(NSString *)path{
    if(TARGET_IPHONE_SIMULATOR)return NO;
    for (int i = 0;i < sizeof(JailbrokenPathArr) / sizeof(char *);i++) {
        NSString *jPath = [NSString stringWithUTF8String:JailbrokenPathArr[i]];
        if([path isEqualToString:jPath]){
            return NO;
        }
    }
    return %orig;
}
%end



防护 stat 函数

  • 使用C语言函数stat判断文件是否存在(注:stat函数用于获取对应文件信息,返回0则为获取成功,-1为获取失败)
+ (BOOL)isJailbroken2{
    if(TARGET_IPHONE_SIMULATOR)return NO;
    for (int i = 0;i < sizeof(JailbrokenPathArr) / sizeof(char *);i++) {
        struct stat stat_info;
        if (0 == stat(JailbrokenPathArr[i], &stat_info)) {
            return YES;
        }
    }
    return NO;
}      
  • 破解:
    • 使用fishhook可hook C函数,fishhook通过在mac-o文件中查找并替换函数地址达到hook的目的
static int (*orig_stat)(char *c, struct stat *s);
int hook_stat(char *c, struct stat *s){
    for (int i = 0;i < sizeof(JailbrokenPathArr) / sizeof(char *);i++) {
        if(0 == strcmp(c, JailbrokenPathArr[i])){
            return 0;
        }
    }
    return orig_stat(c,s);
}
+(void)statHook{
    struct rebinding stat_rebinding = {"stat", hook_stat, (void *)&orig_stat};
    rebind_symbols((struct rebinding[1]){stat_rebinding}, 1);
}

在动态库加载的时候,调用statHook

 %ctor{
    [StatHook statHook];
}
  • 判断stat的来源是否来自于系统库,因为fishhook通过交换函数地址来实现hook,若hook了stat,则stat来源将指向攻击者注入的动态库中 因此我们可以完善上方的isJailbroken2判断规则,若stat来源非系统库,则直接返回已越狱
+ (BOOL)isJailbroken2{
    if(TARGET_IPHONE_SIMULATOR)return NO;
    int ret ;
    Dl_info dylib_info;
    int (*func_stat)(const char *, struct stat *) = stat;
    if ((ret = dladdr(func_stat, &dylib_info))) {
        NSString *fName = [NSString stringWithUTF8String:dylib_info.dli_fname];
        NSLog(@"fname--%@",fName);
        if(![fName isEqualToString:@"/usr/lib/system/libsystem_kernel.dylib"]){
            return YES;
        }
    }
    
    for (int i = 0;i < sizeof(JailbrokenPathArr) / sizeof(char *);i++) {
        struct stat stat_info;
        if (0 == stat(JailbrokenPathArr[i], &stat_info)) {
            return YES;
        }
    }
    
    return NO;
}



BundleID检测

  • 进行BundleID检测可以有效防止多开
  • 获取当前项目的BundleID有多种方法,此处不再赘述,绕过检测则是hook对应的方法,返回原有的BundleID
  • 防止攻击者绕过检测,可以在自行link的framework中获取BundleID并进行检测,以在被hook前进行校验 BundleID并进行校验以避免常见的BundleID获取方法被hook
//获取Boundle ID
char  * bundleName =  getenv("XPC_SERVICE_NAME");
    NSLog(@"%s",bundleName);

定位防护

  1. 在越狱设备中通过方法交换的形式来拦截CLLocationManager对象的 delegate属性中的 - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations实现来返回虚假的定位数据。
  2. 在未越狱的设备上通过电脑和手机进行USB连接,电脑通过特殊协议向手机上的DTSimulateLocation服务发送模拟的坐标数据来实现虚假定位,目前Xcode上内置位置模拟就是借助这个技术来实现的。
  3. 在未越狱的设备上通过符合MFI认证的蓝牙设备和手机连接,并通过MFI蓝牙设备向手机发送虚假的定位数据来实现定位模拟。比较有代表意义的就是市面上的:位置精灵app。
  • 防护
    • 此类定位的海拔高度都是为0,将为0的地区排除掉就可以得知
    • 可以获取本地的IP地址或者WIFI连接后的WIFI地址(但是需要申请权限)

获取局域网IP

// 获取局域网内的ip地址
+ (nullable NSString*)getCurrentLocalIP
{
    NSString *address = nil;
    struct ifaddrs *interfaces = NULL;
    struct ifaddrs *temp_addr = NULL;
    int success = 0;
    // retrieve the current interfaces - returns 0 on success
    success = getifaddrs(&interfaces);
    if (success == 0) {
        // Loop through linked list of interfaces
        temp_addr = interfaces;
        while(temp_addr != NULL) {
            if(temp_addr->ifa_addr->sa_family == AF_INET) {
                // Check if interface is en0 which is the wifi connection on the iPhone
                if([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"en0"]) {
                    // Get NSString from C String
                    address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)];
                }
            }
            temp_addr = temp_addr->ifa_next;
        }
    }
    // Free memory
    freeifaddrs(interfaces);
    return address;
}
  • 还有一种一层一层通过kvc读取CLLocation的_internal的fLocation,只能读取到到此。再通过kvc读取会报以下错误:

  • 在每个CLLocation对象中有一个内部的私有数据成员:_internal 这个内部的私有属性是一个内部类CLLocationInternal的实例。而这个内部类CLLocationInternal中有一个结构体CLLocationInfo指针数据成员:fLocation。CLLocationInfo结构体定义如下:

 struct CLLocationInfo {
#if defined(__i386__) || defined(__x86_64__)
    int padding1;
#endif
    int suitability;    //定位的magic数
    CLLocationCoordinate2D coordinate;   //经纬度
    CLLocationAccuracy horizontalAccuracy;   //水平精确度
    CLLocationDistance altitude;    //海拔
    CLLocationAccuracy verticalAccuracy;  //垂直精确度 
#if defined(__i386__) || defined(__x86_64__)
    double padding2;
    double padding3;
#endif
    CLLocationSpeed speed;   //速度
    CLLocationAccuracy speedAccuracy;  //速度精确度
    CLLocationDirection course;   //方向
    CLLocationAccuracy courseAccuracy;  //方向精确度
    double timestamp;  //时间戳
    int confidence;   //置信值?
    double lifespan;   //有效期限?
    int type;   //定位数据来源类型
   CLLocationCoordinate2D rawCoordinate;  //原始经纬度
    CLLocationDirection rawCourse;    //原始方向
    int floor;     //楼层
#if !defined(__i386__)
    unsigned int integrity; //信息完整度? :75=High 50=Medium  25=Low  0=None
    int referenceFrame; //参考坐标系:2=ChinaShifted 1=wgs84 0=unknown
    int rawReferenceFrame;  //原始参考坐标系
#endif
};

但是此方法没有成功 拿不到 CLLocationInfo结构体

  https://boostnote.io/shared/91d30de9-6ebf-42dc-921e-656c6830a840

About

iOS 安全防护

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published