in iOS

All About In-App Purchases Part 3: Anti-Piracy Measures

One of the big advantages of in-app purchases for some people is that it makes pirating content more difficult. For me it wasn’t much of an issue because a) Most of the people who enjoy Flower Garden aren’t the ones checking out the app warez sites, and b) I know better than to waste my time trying to prevent piracy [1]. Besides, I always roll my eyes whenever I hear that 90% or some other large percentage of the users are using a pirated copy, so the developer lost all that money. News flash: 99% of those users using a cracked version wouldn’t have bought your product anyway, so you can rest a bit easier at night.

Even so, I decided to take a few extra steps that at least will make things a bit more difficult to crack and keep things more secure. I could have gone way beyond this, but I don’t think that would have been worth my time.

Verify the purchase

fg_pirateRight now, you can download programs that will automatically crack an app by extracting its data and resigning it so it works on any jailbroken phone. There’s no doubt that even if you implement in-app purchases just like I described in earlier posts, crackers will have to analyze the code and do some steps by hand, so that might be enough to slow down the pace of cracked apps. Even so, an experienced cracker won’t take long to crack your in-app purchases unless you take some extra steps. All they have to do is find the function that you call in response to getting the go ahead from the App Store and they’ll gain access to all content.

The first thing you should do is to verify that the purchase happened correctly. To do that, once you get the notification in your app that the payment was complete, you send the receipt to your server, and have your server verify it in turn with the App Store. Since crackers don’t have access to your server (if they do you’re in more trouble than a few cracked apps), that step is secure. If your app was cracked and the user didn’t pay for the purchase, the App Store will fail the verification of the receipt and you don’t have to deliver the content.

How do you check that the receipt is valid? First of all, you need to send the receipt of the purchase to your server. I send it along with some other info, like the user’s UDID, what product was purchased, etc so I can keep general usage statistics. The relevant bits of code are the following:

