Pull to refresh

Про Сталина, Дурова, печеньки и параметр EncryptedPasswd

Reading time 5 min
Views 30K
В ходе неудачной попытки хабрасуицида в комментах к статье про психологическую помощь травмированному СМС-ками ребенку, я поведал о своем сне в котором Сталин кормил связанного Дурова шоколадными печеньками, а из трубки у Сталина вместо дыма выходил публичный ключ Google Play в base64. Этот сон мне приснился после длинного и нудного реверсинга мобильного протокола Google Play. Там же, в комментах, мне предложили написать об этом отдельную статью. Ну вот собственно эта статья и есть. В ней я предлагаю поговорить о моем сне, а также о параметре EncryptedPasswd в POST запросе к android.clients.google.com/auth.

Для начала я напомню что это за POST запрос и расскажу почему меня так заинтересовало поле EncryptedPasswd. Пару лет назад с помощью перехвата траффика от Google Apps было установлено что логин Android устройства на Google Play осуществляется с помощью запроса такого вида:

POST /auth HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 404
Host: android.clients.google.com
Connection: Keep-Alive
User-Agent: GoogleLoginService/1.3 (a10 JZO54K)

accountType=HOSTED_OR_GOOGLE&Email=testemail%40gmail.com&has_permission=1&add_account=1&EncryptedPasswd=AFcb4KS9WAU3NI_-jdMDSueqT-oO1-WN2B9pxB-te_Elx3MElC9B2TgAaWqkY7kiQSnGFEwaY1KVkizxadGsRnFnHa7vlRCrB4Me1XnHBuOz9oi48eBcm0rC7r8QaC_GPp1YPI8OFa0fZU_dTJypganc2tREsgE-_TJQSKWkA7zSWnsq8g%3D%3D&service=ac2dm&source=android&androidId=378b05ab23e0e8e9&device_country=ua&operatorCountry=ua&lang=en&sdk_version=16

Этот запрос еще использовался в одной остроумной атаке на двухфакторную аутентификацию Google где-то год назад. В принципе все параметры этого запроса более или менее очевидны, кроме параметра EncryptedPasswd. Усиленное гугление дало лишь что EncryptedPasswd — это возможно пароль google-аккаунта зашифрованный публичным ключом Google. И все, больше никаких технических подробностей. Такой расплывчатый ответ меня конечно не устроил и я полез внутрь com.google.android.gsf.login.

Внутри я обнаружил класс com.google.android.gsf.loginservice.PasswordEncrypter — он-то и отвечает за шифрование пароля. Reverse engineering этого класса показал, что шифрование… впрочем не буду утомлять уважаемого читателя тоннами Dalvik байткода, лучше сразу приведу работающий говнокод на Java (кодер из меня так себе):

// Публичный ключ Google
private static final String googleDefaultPublicKey = "AAAAgMom/1a/v0lblO2Ubrt60J2gcuXSljGFQXgcyZWveWLEwo6prwgi3iJIZdodyhKZQrNWp5nKJ3srRXcUW+F1BD3baEVGcmEgqaLZUNBjm057pKRI16kB0YppeGx5qIQ5QjKzsR8ETQbKLNWgRY0QRNVz34kMJR3P/LgHax/6rmf5AAAAAwEAAQ==";

// На входе:
//      login - почта вида myemail@gmail.com
//      password - пароль
// На выходе:
//      base64 строка с зашифрованным паролем

// Важно! Я несколько упростил оригинальный алгоритм, поэтому
// данный метод корректно работает только если длина login+password
// не превышает 80 байт (это нигде не проверяется в процедуре - я
// предупреждал что кодер из меня так себе)

