Реверс-инжиниринг in-app покупок Apple. (или «там» все тоже ленивые)

    Intro


    Привет, хабр! Ты наверное знаешь о недавних событиях, которые распиарили по интернету как «взлом» системы in-app покупок apple. Так вот, это было не совсем так. Это даже не было взломом. И ключевые выводы, которые я сделал:

    • Закрытость<>Защищенность
    • В Apple тоже очень даже ленивые люди работают


    Так вот, я хочу рассказать как и что делалось, добавить немного сорцов, да и вообще, попытаться направить мысли в правильное русло.

    Технология


    В расцвет облачных и сервисных инфрастуктур, очень многое полагается именно на серверную часть. И зря. Как показала практика, как разработчики клиентов, так и разработчики серверов очень ленятся. Только в случае с последними это выливается в большой скандал.

    Итак, приступим. Для того, чтобы совершить вожделенную in-app покупку, необходимо выполнить от 4 до 6 запросов к серверам Apple, и до скольки угодно запросов к своему серверу, если вы проводите валидацию покупок на своем сервере. Я не буду рассматривать получение списка покупок с серверов apple, а расмотрю непосредственно факт покупки. Общий план действий таков:

    • 1. Получить параметры покупки (appadamid, который хранится у apple) и одновременно диалог о подтверждении покупки
    • 2. (опционально) авторизоваться
    • 3. совершить покупку
    • 4. получить подтверждение совершенной покупки
    • 5. проверить, что покупка «куплена»
    • 6. (опционально) проверить совершенную покупку


    Остановимся на каждом из них:
    1. То, что мы вам отдаем, вам не важно

    Получение параметров покупки происходит GET запросом на p(число)-buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/offerAvailabilityAndInfoDialog, я не разобрался, как именно генерируются числа, но они отражаются в $_COOKIE['Pod'] и, скорее всего, зависят от региона пользователя.
    Передав в GET следующие параметры:

    'restrictionLevel' => '1000', //уровень ограничений?
      'id' => '522704697', // ID приложения
      'versionId' => '7736106', // номер версии приложения
      'guid' => '074b684aa46990f92b60c374611e59a82xxxxxfe', // тот самый GUID/UDID, у некоторых совпадал с UDID, у некоторых нет
      'quantity' => '1', // количество in-app покупок? всегда равняется 1
      'offerName' => 'com.gameloft.TDKR.cashpack1', // название in-app покупки
      'lang' => 'en', // язык?
      'bid' => 'com.gameloft.TDKR', // bundle id приложения
      'bvrs' => '1.0.0', // версия приложения
      'icuLocale' => 'ru_RU' // язык кнопок в PLISTе на подтверждение покупки
    
    


    Мы получаем PLIST в ответ. ответ запакован в gzip, так что надо сначала распаковать:

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 
    
      <plist version="1.0">
        <dict>
           
      
        
        
        
             
      <key>jingleDocType</key><string>inAppSuccess</string>
      <key>jingleAction</key><string>offerAvailabilityAndInfoDialog</string>
      <key>dsid</key><string></string>
      
    
      
      
      
           
           
        
          <key>dialog</key>
          <dict>
        
          
        <key>message</key><string>вы действительно желаете купить?</string>
        <key>explanation</key><string>за такую-то сумму $1.99?</string>
        <key>defaultButton</key><string>Buy</string>
    
        
        <key>okButtonString</key><string>дадада!</string>
        <key>okButtonAction</key><dict>
    
        <key>kind</key><string>Buy</string>
        <key>buyParams</key><string>quantity=1&salableAdamId=525477928&appExtVrsId=7736106&bvrs=1.0.0&offerName=com.gameloft.TDKR.cashpack1&productType=A&appAdamId=522704697&price=1990&bid=com.gameloft.TDKR&pricingParameters=STDQ</string>
        <key>itemName</key><string>com.gameloft.TDKR.cashpack1</string>
        
        
        
    
    
    
    
    
    
    
    
    
    
    
    
    </dict>
    
    
        <key>cancelButtonString</key><string>неа</string>
        
    
        
        
    
        
        
        
        
    </dict>
    
        
        
        
        
        
        
        
        
        
        
        
        
        
      
    
    
        
        </dict>
      </plist>
      
    
    
    
    


    Да, я скопировал именно так, как отдается, с чудовищными разрывами строк и пустыми байтами после окончания PLIST. Такое ощущение, что писали криворукие макаки.
    Тут нас больше всего интересует appAdamId из buyParams, а остальное для всех приложений одинаково (что передали в get, то и получите обратно + A&STDQ). Самое веселое, что почти приложения плюют на appAdamId. Как и написано в заголовке, то, что отдается нам не важно.

    2. Где же gzip? (или как светить паролем от apple id)

    Далее необходимо совершить покупку, выполнив POST запрос на p(число)-buy.itunes.apple.com/WebObjects/MZBuy.woa/wa/inAppBuy и передав в POST-данных незакодированный PLIST от вашего приложения. Да, никакого URLencode или сжатия — просто PLIST:

    <?xml_version' => '"1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
    	<key>appAdamId</key>
    	<string>522704697</string>
    	<key>appDsid</key>
    	<string>1341894157</string>
    	<key>appExtVrsId</key>
    	<string>7736106</string>
    	<key>bid</key>
    	<string>com.gameloft.TDKR</string>
    	<key>bvrs</key>
    	<string>1.0.0</string>
    	<key>guid</key>
    	<string>xxxxxxxxx</string>
    	<key>offerName</key>
    	<string>com.gameloft.TDKR.cashpack1</string>
    	<key>price</key>
    	<string>1990</string>
    	<key>pricingParameters</key>
    	<string>STDQ</string>
    	<key>productType</key>
    	<string>A</string>
    	<key>quantity</key>
    	<string>1</string>
    	<key>salableAdamId</key>
    	<string>525477928</string>
    </dict>
    </plist>


    Если вы давно не пользовались appstore, apple вам ответит PLISTом с требованием авторизации.
    Для авторизации надо отправить GET или POST запрос на p(число)-buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/authenticate вот теперь отправив нормальным POST (urlencode) или GET следующее:

     'appleId' => 'appleid',
      'password' => 'просто так пароль, текстом',
      'rmp' => '0',
      'attempt' => '0',
      'accountKind' => '0',
      'guid' => 'xxxx'
    


    И в ответ вы получите о себе практически все данные, в несжатом виде. Просто PLIST и все:

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 
    
      <plist version="1.0">
        <dict>
           
      
        
        
        
             
        
        
            <key>accountInfo</key>
            <dict>
        <key>appleId</key><string>apleid</string>
        <key>accountKind</key><string>0</string>
        <key>address</key>
        <dict>
          <key>firstName</key><string>имя</string>
          <key>lastName</key><string>фамилия</string>
        </dict>
      </dict>
            <key>passwordToken</key><string>токен пароля (действует 15 минут)</string>
            <key>clearToken</key><string>еще какой-то токен (действует 15 минут)?</string>
            
            <key>is-cloud-enabled</key><string>false</string>
            
            <key>dsPersonId</key><string>ID аккаунта?</string>
    <key>creditDisplay</key><string></string>
    
    <key>creditBalance</key><string>1311811 (просто такие цифры, думаю они значат, что у вас не минусовой баланс)</string>
    <key>freeSongBalance</key><string>1311811 (просто такие цифры, думаю они значат, что у вас не минусовой баланс)</string>
    
    
            
            
            
        
        
        <key>status</key><integer>0</integer>
        
        
           
           
        
        
        
        
        
        
        
        
        
        
        
        
        
      
    
    
        
        </dict>
      </plist>
      
    
    
    
    
    


    Опять корявый PLIST…

    Генерируй@подписывай

    Ну что ж, раз вы авторизовались, получив такой PLIST, стоит повторить запрос на p(число)-buy.itunes.apple.com/WebObjects/MZBuy.woa/wa/inAppBuy и в итоге получим PLIST, сообщающий, что да, вот данные вашей покупки:

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 
    
      <plist version="1.0">
        <dict>
           
      
        
        
        
             
      <key>jingleDocType</key><string>inAppSuccess</string>
      <key>jingleAction</key><string>inAppBuy</string>
      <key>dsid</key><string></string>
      
    
      
      
        <key>download-queue-item-count</key><integer>1</integer>
      
      
        <key>app-list</key>
        <array>
          
          <dict>
            <key>item-id</key><integer>525477928</integer>
            <key>app-item-id</key><integer>522704697</integer>
            
            <key>version-external-identifier</key><integer>7736106</integer>
            <key>bid</key><string>com.gameloft.TDKR</string>
            <key>bvrs</key><string>1.0.0</string>
            <key>offer-name</key><string>com.gameloft.TDKR.cashpack1</string>
            <key>transaction-id</key><string>170000030394952</string>
            <key>original-transaction-id</key><string>170000030394952</string>
            <key>purchase-date</key><date>2012-07-28T14:30:19Z</date>
            <key>original-purchase-date</key><date>2012-07-28T14:30:19Z</date>
            <key>quantity</key><integer>1</integer>
            <key>receipt-data</key><data>base64 рецепта</data>
          </dict>
          
        </array>
        
        
      
           
           
        
        
        
        
        
        
        
        
        
        
        
        
        
      
    
    
        
        </dict>
      </plist>
      
    
    
    
    


    Тут думаю, судя по вышенаписанному, все понятно. Интересен base64, он состоит из закодированного NSDictionary:

    {
    	"signature" = "AmJ2SQJx5yZI+t1XRiPBmRVxuoj8jatJkQ+VHCiMLA3Vek48A45NR02AJRNJkKG9+Ry3YgPBjZxifwnYZv1Ylm18NFblnmgDkValnktoL+5wFHcZZGN6/svhpkFUXHWcYi27dUhWP8DGSAtN4s3DquuU2GvYTZMItFlwMpRK2g6BAAADVzCCA1MwggI7oAMCAQICCGUUkU3ZWAS1MA0GCSqGSIb3DQEBBQUAMH8xCzAJBgNVBAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEzMDEGA1UEAwwqQXBwbGUgaVR1bmVzIFN0b3JlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA5MDYxNTIyMDU1NloXDTE0MDYxNDIyMDU1NlowZDEjMCEGA1UEAwwaUHVyY2hhc2VSZWNlaXB0Q2VydGlmaWNhdGUxGzAZBgNVBAsMEkFwcGxlIGlUdW5lcyBTdG9yZTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMrRjF2ct4IrSdiTChaI0g8pwv/cmHs8p/RwV/rt/91XKVhNl4XIBimKjQQNfgHsDs6yju++DrKJE7uKsphMddKYfFE5rGXsAdBEjBwRIxexTevx3HLEFGAt1moKx509dhxtiIdDgJv2YaVs49B0uJvNdy6SMqNNLHsDLzDS9oZHAgMBAAGjcjBwMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUNh3o4p2C0gEYtTJrDtdDC5FYQzowDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBSpg4PyGUjFPhJXCBTMzaN+mV8k9TAQBgoqhkiG92NkBgUBBAIFADANBgkqhkiG9w0BAQUFAAOCAQEAEaSbPjtmN4C/IB3QEpK32RxacCDXdVXAeVReS5FaZxc+t88pQP93BiAxvdW/3eTSMGY5FbeAYL3etqP5gm8wrFojX0ikyVRStQ+/AQ0KEjtqB07kLs9QUe8czR8UGfdM1EumV/UgvDd4NwNYxLQMg4WTQfgkQQVy8GXZwVHgbE/UC6Y7053pGXBk51NPM3woxhd3gSRLvXj+loHsStcTEqe9pBDpmG5+sk4tw+GK3GMeEN5/+e1QT9np/Kl1nj+aBw7C0xsy0bFnaAd1cSS6xdory/CUvM6gtKsmnOOdqTesbp0bs8sn6Wqs0C9dgcxRHuOMZ2tm8npLUm7argOSzQ==";
    	"purchase-info" = "base64 данных о покупке";
    	"pod" = "число";
    	"signing-status" = "0";
    }
    


    Интересует нас подпись. Вот ее layout:

    RECEIPTVERSION | SIGNATURE | CERTIFICATE SIZE | CERTIFICATE
        1 byte               128                      4 bytes                  …

    То есть, нормально, сертификат+подпись в одном флаконе. окей, суем свой сертификат (только помните, длина ключа 1024!), подписываем им же, и вуаля, вот вам валидный рецепт. Кстати, подписывается receipt_version+base64(purchase_info). И, кстати, в отличии от рецепта Mac App Store, тут только один сертификат. А в рецепте MAS там аж цепочка:

    image

    purchase_info состоит из NSDictionary:
    {
    	"original-purchase-date-pst" = "2012-07-28 07:30:19 America/Los_Angeles";
    	"purchase-date-ms" = "1343485819442";
    	"unique-identifier" = "xxxx";
    	"original-transaction-id" = "170000030394952";
    	"bvrs" = "1.0.0";
    	"app-item-id" = "522704697";
    	"transaction-id" = "170000030394952";
    	"quantity" = "1";
    	"original-purchase-date-ms" = "1343485819442";
    	"item-id" = "525477928";
    	"version-external-identifier" = "7736106";
    	"product-id" = "com.gameloft.TDKR.cashpack1";
    	"purchase-date" = "2012-07-28 14:30:19 Etc/GMT";
    	"original-purchase-date" = "2012-07-28 14:30:19 Etc/GMT";
    	"bid" = "com.gameloft.TDKR";
    	"purchase-date-pst" = "2012-07-28 07:30:19 America/Los_Angeles";
    }
    


    и дублирует то, что отдано в PLIST.

    OH, RLY?

    Вдруг случилось так, что вам выдали транзакцию, а она не прошла? Бывает, скажете вы, по этому надо сделать еще 1 GET запрос на p(число)-buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/inAppTransactionDone
    и передать туда ID транзации и GUID:

    'transactionId' => '170000030394952',
      'guid' => 'xxxxx',
    


    Вернется следующее:

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 
    
      <plist version="1.0">
        <dict>
           
      
        
        
        
             
      <key>jingleDocType</key><string>inAppSuccess</string>
      <key>jingleAction</key><string>inAppTransactionDone</string>
      <key>dsid</key><string>DSID (ID девайса/аккаунта?)</string>
      
    
      
      
      
           
           
        
        
        
        
        
        
        
        
        
        
        
        
        
      
    
    
        
        </dict>
      </plist>
      
    
    
    
    


    Все! Покупка совершена, все счастливы.
    Если же вам необходимо проверить покупку со своего сервера или с приложения, юзайте ман от apple, но он обходится так же, как и все выше.

    PS: Watch your head!


    Важно следить за заголовками и cookies, если вы вдруг отдадите что-то, а не Apple Web Objects, не будет $_COOKIE['Pod'], то приложение ругнется и не пустит вас дальше.

    Ну и самая мякотка, код!


    Забрать можно с GitHub. Написано криво, но, работает. + мануал по развертке там же.

    Ну и варианты защиты

    • проверять все поля рецепта, вшить appadamid в приложение и проверять его тоже
    • проверять на своих серверах с использованием сертификатов и ключей (обычная проверка не поможет)
    • использовать VerificationController из кода apple


    В настоящее время сервис не будет работать именно из-за того, что в коде от apple жестко задана цепочка доверия между сертификатами, => фейковые подписи работать не будут, но если вы подпись не проверяете, то все будет работать.

    И про андройд


    Маркет обрывает соединение, т.к. сертификаты невалидные. Кто хочет мне помочь, свяжитесь со мной, у меня есть пару идей.
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 28

      0
      Молодец, что еще сказать :)
        +3
        Меня уже давно терзают сомнения по поводу комманды фрилансеров которая писала itunesconnect.apple.com. Начиная с первой версии сайта он такой тормозной и убогий, что плакать хочется.

        Надеюсь после публичной порки в Apple зашевелятся.
          +4
          p.s. форматирование в посте — ад. :)
            +1
            Да хорошо что хоть какой-то и хоть как-то работает :) Раньше, в 2009-2010, было гораздо хуже.
              +1
              Это еще что, недавно зачем то добавили выпадающее меню справа сверху где был Logout, так вот выйти у меня оттуда не получается не в одном браузере. Нужно после нажатия Logout либо ждать несколько минут после входа, либо использовать N браузеров, чтобы войти в другой аккаунт. Потому что на выход там повесили что-то хитрое на JS, и там даже не прямая ссылка. Помогает копирование и вставка ссылки для выхода «по старому» через http запрос (также они в некоторых местах забыли убрать старую кнопку и добавить новое меню, например в Sales reports). Ужас какой-то. Не говоря о том, что нигде нельзя нажать назад или долго задерживаться на странице, тут же получите ошибку MZ Label с вылетом из аккаунта и потерей всех введенных данных.
                +3
                Это проклятие Эппла или же часть джобсовой сделки с дьяволом — всё начинающееся на itunes у них тормозное и убогое, что плакать хочется :)
                  0
                  Айтюнс не изделие Эппла, по крайней мере, не с самого начала.

                  en.wikipedia.org/wiki/ITunes#History

                  Компания была куплена в 2000 году, купленный медиа-плеер причесали и год спустя выпустили как собственную программу.
                  0
                  Начиная с первой версии сайта он такой тормозной и убогий, что плакать хочется.

                  Как и сам айтюнс в целом, один в один ваши слова. И какими руками нужно было писать апстор, что бы железно приколотить айдишник к стране так, что ни подарить софт другому человеку из апстора другой стрнаны нельзя было, ни просто сменить чертову страну, привязанную к apple id.

                  Некоторое время назад заглядывал в код виджетов MacOS, там Шива, ад и хаос, с затхлым запахом дешевого аутсорса.
                    0
                    Привязка AppleID к конкретной стране сделана по требованию правообладателей. Связано это с тем, что условия продажи контента ( фильмы, музыка ) устанавливаются для каждого региона индивидуально.
                  0
                  а каким образом https перехватывается?
                    0
                    MITM атака же
                      +1
                      Что же, App Store в айфончике не проверяет серверный сертификат? Это немного странно.
                        +1
                        атака возможно при доступе к айфончику: сначала ставим наш правильный сертификат на телефон, потом уже лезем к стору
                    –9
                    XML поправьте в статье, он весь в переносах разъехался.
                      +1
                      вообще он разьехался не в посте, а в оригинале.
                      пост лишь цитирует %)
                      +1
                      Ну, пустые строки объясняются просто — там темплейт с комментариями.
                      Комментарии парсер вырезал, но пустые строки остались.
                      (Да, я тоже считаю, что XML/HTML в релизной версии надо посылать «одной строкой», это просто эстетичнее. Но практика показывает, что принцип «работает — не трогай» многим по душе)
                      • UFO just landed and posted this here
                          0
                          Кстати, а версия с GOG тоже страдает данным недугом?
                            0
                            Упс, куда-то я не туда…
                            0
                            Что-то мне кажется, что такие пустые куски обусловлены криво написанным шаблоном для вывода ответа
                              0
                              Маркет обрывает соединение, т.к. сертификаты невалидные. Кто хочет мне помочь, свяжитесь со мной, у меня есть пару идей.


                              Если я правильно понял суть проблемы, то чтобы перехватывать обращения к андройд маркет, надобно добавить trusted root CA в устройство (в файл cacerts.bks), который в дальнейшем, при перехвате, будет подписывать запросы.
                                0
                                Да, наверное. Но я не знаю, как это сделать, я не android и не iOS программист.
                                0
                                «Чудовищные разрывы строк» — это фича JSP
                                  0
                                  Передавать пароль текстом мне кажется допустимо внутри HTTPS.
                                  Base64 там бы ничего не изменило, а дополнительно его шифровать — избыточно.
                                    0
                                    Спасибо за чтиво!

                                    Листинги бы с миллионом пустых строк причесали… неудобняк :-(
                                      0
                                      дык это, показать все что скрыто, это оригиналы
                                      0
                                      Как это вы так ловко с kbsync не столкнулись?
                                        0
                                        Что это?

                                      Only users with full accounts can post comments. Log in, please.