App Store内购机制

如今,不同系统平台都有专属的商店应用,Android平台有Google Play,Windows平台有Windows Store,iOS与macOS平台则有App Store。苹果公司的成功,很大程度上得益于该软件的生态环境App Store。

如何让系统上的软件开发人员真正地受益,是操作系统开发商需要关注的问题。只有系统平台上的软件丰富了,才能吸引更多的用户去使用该操作系统,而只有开发人员在系统上开发的软件能够赚到钱,他们才有动力去为系统开发更多更好的软件。苹果公司的Apple Store就曾经创造了无数软件开发人员的成功神话。而这一切背后,苹果公司首创的软件购买、免费软件内付费,都是它成功的关键所在。

App Store的内购又称为IAP(In-app Purchase),它是所有苹果商店内应用内付费软件使用的基础设施。对于软件开发人员,了解其使用方法与运行机制,对开发高质量的商业软件是很有帮助的。苹果公司没有给出IAP的具体技术细节,但在WWDC大会与SDK的开发文档中详细讲解了如何在软件中集成它。IAP技术基于苹果SDK中的Store Kit,它是系统中的一个框架StoreKit.framework。开发人员通过使用StoreKit提供的API来完成IAP的集成工作。整个框架的工作方式如图1所示。
这里写图片描述

苹果的应用内付费支持使用多种类型的程序。

  • 为基本功能的软件提供付费后的功能更强大的专业版。
  • 杂志类App购买成功后,支持订阅与下载。
  • 免费游戏提供付费后等级解锁。
  • 在线游戏通过付费购买道具或虚拟财产。

测试应用内付费软件的最简单方法就是下载应用内付费的应用,然后观察它们与其他应用之间的区别。由于集成了应用内付费功能的App,只能通过App Store来发布,因此在测试时,需要先从App Store中下载App。可以发现,通过App Store下载的程序与网络发布的程序最直观的不同是:在App Store中下载的程序,在app的Contents/_MASReceipt目录下会有一个receipt文件。其实,这是一个“凭证文件”,软件通过App Store发布成功后,苹果公司会为它维护一份凭证(Receipt),凭证信息以文件形式进行存储,该文件记录了以下信息。

  • Purchase Information。存放的软件的购买信息。包括软件的Bundle标识符、版本号、唯一标识以及这些属性值的SHA1哈希值。除此之外,它还包含软件的信用记录(Trusted record)与购买记录(Purchase Record)。这些数据使用ASN.1进行编码存放。所有这些信息被称为Receipt Payload。
  • Certificates。存放的Apple Root CA。用于验证Receipt的签名信息。
  • Signature。签名信息。验证签名,可以检测当前的Receipt是否有效,或者是否已经更新了。苹果在开发文档中指出,开发人员应该在程序启动时,检测Receipt是否有效。如果无效,程序应该调用exit(173)退出,系统收到173退出码后,会自动联网请求去刷新Receipt。相应的Objective-C代码如下:
-(void)applicationWillFinishLaunching:(NSNotification *)notification {
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];

if(![[NSFileManager defaultManager] fileExistsAtPath:[receiptURL path]])
{
exit(173);
}


}

接下来看看如何在程序中集成StoreKit。图9-23所示是应用内付费的步骤,整个应用内付费的开发都围绕它展开。

使用iTunes Connect创建并配置好产品信息后,就可以使用StoreKit提供的API与App Store进行交互了。从图中可以看出,整个交互过程一共发送了两次请求:一次是Makes Products Request,也就是构建产品请求。调用Store Kit向App Store发送产品请求,方法是构建一个SKProductsRequest对象的实例,该对象的作用是接收来自App Store返回的本地化产品信息列表。这些本地化的信息中包含了产品的本地化描述以及价格信息,用来展示给用户。SKProductsRequest构建完成后,再为它设置一个代理delegate,用来处理返回的信息。最后调用它的start()方法。从App Store中下载一个App,查看相应的伪代码,如下所示:

void -[ituShopMAS requestProductData](void * self, void * _cmd) {
r14 = [SKProductsRequest alloc];
rdx = self->_product;
rdx = [NSSet setWithObjects:rdx];
r14 = [r14 initWithProductIdentifiers:rdx];
[r14 setDelegate:self]; //设置代理
rdi = r14;
[rdi start]; //调用start()
return;
}

这里写图片描述
图9-23 应用内付费的步骤

SKProductsRequest的setDelegate()设置了数据返回处理的代理。它是一个SKProducts RequestDelegate协议,用来处理服务器返回的SKProductsResponse对象。该协议有一个接口方法-productsRequest:didReceiveResponse:,用来处理返回的SKProductsResponse。调用它的products属性会返回一个SKProduct列表,解析列表中的产品信息,然后展示给用户。App Store中一个App的相应伪代码如下:

void -[ituShopMAS productsRequest:didReceiveResponse:](void * self, void * _cmd, void * arg2, void *
arg3) {
r15 = self;
rax = [arg3 products];
var_48 = rax;
if ([rax count] != 0x0) {
r12 = @selector(productIdentifier);
var_38 = @selector(isEqualToString:);
var_30 = @selector(setHidden:);
var_78 = @selector(localizedDescription);
var_40 = @selector(setStringValue:);
var_80 = @selector(localizedTitle);
var_68 = @selector(stringWithFormat:);
var_88 = @selector(alloc);
var_90 = @selector(init);
var_98 = @selector(setFormatterBehavior:);
var_A0 = @selector(setNumberStyle:);
var_A8 = @selector(priceLocale);
var_B0 = @selector(setLocale:);
var_B8 = @selector(price);
var_C0 = @selector(stringFromNumber:);
var_C8 = @selector(release);
var_D0 = @selector(setEnabled:);
var_E8 = @selector(shopNeedsIcon);
rbx = 0x0;
do {
rax = [var_48 objectAtIndex:rbx];
r13 = rax;
rax = _objc_msgSend(rax, r12, rbx);
rcx = *objc_ivar_offset_ituShopMAS__product;
rdx = *(r15 + rcx);
if (_objc_msgSend(rax, var_38, rdx, rcx) != 0x0) {
rdi = r15->_product;
rdx = @"iboostup.premium";
if (_objc_msgSend(rdi, var_38) == 0x0) {
var_50 = rbx;
r12 = r15->lblDescription;
rax = _objc_msgSend(r13, var_78, rdx);
_objc_msgSend(r12, var_40, rax);
_objc_msgSend(r15->lblDescription, var_30, 0x0);
var_70 = r15->lblProductName;
rcx = _objc_msgSend(r13, var_80, 0x0);
rax = _objc_msgSend(@class(NSString), var_68, @"%@ only", rcx);
_objc_msgSend(var_70, var_40, rax);
_objc_msgSend(r15->lblProductName, var_30, 0x0);
rbx = _objc_msgSend(_objc_msgSend(@class(NSNumberFormatter),
var_88, 0x0), var_90, 0x0);
_objc_msgSend(rbx, var_98, 0x410);
_objc_msgSend(rbx, var_A0, 0x2);
rdx = _objc_msgSend(r13, var_A8, 0x2);
_objc_msgSend(rbx, var_B0, rdx);
rdi = r13;
r13 = rdi;
rdx = _objc_msgSend(rdi, var_B8, rdx);
var_70 = _objc_msgSend(rbx, var_C0, rdx);
_objc_msgSend(rbx, var_C8, rdx);
r12 = r15->lblPrice;
rcx = var_70;
rax = _objc_msgSend(@class(NSString), var_68, @"Price: %@", rcx);
_objc_msgSend(r12, var_40, rax);
rdi = r15->lblPrice;
rbx = var_50;
_objc_msgSend(rdi, var_30, 0x0);
_objc_msgSend(r15->btnPurchase, var_D0, 0x1);
_objc_msgSend(r15->lblRestore, var_30, 0x0);
_objc_msgSend(r15->imgIcon, var_30, 0x0);
}
......
}
rax = _objc_msgSend(r13, r12, rdx, rcx);
rdx = @"iboostup.premium";
if (_objc_msgSend(rax, var_38, rdx, rcx) != 0x0) {
var_50 = rbx;
r12 = r15->lblDescriptionPro;
rax = _objc_msgSend(r13, var_78, rdx);
_objc_msgSend(r12, var_40, rax);
_objc_msgSend(r15->lblDescriptionPro, var_30, 0x0);
var_70 = r15->lblProductNamePro;
rcx = _objc_msgSend(r13, var_80, 0x0);
rax = _objc_msgSend(@class(NSString), var_68, @"%@", rcx);
_objc_msgSend(var_70, var_40, rax);
_objc_msgSend(r15->lblProductNamePro, var_30, 0x0);
rbx = _objc_msgSend(_objc_msgSend(@class(NSNumberFormatter), var_88,
0x0), var_90, 0x0);
......
_objc_msgSend(rdi, var_40, rax);
_objc_msgSend(r15->lblPricePro, var_30, 0x0);
_objc_msgSend(r15->btnPurchasePro, var_D0, 0x1);
_objc_msgSend(r15->lblRestorePro, var_30, 0x0);
_objc_msgSend(r15->imgIconPro, var_30, 0x0);
}
_objc_msgSend(r15->boxWait, var_30, 0x1, rcx);
rbx = rbx + 0x1;
} while (rbx < [var_48 count]);
}
return;
}