@SuppressWarnings("static-access")
public static String encrypt(String login, String password)
        throws NoSuchAlgorithmException, InvalidKeySpecException,
        NoSuchPaddingException, UnsupportedEncodingException,
        InvalidKeyException, IllegalBlockSizeException, 
        BadPaddingException {

    // Дла начала конвертируем Google login public ключ из base64
    // в PublicKey плюс вычисляем SHA-1 от этого самого ключа:

    // 1. Конвертируем Google login public ключ из base64 в byte[]
    byte[] binaryKey = Base64.decode(googleDefaultPublicKey, 0);

    // 2. Вычисляем первый BigInteger
    int i = readInt(binaryKey, 0);
    byte [] half = new byte[i];
    System.arraycopy(binaryKey, 4, half, 0, i);
    BigInteger firstKeyInteger = new BigInteger(1, half);

    // 3. Вычисляем второй BigInteger
    int j = readInt(binaryKey, i + 4);
    half = new byte[j];
    System.arraycopy(binaryKey, i + 8, half, 0, j);
    BigInteger secondKeyInteger = new BigInteger(1, half);

    // 4. Вычисляем SHA-1 от публичного ключа и помещаем в массив signature:
    // signature[0] = 0 (всегда равен нулю!)
    // signature[1...4] = первые 4 байта SHA-1 хэша от публичного ключа
    byte[] sha1 = MessageDigest.getInstance("SHA-1").digest(binaryKey);
    byte[] signature = new byte[5];
    signature[0] = 0;
    System.arraycopy(sha1, 0, signature, 1, 4);

    // 5. Используем ранее вычисленные BigInteger'ы для генерации
    // публичного ключа
    PublicKey publicKey = KeyFactory.getInstance("RSA").
        generatePublic(new RSAPublicKeySpec(firstKeyInteger, secondKeyInteger));

    // Теперь самое время шифровать:

    // 1. Сначала создадим экземпляр Cipher:
    Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWITHSHA1ANDMGF1PADDING");

    // 2. Конкатенация логина и пароля с разделительным "\u0000" между ними:
    String combined = login + "\u0000" + password;

    // 3. Превращаем то что получилось в байты:
    byte[] plain = combined.getBytes("UTF-8");

    // 4. Шифруем эти байты нашим публичным ключем
    cipher.init(cipher.PUBLIC_KEY, publicKey);
    byte[] encrypted = cipher.doFinal(plain);

    // 5. Заносим результат в массив output длинной 133 байта:
    // output[0] = 0 (всегда 0)
    // output[1...4] = первые 4 байта SHA-1 хэша от публичного ключа
    // output[5...132] = зашифрованные логин + пароль (с разделителем "\u0000")
    byte[] output = new byte [133];
    System.arraycopy(signature, 0, output, 0, signature.length);
    System.arraycopy(encrypted, 0, output, signature.length, encrypted.length);

    // Все! Можно закодировать output в base64 и вернуть в вызывающий метод :)
    return Base64.encodeToString(output, Base64.URL_SAFE + Base64.NO_WRAP);
}

// Вспомогательная процедура, превращающая 4 байта из массива в int
private static int readInt(byte[] arrayOfByte, int start) {
    return 0x0 | (0xFF & arrayOfByte[start]) << 24 | (0xFF & arrayOfByte[(start + 1)]) << 16 | (0xFF & arrayOfByte[(start + 2)]) << 8 | 0xFF & arrayOfByte[(start + 3)];
}

Этот код далеко не идеален, но работает и вполне может служить

  • подробным объяснением как именно шифруется пароль
  • основой для быстрого, красивого и безопасного кода, который уважаемый читатель безусловно напишет случись такая необходимость

Это все что я хотел сказать про параметр EncryptedPasswd. Если есть вопросы или уточнения — прошу в комменты.

Теперь про мой сон подробнее. Мне снился кабинет Сталина как его обычно рисовали на агитационных плакатах времен СССР — какое-то невнятное помещение с большим грубым столом, на котором разложены бумаги и карты. В моем сне у стола стояло большое и явно современное офисное кресло натуральной кожи, к которому толстой пеньковой веревкой был привязан Дуров. На основателе VK не было следов побоев или пыток, но выглядел он все равно несчастным. На столе перед Дуровым, поверх бумаг и карт, стояло белое блюдце с шоколадными печеньками, а чуть сбоку от стола стоял сам Сталин в кителе, курил трубку и улыбался в усы. В правой руке Сталин держал все те же печеньки, которыми беспрерывно кормил Дурова. Запихнув в род Дурову одну печеньку, вождь немедленно брал с блюдца следующую — и процесс “кормления” повторялся сначала. Засыпанный крошками Дуров жевал печеньки, обреченно вращал красными слезящимися глазами и иногда беспомощно мычал. Эта сцена казалась еще более странной и пугающей оттого что из трубки Сталина выходил не дым, а тот самый публичный ключ Google в base64:

AAAAgMom/1a/v0lblO2Ubrt60J2gcuXSljGFQXgcyZWveWLEwo6prwgi3iJIZdodyhKZQrNWp5nKJ3srRXcUW+F1BD3baEVGcmEgqaLZUNBjm057pKRI16kB0YppeGx5qIQ5QjKzsR8ETQbKLNWgRY0QRNVz34kMJR3P/LgHax/6rmf5AAAAAwEAAQ==

Этот сон нанес мне тяжелую психологическую травму, которую я предлагаю обсудить в комментах, раз уж на Хабре неделя мистики и психологии.
Tags:
Hubs:
+61
Comments 40
Comments Comments 40

Articles