8 June 2012

Как НАДЕЖНО защитить in-App Purchase от ломалок

Development for iOS
Совсем недавно я писал статью Как защитить in-App Purchase от ломалок . Прошло немного времени, а хакеры на месте не сидят. Тот метод защиты оказывается можно обойти, не очень сложно. Под катом метод, который намного надежнее.

Перед чтением этого топика, рекомендую прочитать предыдущий Как защитить in-App Purchase от ломалок , так как это продолжение темы.

В моем приложении после проверки receipt отправляется на мой сервер, и я там его анализирую, и сохраняю в логах. И я заметил, что JSON, который приходит есть стандартный, и какой-то модифицированный.
Стандартный выглядит так:
{
    "receipt": {
        "original_purchase_date_pst": "2012-06-08 04:13:04 America/Los_Angeles",
        "purchase_date_ms": "1339153984956",
        "original_transaction_id": "430000009214053",
        "original_purchase_date_ms": "1339153984956",
        "app_item_id": "12312312323",
        "transaction_id": "430000009214053",
        "quantity": "1",
        "bvrs": "1.0",
        "version_external_identifier": "7809437",
        "bid": "xx.yyyyyy.zzzzzzz",
        "product_id": "xx.yyyyyy.zzzzzz.uuuuuu",
        "purchase_date": "2012-06-08 11:13:04 Etc/GMT",
        "purchase_date_pst": "2012-06-08 04:13:04 America/Los_Angeles",
        "original_purchase_date": "2012-06-08 11:13:04 Etc/GMT",
        "item_id": "123123123"
    },
    "status": 0
}


В результате я заметил 3 типа попыток взлома:

1. Самый частый вариант. Ломалка не подделывает ответ сервера Apple, в результате receipt выглядит так:
{
    "status": 21002,
    "exception": "java.lang.ClassCastException"
}

Наш предыдущий метод с таким взломом справлялся, так как статус = 21002

2. Совсем недавно появились новые способы взлома. Я не знаю с помощью каких утилит это делается, но они подделывают ответ серверов Apple и на второй запрос.
JSON выглядит тогда так:
{ 
"status":0
}

Такой взлом отслеживается просто. Можно проверить наличие некоторых переменных, например «product_id», и всё станет на свои места.

3. Но не все так просто. Недавно появился еще один вариант подделаного JSON:
{
    "status": 0,
    "receipt": {
        "product_id": "xx.yyyyyy.zzzzzzzz.uuuuuuu",
        "purchase_date": 1339152660.383128,
        "quantity": 1,
        "transaction_id": "xx.yyyyyy.zzzzzzzz.uuuuuuu"
    }
}

Если опять что-то проверять, тогда надо найти то, чего не сможет получить программа-ломалка из запроса. Просмотрев все данные я понял, что есть достаточно простой способ, который обойти будет очень непросто.
Надо сделать проверку на «item_id» — это id, которое Apple присваевает каждому продукту, в том числе и in-App purchase. Его можно посмотреть в iTunesConnect, нажав на кнопку «Manage In-App Purchases».

Тогда наш код будет выглядеть так:

kFeature1 = "xx.yyyyyy.zzzzzzzz.uuuuuuu";
kFeatureItemID1 = "123123123";
kFeature2 = "xx.yyyyyy.zzzzzzzz.uuuuuuu";
kFeatureItemID2 = "123123123";
kFeature3 = "xx.yyyyyy.zzzzzzzz.uuuuuuu";
kFeatureItemID3 = "123123123";

- (BOOL)verifyReceipt:(NSData*)receiptData {
    NSString *urlsting = @"https://buy.itunes.apple.com/verifyReceipt";
    NSURL *url = [NSURL URLWithString:urlsting];
    NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:url];
    NSString *st =  [receiptData base64EncodedString];
    NSString *json = [NSString stringWithFormat:@"{\"receipt-data\":\"%@\"}", st];
    
    [theRequest setHTTPBody:[json dataUsingEncoding:NSUTF8StringEncoding]];
	[theRequest setHTTPMethod:@"POST"];		
	[theRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];    
	NSString *length = [NSString stringWithFormat:@"%d", [json length]];	
	[theRequest setValue:length forHTTPHeaderField:@"Content-Length"];	
    NSHTTPURLResponse* urlResponse = nil;
	NSError *error = [[NSError alloc] init];  
	NSData *responseData = [NSURLConnection sendSynchronousRequest:theRequest
												 returningResponse:&urlResponse 
															 error:&error];  
	NSString *responseString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
  NSDictionary *dic = [responseString JSONValue];
  NSInteger status = [[dic objectForKey:@"status"] intValue];
  NSDictionary *receiptDic = [dic objectForKey:@"receipt"];
	BOOL retVal = NO;
	if (status == 0 && receiptDic) {
    NSString *itemId = [receiptDic objectForKey:@"item_id"];    
    NSString *productId = [receiptDic objectForKey:@"product_id"];

    if (productId && ([productId isEqualToString:kFeature1]  || 
                      [productId isEqualToString:kFeature2]  ||
                      [productId isEqualToString:kFeature3]   )) {
        if (itemId && ( [itemId isEqualToString:kFeatureItemID1] ||
                        [itemId isEqualToString:kFeatureItemID2] ||
                        [itemId isEqualToString:kFeatureItemID3] )) {
        retVal = YES;
        }
        
    }
  }
  return retVal;
}

В чем плюс этого кода: ломалка не знает значения item_id, оно нигде не передается при запросе. Именно поэтому подделать такой receipt будет непросто. Хотя, наверное, возможно.

И просьба, не говорите, что все можно сломать. Цель этой защиты — чтобы не было массовых взломов стандартными средствами.
Tags:in-app purchase apiiOS
Hubs: Development for iOS
+22
9.2k 154
Comments 57