本文只是用于记录,在工作中遇到有人攻击我们APP,我们所做的一些事。
- 在无意间,查看数据库数据,发现有许多数据,像是利用脚本提交的。至于怎么看出来的,在这儿不好说,毕竟是公司项目。但是有一点是可以确定的,就是他的设备是iOS设备,那么说明我们的APP存在安全隐患。
- 最开始,我们的APP,数据层没有进行任何形式的加密,后端也没有对ip进行校验,同时网络层,也没有做HTTPS加密,同时,APP本地更没有做安全加固。
于是乎,我们想到的第一件事,就是对重要的数据进行加密处理,请求包数据进行了MD5(可参考:https://www.jianshu.com/p/d7fcb503bd15),本地用户敏感数据进行钥匙串保存(demo:https://github.com/deng690990/SF_KeyChain),同时我们为网络请求加上了HTTPS证书校验。但是很悲剧的事情发生了,我们做的一切仅仅维持了一周,又有脚本数据出现在了数据库。
这个时候,我们意识到,别人可能是对我们的IPA包植入了动态库,同时结合静态分析,把我们app的主要逻辑代码摸清了,以至于人家能用脚本直接提交数据。下面我就来谈谈我们是怎么一步步防住了对方。
一、埋点,采集数据。
埋点可以用三方,也可以用自己的服务器,友盟就有自定义事件采集。
- 埋点需要采集的信息主要是:重签名,越狱设备进行的操作,还有动态调试等。
一、检测越狱机:
封装一个工具类,头文件导入:
//防越狱相关
#import <sys/stat.h>
#import <dlfcn.h>
#import <stdlib.h>
#import <mach-o/dyld.h>
#define kApplicationIdentifier @"这里是你的证书的组织标识,可在本地钥匙串查看"
/**
以下方法,防止越狱设备,防止hook,防止代码注入,命名故意错误方式命名,防止别人逆向
*/
+ (BOOL)Youmeng1 {
//可能存在hook了NSFileManager方法,此处用底层C stat去检测
// /Library/MobileSubstrate/MobileSubstrate.dylib 最重要的越狱文件,几乎所有的越狱机都会安装MobileSubstrate
// /Applications/Cydia.app/ /var/lib/cydia/绝大多数越狱机都会安装
struct stat stat_info;
if (0 == stat("/Library/MobileSubstrate/MobileSubstrate.dylib", &stat_info)) {
return YES;
}
if (0 == stat("/Applications/Cydia.app", &stat_info)) {
return YES;
}
if (0 == stat("/var/lib/cydia/", &stat_info)) {
return YES;
}
if (0 == stat("/var/cache/apt", &stat_info)) {
return YES;
}
if (0 == stat("/var/lib/apt", &stat_info)) {
return YES;
}
if (0 == stat("/etc/apt", &stat_info)) {
return YES;
}
if (0 == stat("/bin/bash", &stat_info)) {
return YES;
}
if (0 == stat("/bin/sh", &stat_info)) {
return YES;
}
if (0 == stat("/usr/sbin/sshd", &stat_info)) {
return YES;
}
if (0 == stat("/usr/libexec/ssh-keysign", &stat_info)) {
return YES;
}
if (0 == stat("/etc/ssh/sshd_config", &stat_info)) {
return YES;
}
return NO;
}
+ (BOOL)Youmeng2 {
//可能存在stat也被hook了,可以看stat是不是出自系统库,有没有被攻击者换掉
//这种情况出现的可能性很小
int ret;
Dl_info dylib_info;
int (*func_stat)(const char *,struct stat *) = stat;
if ((ret = dladdr(&func_stat, &dylib_info))) {
NSLog(@"lib:%s",dylib_info.dli_fname); //如果不是系统库,肯定被攻击了
if (strcmp(dylib_info.dli_fname, "/usr/lib/system/libsystem_kernel.dylib")) { //不相等,肯定被攻击了,相等为0
return YES;
}
}
return NO;
}
+ (BOOL)Youmeng3 {
//列出所有已链接的动态库:
//通常情况下,会包含越狱机的输出结果会包含字符串: Library/MobileSubstrate/MobileSubstrate.dylib 。
uint32_t count = _dyld_image_count();
for (uint32_t i = 0 ; i < count; ++i) {
NSString *name = [[NSString alloc]initWithUTF8String:_dyld_get_image_name(i)];
if ([name containsString:@"Library/MobileSubstrate/MobileSubstrate.dylib"]) {
return YES;
}
}
return NO;
}
+ (BOOL)Youmeng4 {
//如果攻击者给MobileSubstrate改名,但是原理都是通过DYLD_INSERT_LIBRARIES注入动态库
//那么可以,检测当前程序运行的环境变量
char *env = getenv("DYLD_INSERT_LIBRARIES");
if (env != NULL) {
return YES;
}
return NO;
}
这里我是故意对方法名乱命名,是为了混淆逆向人员
二、检测重签名:
/**
以下是防重签的方法
*/
+(void)Youmeng5{
//重签名检测
NSString *teamIdentifier = bundleTeamIdentifier();
if ([teamIdentifier isNotEmptyObject] && ![teamIdentifier isEqualToString:kApplicationIdentifier]) {
[MobClick updateDataWithString:@"APPResign"];
#ifdef __arm64__
asm volatile(
"mov x0,#0\n"
"mov w16,#1\n"
"svc #0x80\n"
);
#endif
#ifdef __arm__
asm volatile(
"mov r0,#0\n"
"mov r12,#1\n"
"svc #0x80\n"
);
#endif
}
}
/// 拿到证书里的组织标识
static NSString *bundleTeamIdentifier(void)
{
NSString *mobileProvisionPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"embedded.mobileprovision"];
FILE *fp=fopen([mobileProvisionPath UTF8String],"r");
char ch;
if(fp==NULL) {
return NULL;
}
NSMutableString *str = [NSMutableString string];
while((ch=fgetc(fp))!=EOF) {
[str appendFormat:@"%c",ch];
}
fclose(fp);
NSString *teamIdentifier = nil;
NSRange teamIdentifierRange = [str rangeOfString:@"<key>com.apple.developer.team-identifier</key>"];
if (teamIdentifierRange.location != NSNotFound) {
NSInteger location = teamIdentifierRange.location + teamIdentifier.length;
NSInteger length = [str length] - location;
if (length > 0 && location >= 0) {
NSString *newStr = [str substringWithRange:NSMakeRange(location, length)];;
NSArray *val = [newStr componentsSeparatedByString:@"</string>"];
NSString *v = [val firstObject];
NSRange startRange = [v rangeOfString:@"<string>"];
NSInteger newLocation = startRange.location + startRange.length;
NSInteger newLength = [v length] - newLocation;
if (newLength > 0 && location >= 0) {
teamIdentifier = [v substringWithRange:NSMakeRange(newLocation, newLength)];
}
}
}
return teamIdentifier;
}
三、混淆方法名,防止别人class_dump。这个可以用宏定义去混淆,也可以用三方库。
四、防止动态调试:
- 防止反调试的方法最好用一个私有库来做,为什么呢?将代码放到framework里,逆向成本高得多,当然并不是不可能破解,毕竟没有绝对的安全。
- 头文件导入#import <sys/sysctl.h>
static __attribute__((always_inline)) void workwell() {
#ifdef __arm64__
asm volatile(
"mov x0,#0\n"
"mov w16,#1\n"
"svc #0x80\n"
);
#endif
#ifdef __arm__
asm volatile(
"mov r0,#0\n"
"mov r12,#1\n"
"svc #0x80\n"
);
#endif
}
//检测调试
BOOL isDlnaInitialize(){
int name[4];//里面放字节码。查询的信息
name[0] = CTL_KERN;//内核查询
name[1] = KERN_PROC;//查询进程
name[2] = KERN_PROC_PID;//传递的参数是进程的ID
name[3] = getpid();//PID的值
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)
return ((info.kp_proc.p_flag & P_TRACED) != 0);
}
static dispatch_source_t timer;
void dlnaInitialize(){
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 30.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
if (isDlnaInitialize()) {
workwell();
}
});
dispatch_resume(timer);
}
static __attribute__((always_inline)) void UPnPInitialize() {
#ifdef __arm64__
asm volatile(
"mov x0,#26\n"
"mov x1,#31\n"
"mov x2,#0\n"
"mov x3,#0\n"
"mov x16,#0\n"//中断根据x16 里面的值,跳转syscall
"svc #0x80\n"//这条指令就是触发中断(系统级别的跳转!)
);
#endif
}
+ (void)load {
UPnPInitialize();
dlnaInitialize();
}
- 强调一下这几句代码的作用:利用汇编代码,强制退出程序,比exit(0)要安全一些,至少不会被hook。
#ifdef __arm64__
asm volatile(
"mov x0,#0\n"
"mov w16,#1\n"
"svc #0x80\n"
);
#endif
#ifdef __arm__
asm volatile(
"mov r0,#0\n"
"mov r12,#1\n"
"svc #0x80\n"
);
#endif
}
这是目前我们做了的技术防护。还有一部分是逻辑防护,这个就得根据项目的实际情况来,比如我们的项目,就是一个用户只能一台设备一个ip,当用户提交的数据不满足任意条件就会失败。同时,我们在登录模块加了图形验证码,还有短信验证码,就是一旦检测到用户换了设备,只能手机验证码登录。
本来我们还想做防hook操作,可是这一步已经拦住了破我们app的人。这个时候,我们就对数据库里,脚本提交的数据,做了清理,进一步让那些依靠逆向人员提交假数据的人和逆向人员之间出现了信任问题,从此,太平了。希望以后我们的APP能一帆风顺。