爱伪装(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。