解析完产品信息,展示给用户。当用户选择好产品点击购买时,就会发出第2次请求:Makes Payment Request,也就是构建付款请求。该请求通过调用SKPaymentQueue的addPayment()方法,添加一个SKPayment对象。例如,某产品点击购买某功能选项的伪代码如下:

void -[ituShopMAS btnPurchaseClicked:](void * self, void * _cmd, void * arg2) {
[self waitUI];
rdi = [SKMutablePayment alloc];
rdi = [rdi init];
r15 = [rdi autorelease];
rdx = self->_product;
[r15 setProductIdentifier:rdx]; //设置产品标识
[r15 setQuantity:0x1];
rdi = [SKPaymentQueue defaultQueue];
rdx = r15;
[rdi addPayment:rdx]; //添加支付请求
return;
}

defaultQueue()方法返回一个单例的SKPaymentQueue实例,它是一个队列结构,由App
Store去处理。操作完成后,产品支付请求就加入到支付队列中了。要想处理支付的状态,例如购买成功、购买失败、购买取消等处理的逻辑,就需要为队列添加一个观察者。当队列中交易的状态被更新,或者当交易从队列中删除的时候,观察者应该能正确及时地处理所有的交易信息,并根据交易的结果为购买成功的用户提供相应的功能。添加观察者的操作要在addPayment()调用前完成,通常是在程序的初始化时完成的,代码如下所示:

void * -[ituShopMAS init](void * self, void * _cmd) {
rbx = self;
rcx = [SKPaymentQueue canMakePayments];
rax = 0x0;
if (rcx != 0x0) {
rbx = [[rbx super] init];
rax = 0x0;
if (rbx != 0x0) {
rbx->_checked = 0x0;
rbx->_failures = 0x0;
if ([NSBundle loadNibNamed:@"ituShopMAS" owner:rbx] != 0x0) {
rdi = rbx->lblCancel;
[rdi setStringValue:@"Cancel"];
[rbx->lblCancel setClickTarget:rbx sel:@selector(lblCancelClicked)];
[rbx->lblRestore setStringValue:@"Restore"];
[rbx->lblRestore setClickTarget:rbx sel:@selector(lblRestoreClicked)];
[rbx->lblRestorePro setStringValue:@"Restore"];
[rbx->lblRestorePro setClickTarget:rbx
sel:@selector(lblRestoreProClicked)];
rax = [SKPaymentQueue defaultQueue]; //获取单例队列实例
[rax addTransactionObserver:rbx]; //添加观察者
}
rax = rbx;
}
}
return rax;
}

