爱伪装(AWZ)/爱立思(ALS)改机改串一键新机原理分析

爱伪装(AWZ)/爱立思(ALS)改机改串一键新机原理分析

(如有合作,交流方面的意愿,请联系QQ:571652571)

分析要点

这里只做学习讨论改机原理及ALS自身反逆向机制。ALS安装后,有如下文件:

  • /Applications/ALS.app 主程序,用于生成改机参数,参数保存在
  • /Library/LaunchDaemons/dhpdaemon.plist 用于daemon方式执行DHPDaemon,用于帮助ALS实现一些隐藏操作
  • /usr/bin/DHPDaemon
  • /MobileSubstrate/DynamicLibraries/ALS.{dylib,plist},该tweak通过hook一些可以获取系统属性和app属
    性的C函数和ObjC函数实现的修改app参数

第一阶段

  • ALS属性2755防止加载tweak

  • 存在restrict段,防止被加载tweak

  • 存在syscall函数进行ptrace系统调用,存在ptrace函数,以及svc汇编指令实现的ptrace,防止调试器附加

  • 对于restrict段和汇编指令反调试的处理,解法就是patch文件然后重签名,但是要写一个通用的命令是比较困
    难的,慢慢收集吧,这里提供的方式如下:
    sed -i 's/RESTRICT/RXSTRICT/g' ALS
    sed -i 's/\x80\x52\x01\x10\x00\xd4/\x80\x52\x1f\x20\x03\xd5/g' ALS

  • 对于文件属性和函数级的反调试,方法不再赘述,写tweak即可

第二阶段

在第一阶段破解后,仍然是闪退的,此时反调试已经去除,所以可以加调试器看检测点

  • -[NSBundle executablePath]检测主进程文件是否被修改
  • _dyld_get_image_name检测tweak模块
  • sysctl检测进程的p_flag是否有调试器flag
  • isatty检测终端
  • ioctl(TIOCGWINSZ)检测终端
  • fopen检测主进程文件是否被修改(在DHPDaemon中)
  • posix_spawn检测/Application/ALS.app下是否存在超过3M的文件(补丁啊)

注意

在IFMagicMainVC的几个按钮handler函数中,存在大量检测代码,这里建议直接自己实现,例如:

void (*old_IFMagicMainVC_doGoMagicSetting)(Class cls, SEL sel, void* click);
void new_IFMagicMainVC_doGoMagicSetting(Class cls, SEL sel, void* click) {
    UIStoryboard* board = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    UIViewController* newcontrol = [board instantiateViewControllerWithIdentifier:@"IFMagicSettingVC"];
    UIViewController* control = (UIViewController*)cls;
    [[control navigationController] pushViewController:newcontrol animated:YES];
}

void (*old_IFMagicMainVC_paramSettingClick)(Class cls, SEL sel, void* click);
void new_IFMagicMainVC_paramSettingClick(Class cls, SEL sel, void* click) {
    UIStoryboard* board = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    UIViewController* newcontrol = [board instantiateViewControllerWithIdentifier:@"IFMagicDeviceSettingVC"];
    UIViewController* control = (UIViewController*)cls;
    [[control navigationController] pushViewController:newcontrol animated:YES];
}

void (*old_IFMagicMainVC_appListClick)(Class cls, SEL sel, void* click);
void new_IFMagicMainVC_appListClick(Class cls, SEL sel, void* click) {
    UIStoryboard* board = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    UIViewController* newcontrol = [board instantiateViewControllerWithIdentifier:@"IFApplicationSelectorVC"];
    UIViewController* control = (UIViewController*)cls;
    [[control navigationController] pushViewController:newcontrol animated:YES];
}

第三阶段

在第二阶段后,不闪退了,但是显示注册码过期

  • 解密栈字符串,ALS和DHPDaemon几乎全用的栈字符串混淆,蛮体力活的
  • 定位到注册的函数,一方面通过socket通信,用加密本机信息获取软件激活状态,另一方面通过cjson反序列化回写注册状态。
  • 定位get_json_value函数,该函数为c层cjson解析函数,用于从json数据的到key对应的value,该函数刚好位于socket网络通信,解密响应得到json数据后。其中必要重要的key有:ps,vs,hs,ts,as,aes
  • 还原ObjC函数调用关系
  • system函数会独立向服务器验证激活码,如果校验不过会删除backup文件,这样操作记录都没了,但是并没实际删除,可通过fopen检测绕过
  • 控制软件注册状态的主要字段是status和expiry_date,分别对应注册状态及过期时间字符串,其中status字段含义为:
    enum {
    STATE_LOCKED = 0, // 已锁定
    STATE_NORMAL = 1, // 已激活
    STATE_INACTIVE1 = 2, // 未激活
    STATE_OUTDATE = 3, // 激活码过期
    STATE_INACTIVE2 = 4, // 未激活
    STATE_LOGOFF = 5, // 已注销
    };
  • 对于栈字符串的处理,见:https://github.com/lichao890427/personal_script/blob/master/IDA_Script/
    parse_stack_string.py
  • 对于ObjC函数调用关系还原,见:https://github.com/lichao890427/personal_script/blob/master/IDA_Script/
    add_xref_for_macho.py
  • 使用网络请求方式更新注册状态的响应中,get_json_value获取的as键对应status,aes键对应于expiry_date
    另外一些字段用于激活码验证,如果不通过则结束进程,可以自行在newAppEnvClick函数中研究。
  • 使用cjson反序列化回写注册状态逻辑存在于文件/private/var/mobile/Library/Preferences/
    com.app1e.mobile.ifalscommon.plist,解密后仍然是json数据,要修改的字段如下:
    {
    "authInfo": {
    "status": @0,
    "expiry_date": @"21000101080000000"
    }
    }
  • mapapi.bundle模块存在一些干扰,在hook函数的时候要注意