NSString* receiptString = [self createEncodedString:transaction.transactionReceipt];
//...
[postBody appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
[postBody appendData:[@"Content-Disposition: form-data; name=\"receipt\"\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
[postBody appendData:[receiptString dataUsingEncoding:NSUTF8StringEncoding]];
//...
[req setHTTPBody:postBody];

The createEncodedString function takes some arbitrary data and creates a base64 encoding. That’s one of the parts that is not particularly well documented in the SDK documentation.

- (NSString*) createEncodedString:(NSData*)data
{
    static char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

    const int size = ((data.length + 2)/3)*4;
    uint8_t output[size];

    const uint8_t* input = (const uint8_t*)[data bytes];
    for (int i = 0; i < data.length; i += 3)
    {
        int value = 0;
        for (int j = i; j < (i + 3); j++)
        {
            value <<= 8;
            if (j < data.length)
                value |= (0xFF & input[j]);
        }

        const int index = (i / 3) * 4;
        output[index + 0] =  table[(value >> 18) & 0x3F];
        output[index + 1] =  table[(value >> 12) & 0x3F];
        output[index + 2] = (i + 1) < data.length ? table[(value >> 6)  & 0x3F] : '=';
        output[index + 3] = (i + 2) < data.length ? table[(value >> 0)  & 0x3F] : '=';
    }    

    return  [[NSString alloc] initWithBytes:output length:size encoding:NSASCIIStringEncoding];
}

That just sends the receipt to my server. Once the server receives that request, it needs to verify the data with the App Store. The exact format of the App Store request is, again, not well documented. It turns out it needs to be sent as anonymous data in a POST request. After some trial and error, this is the php code I’m using in my server to verify the receipt:

function isReceiptValid($purchase)
{
	$data = array ('receipt-data' => $purchase->receipt );
	$encodedData = json_encode($data);

	$url = "https://buy.itunes.apple.com/verifyReceipt";

	$ch = curl_init();
	curl_setopt($ch, CURLOPT_URL, $url);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
	curl_setopt($ch, CURLOPT_POST, true);
	curl_setopt($ch, CURLOPT_POSTFIELDS, $encodedData);
	$encodedResponse = curl_exec($ch);
	curl_close($ch);

	if ($encodedResponse == false)
	{
		logError($purchase, "Payment could not be verified (no response data).");
		return false;
	}

	$response = json_decode($encodedResponse);
	if ($response->{'status'} != 0)
	{
		logError($purchase, "Payment could not be verified (status != 0).");
		return false;
	}

	$purchase->storeReceipt = $response->{'receipt'};
	return true;
}

If things check out, the server returns a valid code, and you can go ahead and make the content available to the user. Otherwise, it means the user is trying to pass up a fake purchase as valid and you can deal with it any way you want.

Get content from the server

Of course, if the content is already embedded in the app itself, the cracker can bypass both the App Store purchase and the receipt verification through your server, and just access the content directly. If you want a better chance of not being cracked, have your server deliver the content. That way, if the receipt doesn’t check out, you never transfer the content.

In the case of Flower Garden, all seed packets are downloaded from the server, but the other purchases (extra pots, extra fertilizer) are just changing a few internal variables, so they would be a lot easier to crack.

One of the easiest ways that crackers can get around this verification is by buying the content once and saving the receipt that comes with it. Then, cracked versions can pass that receipt to your server, they’ll be validated as correctly, and they’ll be able to download the content. So, as an added precaution, you might want to keep track how many times a certain purchase is re-downloaded, especially from different UDIDs. I can’t imagine a legitimate reason for a user to download the same purchase more than 20-30 times from different devices, so denying the purchase at that point in the server seems justified.

Save those receipts

The last step I to make cracking more difficult was to save the receipt itself to disk along with the content that was downloaded. Then, periodically, the game can pick one of those receipts, and verify with the server that it’s still valid. If it’s ever reported as an invalid receipt, the game deletes the content for that purchase. This is not as draconian as it sounds, because even if it’s ever triggered for a legitimate user (and I have no idea how that could be other than data corruption), they can always re-purchase it again for free (and they would want to do that to fix their data corruption anyway).

To be able to delete things easily and cleanly, I keep each downloaded in-app purchase on a separate directory. So it’s very easy to delete the whole directory without having to access files everywhere. The hardest thing is making sure the game deals with the missing data and that nothing is referencing it. I’ll touch on that on a future part of this article.

Keeping purchases in separate directories also makes it easier to reset all purchases. That’s something I didn’t originally include in Flower Garden, but I should have. Sometimes, data can get corrupted or maybe a bug occurs because of some combination of purchases (hopefully not, but you never know!). Allowing the user to reset the purchases and re-download them allows them to fix those problems without having to reinstall the app and lose their progress.

[1] I know from first hand experience that one dedicated teenager with all the time in his hands can spend inordinate amounts of time bypassing the most elaborate anti-piracy schemes. The harder it is, the more of a challenge it becomes, and the more fun it is. To this day, I still remember the thrill of finally bypassing the copy protection of 1943 on the Amstrad CPC after two days of non-stop hacking, which included several loops XORing all the available memory (including the currently executing code), and messages from the developers taunting me!

12 Comments

  1. Thanks for the post, great info, particularly the base 64 encoding. I hadn’t seen that in the docs.

    One comment, your php function seems to return false if the call to the Apple servers fails. Wouldn’t it be better to give the user the benefit of the doubt in that case and approve the purchase? I see you’re storing the receipts so you can check the next time the client reports to the server.

    This would remove any user frustration when either your server or the Apple server fails to make/return from the verification step.

  2. Hi Neil,

    Good point. You’re absolutely right. If the Apple server fails to respond, then it would be better not to automatically flag it as an invalid purchase. I probably haven’t considered that case because the Apple server has been up 100% of the time, but I think I’ll change it.
    Thanks.

  3. If you’re going to assume that an Apple failure means the end user’s receipt is legit, you should make sure that the end user has no way to manipulate the receipt sent to the server. Otherwise, all they have to do is shove a custom built malformed receipt into the right place and your app gives them everything for free.

  4. Kevin,

    They’d only get things for free temporarily (because the app can periodically check and see if the receipt is valid) and only if the Apple servers happen to be down when they’re trying. It’s a pretty small, and temporary, window.

  5. John,

    That’s assuming that it’s impossible to craft a receipt that causes the Apple server to return an error code. That’s rarely a correct assumption.

  6. Hi,
    First, thank you Noel for the blog and the info – you are really a developers developer 🙂

    @Kevin && @John: All a cracker need do is find out where in the code Noel get his server answer and turn a false into true. No need to falsify receipts or anything.

  7. Shul, Thanks! Glad you like the blog.

    What you suggest is only true for items that are already included in the code (fertilizer, extra garden). But the ones that need to be downloaded from the server would require a bit more work: download it, add it to the binary, and turn off the whole purchase path. I guess the consolation is that someone needs to do it by hand instead of just running a script like they did before to “crack” it.

  8. Noel,
    I agree with you. another step you can take to make life harder for the cracker is to download the extensions each time, and delete them before the application closes. this works if your server is up and works 100% of the time and the user has network support.

    But we arrive at the same conclusion again, the minute the cracker has a way to read the OSs memory, the game is lost 🙁

    btw, would be great to hear more about your footnote :0) – especially the taunting part 🙂

  9. Ha! The 1943 protection was really fun to beat. In those days, it was all about trying to get access to the memory after the game was loaded from tape/disk (which was the part that was really copy-protected). In this particular case, after all the usual tricks to confuse and lose the hacker (do a call statement, pop a value from the stack, and return to whatever address was previously on the stack, or set special interrupt handlers and jump to them), they had a particularly nasty piece of work.

    It was a simple loop that would iterate through the *whole* 64K of memory, XORing it against some particular number. The code following the loop seemed total garbage, but once the XOR was applied, it was revealed to be another loop XORing all of memory again with another number. This was repeated many times (maybe 100 times?). So clearly, if you had any other code residing in memory, it would be wiped completely. It was almost like playing CoreWars on a real computer! 🙂

    And every so often, if you looked at the ASCII representation of some parts of memory that you thought would contain something useful, you found messages from the developers egging you on, or telling you how you’d never break it. Pretty fun really. A lot more fun than the game itself, that’s for sure!

  10. Great story 🙂

    I think I heard knuth saying that back in the day he got a xor matrix wiped off a board in one convention, because it was a state secret 🙂

  11. Ahh heck dude. You and me both tried to break into 1943 it seems. I failed dismally, but then I was bit of a novice.

    I was more interested in figuring out how to poke in cheat codes. I had made a little TSR thing I had hooked into memory location zero (You just hit reset and instead of resetting it’d call your tsr) which I could then use to try and find where the live decrement counter was. Usually not hard to work out, but completely bamboozling when the code was a bit more defensive against that sort of thing.

    So they just XOR’d it huh?

    Man, playing around with ARM assembly lately (inlining some time critical operations in a G729 codec) really brought back memories of the days when I had most of the key Z80 opcodes memorised. Good times.

Comments are closed.

Webmentions

  • Tweets that mention Games from Within | All About In-App Purchases Part 3: Anti-Piracy Measures -- Topsy.com August 4, 2013

    […] This post was mentioned on Twitter by Noel Llopis and Gagan Biyani, GameDev.net. GameDev.net said: RT @SnappyTouch: New blog post: All About In-App Purchases Part 3: Anti-Piracy Measures http://bit.ly/907UmC […]