添加观察者对象使用addTransactionObserver()方法,它传入的是一个SKPaymentTransaction Observer协议对象,SKPaymentTransactionObserver协议有一系列方法被SKPaymentQueue调用。下面我们分别进行介绍。

  1. 处理交易

处理交易包括-paymentQueue:updatedTransactions:与-paymentQueue:removedTransactions:方法。前者在一个或多个交易状态更新时被调用,在目标程序中,它必须实现;后者则在交易移除时被调用,在目标程序中,它的实现是可选的。

这两个方法传入的参数都是一个SKPaymentTransaction类型的数组,每一个SKPayment- Transaction代表着一个支付交易对象,应用程序要明确地处理每个交易对象的返回结果,根据它的transactionState属性来判断交易是否成功。如果transactionState的值是SKPayment- TransactionStatePurchased,则表示交易成功,此时程序应该向用户提供收费成功后的功能;如果transactionState的值为SKPaymentTransactionStateFailed,则表示交易失败,应用程序应该获取交易失败的错误信息并反馈给用户。交易的状态是一个枚举值,定义如下:

enum {
SKPaymentTransactionStatePurchasing, //正在付款
SKPaymentTransactionStatePurchased, //付款成功
SKPaymentTransactionStateFailed, //付款失败
SKPaymentTransactionStateRestored, //交易已恢复
SKPaymentTransactionStateDeferred, //交易已推迟
};
typedef NSInteger SKPaymentTransactionState;

一个典型的-paymentQueue:updatedTransactions:代码的逻辑如下所示:

void -[ituShopMAS paymentQueue:updatedTransactions:](void * self, void * _cmd, void * arg2, void * arg3) {
var_F8 = arg3;
r13 = self;
var_30 = *___stack_chk_guard;
intrinsic_movaps(var_40, 0x0, arg2, arg3);
intrinsic_movaps(var_50, 0x0);
var_60 = intrinsic_movaps(var_60, 0x0);
var_70 = intrinsic_movaps(var_70, 0x0);
rbx = [arg3 countByEnumeratingWithState:var_70 objects:var_F0 count:0x10];
if (rbx == 0x0) goto loc_10001eb00; //如果交易的数量为0,则直接返回

loc_10001ea53:
r14 = *var_60;
goto loc_10001ea5a;

loc_10001ea5a:
r15 = 0x0;
goto loc_10001ea5d;

loc_10001ea5d:
if (*var_60 != r14) { //枚举所有的交易
objc_enumerationMutation(var_F8);
}
r12 = *(var_68 + r15 * 0x8);
rax = [r12 transactionState]; //判断每个交易的transactionState
if (rax == 0x3) goto loc_10001eaa2; //3表示SKPaymentTransactionStateRestored

loc_10001ea90:
if (rax != 0x2) goto loc_10001eaae; //2表示SKPaymentTransactionStateFailed

loc_10001ea96:
rdi = r13;
rsi = @selector(failedTransaction:); //交易失败
goto loc_10001eabe;

loc_10001eabe:
_objc_msgSend(rdi, rsi); //为不同的状态执行不同的选择器方法
goto loc_10001eac7;

loc_10001eac7:
r15 = r15 + 0x1;
if (r15 < rbx) goto loc_10001ea5d;

loc_10001eacf:
rbx = [var_F8 countByEnumeratingWithState:var_70 objects:var_F0 count:0x10];
if (rbx != 0x0) goto loc_10001ea5a;

loc_10001eb00:
if (*___stack_chk_guard != var_30) {
__stack_chk_fail();
}
return;

loc_10001eaae:
if (rax != 0x1) goto loc_10001eac7; //1表示SKPaymentTransactionStatePurchased

loc_10001eab4:
rdi = r13;
rsi = @selector(completeTransaction:); //交易完成
goto loc_10001eabe;

loc_10001eaa2:
rdi = r13;
rsi = @selector(restoreTransaction:); //交易恢复
goto loc_10001eabe;
}
  1. 处理恢复交易

