Совсем недавно я писал статью Как защитить 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 будет непросто. Хотя, наверное, возможно.
И просьба, не говорите, что все можно сломать. Цель этой защиты — чтобы не было массовых взломов стандартными средствами.
Автор: cohe4ko