這一篇文章著重於保護重要數據不被攻擊者使用Cycript或者Runtime修改,概要內容如下:
防止choose(類名)
禁忌,二重存在
自己的內存塊
虛偽的setter/getter
加密內存數據
English version is here
以下內容均以此假想情況為基礎: 我們有一個Person類,它的定義如下:
@interface Person : NSObject { NSString * _name; int _age; } @property (strong, nonatomic, readonly) NSString * name; @property (nonatomic, readonly) int age; - (instancetype)initWithName:(NSString *)name age:(int)age; @end @implementation Person @synthesize name = _name; @synthesize age = _age; - (instancetype)initWithName:(NSString *)name age:(int)age{ self = [self init]; if (self) { _name = name; _age = age; } return self; } - (void)setName:(NSString *)name { if (name != _name) { _name = name ; } } - (void)setAge:(int)age { _age = age; } - (NSString *)name { return _name; } - (int)age { return _age; } @end
現在我們需要保護這個類的數據,雖然我們在@property裡聲明了這兩個都是readonly,但是因為Objective-C的runtime特性,這個屬性說了基本等於沒說(對於破解者而言)。 那麼我們要怎麼做才能保護呢?
防止choose(類名)
我們知道,在Cycript中可以很方便的使用choose(類名)來獲取到App中該類所有的實例變量(圖1),那麼我們就先從這裡下手吧!
解決方案: 重載- (NSString *)description方法。效果如圖2所示。
- (NSString *)description { return [NSString stringWithFormat:@"This person is named %@, aged %d.", self.name, self.age]; }
禁忌,二重存在
上面雖然在cycript中用choose函數拿不到了,但是如果一開始就被Hook了init方法怎麼辦呢?
解決方案:memcpy一份。
首先確定Person類實例的大小:(類指針大小+所有成員變量大小)
ssize_t object_size = sizeof(Person *) + sizeof(NSString *) + sizeof(int);
然後就可以愉快的memcpy了:
Person * normal_man = [[Person alloc] initWithName:@"Nobody" age:0]; void * superman = malloc(object_size); memcpy(superman, (__bridge void *)normal_man, object_size);
在用的時候,通過__bridge轉換:
[(__bridge Person *)superman setName:@"Superman"];
代碼片段:
Person * normal_man = [[Person alloc] initWithName:@"Nobody" age:0]; ssize_t object_size = sizeof(Person *) + sizeof(NSString *) + sizeof(int); void * superman = malloc(object_size); memcpy(superman, (__bridge void *)normal_man, object_size); [(__bridge Person *)superman setName:@"Superman"]; [(__bridge Person *)superman setAge:20]; /** * @brief 為了演示方便加的while */ while (1) { NSLog(@"Normal: %p %@",normal_man, [normal_man name]); NSLog(@"Superman: %p %@",superman, [(__bridge Person *)superman name]); sleep(2); }
那麼為了模擬實際情況(即init方法被Hook,拿到了normal_man的地址),我們直接在NSLog裡輸出。
使用Cycript攻擊的實際效果如圖3、圖4:
通過Hook init方法,拿到了normal_man的地址0x7fbffbe06b00。
在Cycript中使用choose,只能看見兩個字符串。現在直接調用[#0x7fbffbe06b00 setName:@"Cracker"];更改name屬性。
可以看到normal_man的name的確被更改了。而我們memcpy的superman表示無壓力。
那麼superman的地址也被找到了的話,怎麼辦呢?如圖5
P.S 事實上,它也的確被找到了,cycript會檢索所有malloc的內存,圖4、圖5裡,choose執行後的兩句NSString就是證明,只不過因為我們重載了description方法,才沒有直接看到地址。
自己的內存塊
那麼我們把這個normal_man復制到自己的一個內存區塊如何呢?正好借用之前寫的MemoryRegion。試試看吧!
代碼片段:(其余部分與上面的相同)
ssize_t object_size = sizeof(Person *) + sizeof(NSString *) + sizeof(int); MemoryRegion mmgr = MemoryRegion(1024); void * superman = mmgr.malloc(object_size); memcpy(superman, (__bridge void *)normal_man, object_size);
實際效果(圖6):
可以看到,現在choose找不到處於MemoryRegion中的superman。
不過就算找不到,Cracker還可以Hook這個類的setter和getter呀!我們又要如何應對呢?
虛偽的setter/getter
讓我們把setter和getter改成這個樣子:
- (void)setName:(NSString *)name { _name = @"Naive"; } - (void)setAge:(int)age { _age = INT32_MAX; } - (NSString *)name { return @"233"; } - (int)age { return INT32_MIN; }
這樣Cracker們通過setter方法就改不了了,也不能通過getter來獲取,只能HookIvar了。當然我們也是,那麼我們自己要怎麼修改呢?添加兩個C函數吧!
__attribute__((always_inline)) void setName(void * obj, NSString * newName) { void * ptr = (void *)((long)(long *)(obj) + sizeof(Person *)); memcpy(ptr, (void*) &newName, sizeof(char) * newName.length); } __attribute__((always_inline)) void setAge(void * obj, int newAge) { void * ptr = (void *)((long)(long *)obj + sizeof(Person *) + sizeof(NSString *)); memcpy(ptr, &newAge, sizeof(int)); }
在修改的時候使用:
setName(superman, @"Superman"); setAge (superman, 20);
在獲取的時候:
NSLog(@"This person is named %@, aged %d", *((CFStringRef *)(void*)((long)(long *)(superman) + sizeof(Person *))), *((int *)((long)(long *)superman + sizeof(Person *) + sizeof(NSString *))));
加密內存區塊
在我們把Person類改成上面那個樣子之後,已經能阻止大部分只用cycript就想調戲我們的App的人了。
然而,如果Cracker們搜索內存的話,還是有可能找到一些數據的,比如這裡superman的年齡,
superman的內存地址是0x102800f00,_age在(0x102800f00 + sizeof(Person *) + sizeof(NSString *)),也就是0x102800f10,如圖7。
那麼我們不用的時候加密這塊內存,用的時候再解密,演示用的加密、解密函數如下,
__attribute__((always_inline)) void encryptSuperman(void ** data_ptr, ssize_t length) { char * data = (char *) * data_ptr; for (ssize_t i = 0; i < length; i++) { data[i] ^= 0xBBC - i; } } __attribute__((always_inline)) void decryptSuperman(void ** data_ptr, ssize_t length) { char * data = (char *) * data_ptr; for (ssize_t i = 0; i < length; i++) { data[i] ^= 0xBBC - i; } }
使用代碼:
Person * normal_man = [[Person alloc] initWithName:@"Nobody" age:0]; ssize_t object_size = sizeof(Person *) + sizeof(NSString *) + sizeof(int); MemoryRegion mmgr = MemoryRegion(1024); void * superman = mmgr.malloc(object_size); memcpy(superman, (__bridge void *)normal_man, object_size); setName(superman, @"Superman"); setAge (superman, 20); encryptSuperman(&superman, object_size); /** * @brief 為了演示方便加的while */ while (1) { NSLog(@"Normal: %p %@",normal_man,[normal_man name]); NSLog(@"Superman: %p",superman); decryptSuperman(&superman, object_size); NSLog(@"This person is named %@, aged %d",*((CFStringRef *)(void*)((long)(long *)(superman) + sizeof(Person *))), *((int *)((long)(long *)superman + sizeof(Person *) + sizeof(NSString *)))); encryptSuperman(&superman, object_size); sleep(5); }
現在再來看看內存裡的數據(圖8):
嗯,似乎是沒問題了呢~
完整示例代碼,https://github.com/BlueCocoa/HookMeIfYouCan