恢复交易有两个方法,一个是交易成功后的处理,另一个是失败后的处理。它们分别是-paymentQueueRestoreCompletedTransactionsFinished:与-paymentQueue:restoreCompletedTransactionsFailedWithError:。恢复失败的原因通常是网络或本地的Receipt验证失败。一段典型的处理代码如下:

void -[ituShopMAS paymentQueue:restoreCompletedTransactionsFailedWithError:](void * self, void * _cmd, void * arg2, void * arg3) {
rcx = [arg3 description];
NSLog(@"Restore failed: %@", rcx);
[self->boxWait setHidden:0x0];
[self->lblResult setStringValue:@"Restore failed."];
[self->lblProductName setHidden:0x0];
[self->lblDescription setHidden:0x0];
[self->wvWait setHidden:0x1];
[self->lblPrice setHidden:0x0];
[self->btnPurchase setEnabled:0x1];
[self->lblRestore setHidden:0x0];
rcx = self->_product;
[self sendTxEvent:@"restore-fail" product:rcx];
rax = [NSBundle mainBundle];
rdx = @selector(appStoreReceiptURL);
if (([rax respondsToSelector:rdx] != 0x0) && ([[NSFileManager defaultManager]
fileExistsAtPath:[[[NSBundle mainBundle] appStoreReceiptURL] path], rcx] == 0x0)) {
exit(0xad);
}
return;
}
  1. 处理下载动作

处理下载动作只有一个方法-paymentQueue:updatedDownloads:,而且它是可选的,只有提供付费下载与订阅的程序才需要实现它。

了解了API的使用方法,再来分析如何破解它就没那么困难了。首先,应该考虑的是如何做到通用破解,即破解IAP的机制后,可以将同类型的产品一次全部破解!这种想法并不是异想天开,在macOS 10.9系统以前,就曾经出现过这样的破解工具与方法。例如2012年7月,一位名叫Alexy的俄罗斯黑客公布了一个针对macOS系统上的IAP的破解方法。在本地系统中安装两张证书,然后使用一款名为Grim Receiper(死神)的工具,就可以一次性破解App Store中大量支持内购的程序。它的原理是将App Store内购请求的通信地址转向自己搭建的内购验证服务器上,使交易发生变化时transactionState的值永远是SKPaymentTransactionStatePurchased。这类破解行为对苹果公司与软件开发人员的打击是巨大的。之后,苹果公司为了阻止这类行为发生,采取了不少措施,包括联系Alexy网站的ISP关闭Alexy的网站,联系Paypal拒绝为Alexy的公开账号提供转帐服务,修补App Store的程序验证漏洞等。10.9版本后,Grim Receiper变得无效,但Alexy似乎并没有放弃对IAP破解的尝试。其实这位黑客还开发出了针对Android与iOS系统内购的破解工具。之后,Alexy使用比特币来收取世界各地人员的开发捐助,网站的ISP也改为了一个地下服务商。在macOS系统10.11初期,Alexy甚至成功开发出了内购破解工具。不过苹果公司一直没放弃对他的关注,很快就为破解的漏洞打上了补丁。目前,Alexy还在积极地尝试如何破解最新的IAP机制。Grim
Receiper运行效果如图9-24所示。
这里写图片描述

虽然做到通用破解有些难度,但针对个体内购机制的App破解,难度可能就没这么大了。一个典型的破解思路是:修改-paymentQueue:updatedTransactions:方法的代码逻辑,将交易transactionState为SKPaymentTransactionStateFailed时的代码逻辑改成SKPaymentTransaction- StatePurchased时的代码就可以了。而在具体执行破解操作时,可以使用爆破的手段在程序中进行修改,或者使用Hook技术,对方法的返回结果进行Hook。

本文摘自《macOS软件安全与逆向分析》。

猜你喜欢

转载自blog.csdn.net/turingbooks/article/details/76489357