App Store内购机制

作者: 图灵教育 | 来源:发表于2017-08-01 09:19 被阅读629次

    如今,不同系统平台都有专属的商店应用,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;

    }

    2. 处理恢复交易

    恢复交易有两个方法,一个是交易成功后的处理,另一个是失败后的处理。它们分别是-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;

    }

    3. 处理下载动作

    处理下载动作只有一个方法-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软件安全与逆向分析》。

    相关文章

      网友评论

        本文标题:App Store内购机制

        本文链接:https://www.haomeiwen.com/subject/zindlxtx.html