這一篇文章着重于保護重要資料不被攻擊者使用cycript或者runtime修改,概要内容如下:
防止choose(類名)
禁忌,二重存在
自己的記憶體塊
虛僞的setter/getter
加密記憶體資料
<a target="_blank" href="https://blog.0xbbc.com/2015/05/protection-against-cycriptruntime/">english version is here</a>
以下内容均以此假想情況為基礎: 我們有一個person類,它的定義如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<code>@interface person : nsobject {</code>
<code> </code><code>nsstring * _name;</code>
<code> </code><code>int _age;</code>
<code>}</code>
<code>@property (strong, nonatomic, readonly) nsstring * name;</code>
<code>@property (nonatomic, readonly) int age;</code>
<code> </code>
<code>- (instancetype)initwithname:(nsstring *)name age:(int)age;</code>
<code>@end</code>
<code>@implementation person</code>
<code>@synthesize name = _name;</code>
<code>@synthesize age = _age;</code>
<code>- (instancetype)initwithname:(nsstring *)name age:(int)age{</code>
<code> </code><code>self = [self init];</code>
<code> </code><code>if</code> <code>(self) {</code>
<code> </code><code>_name = name;</code>
<code> </code><code>_age = age;</code>
<code> </code><code>}</code>
<code> </code><code>return</code> <code>self;</code>
<code>- (void)setname:(nsstring *)name {</code>
<code> </code><code>if</code> <code>(name != _name) {</code>
<code> </code><code>_name = name ;</code>
<code>- (void)setage:(int)age {</code>
<code> </code><code>_age = age;</code>
<code>- (nsstring *)name {</code>
<code> </code><code>return</code> <code>_name;</code>
<code>- (int)age {</code>
<code> </code><code>return</code> <code>_age;</code>
現在我們需要保護這個類的資料,雖然我們在@property裡聲明了這兩個都是readonly,但是因為objective-c的runtime特性,這個屬性說了基本等于沒說(對于破解者而言)。 那麼我們要怎麼做才能保護呢?
我們知道,在cycript中可以很友善的使用choose(類名)來擷取到app中該類所有的執行個體變量(圖1),那麼我們就先從這裡下手吧!
解決方案: 重載- (nsstring *)description方法。效果如圖2所示。
<code>- (nsstring *)description {</code>
<code> </code><code>return</code> <code>[nsstring stringwithformat:@</code><code>"this person is named %@, aged %d."</code><code>, self.name, self.age];</code>
上面雖然在cycript中用choose函數拿不到了,但是如果一開始就被hook了init方法怎麼辦呢?
解決方案:memcpy一份。
首先确定person類執行個體的大小:(類指針大小+所有成員變量大小)
<code>ssize_t object_size = sizeof(person *) + sizeof(nsstring *) + sizeof(int);</code>
然後就可以愉快的memcpy了:
<code>person * normal_man = [[person alloc] initwithname:@</code><code>"nobody"</code> <code>age:0];</code>
<code>void * superman = malloc(object_size);</code>
<code>memcpy(superman, (__bridge void *)normal_man, object_size);</code>
在用的時候,通過__bridge轉換:
<code>[(__bridge person *)superman setname:@</code><code>"superman"</code><code>];</code>
代碼片段:
<code> </code><code>person * normal_man = [[person alloc] initwithname:@</code><code>"nobody"</code> <code>age:0];</code>
<code> </code><code>ssize_t object_size = sizeof(person *) + sizeof(nsstring *) + sizeof(int);</code>
<code> </code><code>void * superman = malloc(object_size);</code>
<code> </code><code>memcpy(superman, (__bridge void *)normal_man, object_size);</code>
<code> </code><code>[(__bridge person *)superman setname:@</code><code>"superman"</code><code>];</code>
<code> </code><code>[(__bridge person *)superman setage:20];</code>
<code> </code><code>/**</code>
<code> </code><code>* @brief 為了示範友善加的while</code>
<code> </code><code>*/</code>
<code> </code><code>while</code> <code>(1) {</code>
<code> </code><code>nslog(@</code><code>"normal: %p %@"</code><code>,normal_man, [normal_man name]);</code>
<code> </code><code>nslog(@</code><code>"superman: %p %@"</code><code>,superman, [(__bridge person *)superman name]);</code>
<code> </code><code>sleep(2);</code>
那麼為了模拟實際情況(即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。試試看吧!
代碼片段:(其餘部分與上面的相同)
<code> </code><code>memoryregion mmgr = memoryregion(1024);</code>
<code> </code><code>void * superman = mmgr.malloc(object_size); memcpy(superman, (__bridge void *)normal_man, object_size);</code>
實際效果(圖6):
可以看到,現在choose找不到處于memoryregion中的superman。
不過就算找不到,cracker還可以hook這個類的setter和getter呀!我們又要如何應對呢?
讓我們把setter和getter改成這個樣子:
<code> </code><code>_name = @</code><code>"naive"</code><code>;</code>
<code> </code><code>_age = int32_max;</code>
<code> </code><code>return</code> <code>@</code><code>"233"</code><code>;</code>
<code>} </code>
<code> </code><code>return</code> <code>int32_min;</code>
這樣cracker們通過setter方法就改不了了,也不能通過getter來擷取,隻能hookivar了。當然我們也是,那麼我們自己要怎麼修改呢?添加兩個c函數吧!
<code>__attribute__((always_inline)) void setname(void * obj, nsstring * newname) {</code>
<code> </code><code>void * ptr = (void *)((long)(long *)(obj) + sizeof(person *));</code>
<code> </code><code>memcpy(ptr, (void*) &newname, sizeof(char) * newname.length);</code>
<code>__attribute__((always_inline)) void setage(void * obj, int newage) {</code>
<code> </code><code>void * ptr = (void *)((long)(long *)obj + sizeof(person *) + sizeof(nsstring *));</code>
<code> </code><code>memcpy(ptr, &newage, sizeof(int));</code>
在修改的時候使用:
<code>setname(superman, @</code><code>"superman"</code><code>);</code>
<code>setage (superman, 20);</code>
在擷取的時候:
<code>nslog(@</code><code>"this person is named %@, aged %d"</code><code>, *((cfstringref *)(void*)((long)(long *)(superman) + sizeof(person *))), *((int *)((long)(long *)superman + sizeof(person *) + sizeof(nsstring *))));</code>
加密記憶體區塊
在我們把person類改成上面那個樣子之後,已經能阻止大部分隻用cycript就想調戲我們的app的人了。
然而,如果cracker們搜尋記憶體的話,還是有可能找到一些資料的,比如這裡superman的年齡,
superman的記憶體位址是0x102800f00,_age在(0x102800f00 + sizeof(person *) + sizeof(nsstring *)),也就是0x102800f10,如圖7。
那麼我們不用的時候加密這塊記憶體,用的時候再解密,示範用的加密、解密函數如下,
<code>__attribute__((always_inline)) void encryptsuperman(void ** data_ptr, ssize_t length) {</code>
<code> </code><code>char * data = (char *) * data_ptr;</code>
<code> </code><code>for</code> <code>(ssize_t i = 0; i < length; i++) {</code>
<code> </code><code>data[i] ^= 0xbbc - i;</code>
<code>__attribute__((always_inline)) void decryptsuperman(void ** data_ptr, ssize_t length) {</code>
使用代碼:
<code> </code><code>void * superman = mmgr.malloc(object_size);</code>
<code>encryptsuperman(&superman, object_size);</code>
<code> </code><code>nslog(@</code><code>"normal: %p %@"</code><code>,normal_man,[normal_man name]);</code>
<code> </code><code>nslog(@</code><code>"superman: %p"</code><code>,superman);</code>
<code> </code><code>decryptsuperman(&superman, object_size);</code>
<code> </code><code>nslog(@</code><code>"this person is named %@, aged %d"</code><code>,*((cfstringref *)(void*)((long)(long *)(superman) + sizeof(person *))), *((int *)((long)(long *)superman + sizeof(person *) + sizeof(nsstring *))));</code>
<code> </code><code>encryptsuperman(&superman, object_size);</code>
<code> </code><code>sleep(5);</code>
現在再來看看記憶體裡的資料(圖8):
嗯,似乎是沒問題了呢~
http://www.cocoachina.com/ios/20150511/11801.html