本文只是用于記錄,在工作中遇到有人攻擊我們APP,我們所做的一些事。
- 在無意間,查看數(shù)據(jù)庫數(shù)據(jù),發(fā)現(xiàn)有許多數(shù)據(jù),像是利用腳本提交的。至于怎么看出來的,在這兒不好說,畢竟是公司項目。但是有一點是可以確定的,就是他的設備是iOS設備,那么說明我們的APP存在安全隱患。
- 最開始,我們的APP,數(shù)據(jù)層沒有進行任何形式的加密,后端也沒有對ip進行校驗,同時網(wǎng)絡層,也沒有做HTTPS加密,同時,APP本地更沒有做安全加固。
于是乎,我們想到的第一件事,就是對重要的數(shù)據(jù)進行加密處理,請求包數(shù)據(jù)進行了MD5(可參考:http://www.itdecent.cn/p/d7fcb503bd15),本地用戶敏感數(shù)據(jù)進行鑰匙串保存(demo:https://github.com/deng690990/SF_KeyChain),同時我們?yōu)榫W(wǎng)絡請求加上了HTTPS證書校驗。但是很悲劇的事情發(fā)生了,我們做的一切僅僅維持了一周,又有腳本數(shù)據(jù)出現(xiàn)在了數(shù)據(jù)庫。
這個時候,我們意識到,別人可能是對我們的IPA包植入了動態(tài)庫,同時結(jié)合靜態(tài)分析,把我們app的主要邏輯代碼摸清了,以至于人家能用腳本直接提交數(shù)據(jù)。下面我就來談談我們是怎么一步步防住了對方。
一、埋點,采集數(shù)據(jù)。
埋點可以用三方,也可以用自己的服務器,友盟就有自定義事件采集。
- 埋點需要采集的信息主要是:重簽名,越獄設備進行的操作,還有動態(tài)調(diào)試等。
一、檢測越獄機:
封裝一個工具類,頭文件導入:
//防越獄相關
#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/絕大多數(shù)越獄機都會安裝
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是不是出自系統(tǒng)庫,有沒有被攻擊者換掉
//這種情況出現(xiàn)的可能性很小
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); //如果不是系統(tǒng)庫,肯定被攻擊了
if (strcmp(dylib_info.dli_fname, "/usr/lib/system/libsystem_kernel.dylib")) { //不相等,肯定被攻擊了,相等為0
return YES;
}
}
return NO;
}
+ (BOOL)Youmeng3 {
//列出所有已鏈接的動態(tài)庫:
//通常情況下,會包含越獄機的輸出結(jié)果會包含字符串: 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注入動態(tài)庫
//那么可以,檢測當前程序運行的環(huán)境變量
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。這個可以用宏定義去混淆,也可以用三方庫。
四、防止動態(tài)調(diào)試:
- 防止反調(diào)試的方法最好用一個私有庫來做,為什么呢?將代碼放到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
}
//檢測調(diào)試
BOOL isDlnaInitialize(){
int name[4];//里面放字節(jié)碼。查詢的信息
name[0] = CTL_KERN;//內(nèi)核查詢
name[1] = KERN_PROC;//查詢進程
name[2] = KERN_PROC_PID;//傳遞的參數(shù)是進程的ID
name[3] = getpid();//PID的值
struct kinfo_proc info;//接受查詢結(jié)果的結(jié)構(gòu)體
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,表示調(diào)試狀態(tài)。
//(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"http://中斷根據(jù)x16 里面的值,跳轉(zhuǎn)syscall
"svc #0x80\n"http://這條指令就是觸發(fā)中斷(系統(tǒng)級別的跳轉(zhuǎn)?。? );
#endif
}
+ (void)load {
UPnPInitialize();
dlnaInitialize();
}
- 強調(diào)一下這幾句代碼的作用:利用匯編代碼,強制退出程序,比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
}
這是目前我們做了的技術防護。還有一部分是邏輯防護,這個就得根據(jù)項目的實際情況來,比如我們的項目,就是一個用戶只能一臺設備一個ip,當用戶提交的數(shù)據(jù)不滿足任意條件就會失敗。同時,我們在登錄模塊加了圖形驗證碼,還有短信驗證碼,就是一旦檢測到用戶換了設備,只能手機驗證碼登錄。
本來我們還想做防hook操作,可是這一步已經(jīng)攔住了破我們app的人。這個時候,我們就對數(shù)據(jù)庫里,腳本提交的數(shù)據(jù),做了清理,進一步讓那些依靠逆向人員提交假數(shù)據(jù)的人和逆向人員之間出現(xiàn)了信任問題,從此,太平了。希望以后我們的APP能一帆風順。