第四阶段

a. 捕获按钮触发的功能函数
b. 分析ALS和DHPDaemon的notify通信,有些重要函数是ALS调用DHPDaemon执行

a. 捕获按钮触发,利用frida脚本,https://github.com/lichao890427/personal_script/blob/master/
Frida_script/utils.js,这里tranverse_view用于检测当前呈现的界面可以获取的元素,以及对应的响应
selector,如果找按钮的回调,又不想触发,可以用这个。另外更通用的得是trace_view函数,可以拦截到
所有界面消息以及响应selector,在执行点击等操作后可以得到更全的信息

b. 下面是一些分析结果:
清理safari逻辑在函数中-[IFMagicMainVC cleanSafariClick:]

    // 杀死进程
    BKSTerminateApplicationForReasonAndReportWithDescription(__bridge CFStringRef)@"com.apple.mobilesafari", 5, 0, NULL);
    NSFileManager* man = [NSFileManager defaultManager];
    
    // 清理cookie
    NSString cookiepath = @"/var/mobile/Library/Cookies";
    if ([man fileExistsAtPath:cookiepath]) {
        NSString* cmd = [NSString stringWithFormat:@"rm -rf %@/*", cookiepath];
        system(cmd);
    }
    cookiepath = @"/private/var/root/Library/Cookies";
    if ([man fileExistsAtPath:cookiepath]) {
        NSString* cmd = [NSString stringWithFormat:@"rm -rf %@/*", cookiepath];
        system(cmd);
    }
    
    // 获取safari的沙盒路径
    NSString* safaricontainer = nil;
    NSString* installplist = @"/var/mobile/Library/Caches/com.apple.mobile.installation.plist";
    if ([man fileExistsAtPath:]) {
        NSDictionary* plist = [NSDictionary dictionaryWithContentsOfFile:installplist];
        id obj = plist[@"User"][@"com.apple.mobilesafari"];
        if (obj == nil) {
            obj = plist[@"System"][@"com.apple.mobilesafari"];
        }
        if (obj != nil) {
            safaricontainer = obj[@"Container"];
        }
    } else {
        Class* LSApplicationProxy = NSClassFromString(@"LSApplicationProxy");
        id obj = [LSApplicationProxy performSelector:applicationProxyForIdentifier: withObject:@"com.apple.mobilesafari"]);
        if (obj != nil && [obj respondsToSelector:@selector(dataContainerURL)]) {
            safaricontainer = [[obj performSelector:@selector(dataContainerURL)] path];
        }
    }
    
    // 清理library
    NSString* libpath = [safaricontainer stringByAppendingPathComponent:@"Library"];
    NSString* libcachepath = [libpath stringByAppendingPathComponent:@"Caches"];
    if ([man fileExistsAtPath:libcachepath]) {
        NSString* cmd = [NSString stringWithFormat:@"rm -rf %@/*", libcachepath];
清理keychain逻辑在函数中-[IFMagicMainVC cleanKeychainClick:]
    NSFileManager* man = [NSFileManager defaultManager];
    if ([man fileExistsAtPath:@"/var/Keychains/keychain-2.db"]) {
    system("cp /var/Keychains/keychain-2.db /tmp/");
    void* ppDb = 0;
    char cmd[256];
    if (0 == sqlite3_open("/tmp/keychain-2.db", &ppDb)) {
        strcpy(cmd, "DELETE FROM cert WHERE agrp<>'apple' and agrp not like '%apple%' and agrp <> 'ichat' and agrp <>'lockdown-identities'");
        sqlite3_exec(ppDb, cmd, 0, 0, 0);
        strcpy(cmd, "DELETE FROM keys WHERE agrp<>'apple' and agrp not like '%apple%' and agrp <> 'ichat' and agrp <>'lockdown-identities'");
        sqlite3_exec(ppDb, cmd, 0, 0, 0);
        strcpy(cmd, "DELETE FROM inet WHERE agrp<>'apple' and agrp not like '%apple%' and agrp <> 'ichat' and agrp <>'lockdown-identities'");
        sqlite3_exec(ppDb, cmd, 0, 0, 0);
        system("cp /tmp/keychain-2.* /var/Keychains/");
    }
清理pasteboard逻辑在函数中-[IFMagicMainVC cleanPastboardClick:]
    UIPasteboard* pb = [UIPasteboard generalPasteboard];
    if (pb != nil) {
        NSArray* items = [pb items];
        if (items != nil) {
            [items removeAllObjects];
        }
        [pb setItems:items];
    }
    
    NSFileManager* man = [NSFileManager defaultManager];
    NSProcessInfo* proc = [NSProcessInfo processInfo];
    BOOL isbe8 = FALSE;
    NSOperatingSystemVersion ver;
    ver.majorVersion = 8;
    ver.minorVersion = 0;
    ver.patchVersion = 0;
    if ([proc respondsToSelector:@selector(isOperatingSystemAtLeastVersion:&ver)]) {
        isbe8 = [proc isOperatingSystemAtLeastVersion:&ver];
    }
    
    NSString* pbplist = nil;
    NSString* pbbundle = nil;
    if ([man fileExistsAtPath:@"/System/Library/LaunchDaemons/com.apple.UIKit.pasteboardd.plist"]) {
        pbplist = @"/System/Library/LaunchDaemons/com.apple.UIKit.pasteboardd.plist";
        pbbundle = @"com.apple.UIKit.pasteboardd";
    }
    else if ([man fileExistsAtPath:@"/Library/LaunchDaemons/com.apple.UIKit.pasteboardd.plist"]) {
        pbplist = @"/Library/LaunchDaemons/com.apple.UIKit.pasteboardd.plist";
        pbbundle = @"com.apple.UIKit.pasteboardd";
    }
    else if ([man fileExistsAtPath:@"/System/Library/LaunchDaemons/com.apple.pasteboard.pasted.plist"]) {
        pbplist = @"/System/Library/LaunchDaemons/com.apple.pasteboard.pasted.plist";
        pbbundle = @"com.apple.pasteboard.pasted";
    }
    
    BOOL pbdbexist = [man fileExistsAtPath:@"/var/mobile/Library/Caches/com.apple.UIKit.pboard/pasteboardDB"];
    NSString* pbcontainer = nil;
    if ([man fileExistsAtPath:@"/var/mobile/Library/Caches/com.apple.UIKit.pboard"]) {
        pbcontainer = @"/var/mobile/Library/Caches/com.apple.UIKit.pboard";
    } else if ([man fileExistsAtPath:@"/var/mobile/Library/Caches/com.apple.Pasteboard"]) {
        pbcontainer = @"/var/mobile/Library/Caches/com.apple.Pasteboard";
    }
    if (!isbe8 && [man fileExistsAtPath:pbplist]) {
        system("launchctl unload -w");
    }
    if (pbcontainer != nil && [man fileExistsAtPath:pbcontainer]) {
        NSString* cmd = [NSString stringWithFormat:@"rm -rf %@/*", pbcontainer];
        system([cmd UTF8String]);
    }
    if (pbdbexist) {
        NSString* cmd = [NSString stringWithFormat:@"cp %@ %@", @"/Applications/ALS.app/pb.dat", @"/var/mobile/Library/Caches/com.apple.UIKit.pboard/pasteboardDB"];
        system([cmd UTF8String]);
    }

思考

改机原理是什么?

在iOS上目前所有流行的改机工具,本质上是利用substrate框架对某些用来获取设备和系统参数函数进行hook,从而欺骗App达到修改的目的,具体的如下:

  • 用作获取设备参数的函数,无论是C函数,还是Objective-C/Swift函数,可以使用hook框架来修改其返回值
  • 361是通过hook系统服务

一键新机怎么实现的?

在用户进行一键新机时,ALS有如下操作:

  • 生成设备参数并保存到文件
/private/var/mobile/Library/Preferences/
com.app1e.mobile.ifalscommon.plist  保存伪造设备参数数据
com.app1e.mobile.ifalslocation.plist 保存伪造位置数据
  • 将应用沙盒目录下的数据备份,同时为新环境创建沙盒目录结构
    备份的数据存放在/private/var/mobile/alsdata

  • 应用启动后,ALS.dylib会hook关键函数,并根据plist文件修改函数返回的数据
    在这一步,ALS还会根据情况清理keychain,同时做简单的反越狱检测

我该如何保护自己的App,防止被改机软件篡改设备信息?

可以将一些重要检测使用汇编指令(SVC)代替函数调用完成,同时使用内存校验来检测汇编代码是否被篡改。这样AWZ这种通用工具便无能为力,因为hook框架只能做到函数级别的hook。

你可能感兴趣的:(爱伪装(AWZ)/爱立思(ALS)改机改串一键新机原理分析)