Началась эта история с новости о том, что летом в Минске открывается салон Bentley. Так я понял, что пришло время встраивать рекламу в свою вторую игру, иначе я рискую оказаться в конце очереди. Скачал последнюю версию SDK (6.4.1 на данный момент), интегрировал в игру, запустил и сразу увидел подозрительные строчки в logcat:
05-14 15:06:06.312: D/dalvikvm(1379): DexOpt: --- BEGIN 'ads2133480362.jar' (bootstrap=0) ---
05-14 15:06:06.632: D/dalvikvm(1413): creating instr width table
05-14 15:06:06.671: D/dalvikvm(1413): DexOpt: load 2ms, verify+opt 18ms
05-14 15:06:06.703: D/dalvikvm(1379): DexOpt: --- END 'ads2133480362.jar' (success) ---
05-14 15:06:06.703: D/dalvikvm(1379): DEX prep '/data/data/by.squareroot.kingsquare/cache/ads2133480362.jar': unzip in 0ms, rewrite 391ms
dexopt — это программа для проверки и оптимизации DEX-файлов. Непонятно, с чего бы это ей работать, особенно после запуска приложения и со странным файлом ads2133480362.jar. Так как я к этому файлу никакого отношения не имел и раньше такого не было, все подозрения пали на AdMob. Видимо, AdMob SDK сохраняет какой-то jar-файл в кэш-директорию приложения, подгружает оттуда классы и использует их при загрузке и показе баннеров. Осталось узнать, что же так старательно прячут от нас разработчики AdMob SDK.
Реверсим AdMob SDK
Конечно же классы в SDK обфусцированы, но это не сильно усложняет нам задачу. Для того, чтобы найти какую-то отправную точку, посмотрим в каких классах есть вызов метода Context.getCacheDir(). Их оказалось немного, всего лишь два. В одном из них этот метод используется для установки WebSettings.setAppCachePath(), так что остается только один подозрительный класс с ни о чем уже не говорящим названием ak.class.
Лично я для декомпиляции использую JD. Посмотрим на часть метода в этом классе, где есть вызов Context.getCacheDir():
byte[] arrayOfByte1 = an.a(ao.a());
byte[] arrayOfByte2 = an.a(arrayOfByte1, ao.b());
File localFile2 = File.createTempFile("ads", ".jar", paramContext.getCacheDir());
FileOutputStream localFileOutputStream = new FileOutputStream(localFile2);
localFileOutputStream.write(arrayOfByte2, 0, arrayOfByte2.length);
localFileOutputStream.close();
Если снять проклятие, наложенное злым proguard-ом, и переименовать классы и переменные в более понятные, то получится такой код:
String keyBase64 = Base64Consts.getKeyBase64();
byte[] keyBytes = Decrypter.decodeKey(keyBase64);
String classBase64 = Base64Consts.getClassBase64();
byte[] classBytes = Decrypter.decodeClassBytes(keyBytes, classBase64);
File classFile = File.createTempFile("ads", ".jar", context.getCacheDir());
FileOutputStream out = new FileOutputStream(classFile);
out.write(classBytes, 0, classBytes.length);
out.close();
Теперь можно разобрать по порядку, откуда же берется jar-файл. Класс Base64Consts (бывший ao) содержит строки в Base64 кодировке:
public class Base64Consts {
public static String getKeyBase64() {
return "ARuhFl7nBw/97YxsDjOCIqF0d9D2SpkzcWN42U/KR6Q=";
}
public static String getClassBase64() {
return "SuhNEgGjhJl/XS1FVuhqPkUehkYsZY0198PVH9C0C..."; // эта строка очень длинная, поэтому здесь только ее начало
}
}
Строка keyBase64 превращается в ключ с помощью метода Decrypter.decodeKey():
public static byte[] decodeKey(String keyBase64) {
byte[] keyBytes = Base64Util.decode(keyBase64);
ByteBuffer byteBuffer = ByteBuffer.wrap(keyBytes, 4, 16);
byte[] key128 = new byte[16];
byteBuffer.get(key128);
for (int i = 0; i < key128.length; i++) {
key128[i] = ((byte)(key128[i] ^ 0x44));
}
return key128;
}
Метод декодирует строку в массив байт (AdMob SDK использует свой класс для этих целей, т. к. android.util.Base64 появился только в api level 8) и из получившегося массива длинной в 32 байта берется блок в 16 байт начиная с 5-го. Каждый байт xor-ится волшебным числом 0x44. В результате этих манипуляций получается 128-битный ключ AES.
Строка classBase64 превращается в массив байт, который представляет собой jar-файл, с помощью метода Decrypter.decodeClassBytes():
public static byte[] decodeClassBytes(byte[] keyBytes, String cryptedBytesBase64) {
byte[] cryptedBytes = Base64Util.decode(cryptedBytesBase64);
ByteBuffer buffer = ByteBuffer.allocate(cryptedBytes.length);
buffer.put(cryptedBytes);
buffer.flip();
byte[] initializationVector = new byte[16];
byte[] input = new byte[cryptedBytes.length - 16];
buffer.get(initializationVector);
buffer.get(input);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(initializationVector));
return cipher.doFinal(input);
}
Метод декодирует строку в массив байт, первые 16 байт в этом массиве — вектор инициализации, все остальное — зашифрованные данные. На выходе получается массив байт, который сохраняется в jar-файл и из которого динамически подгружаются классы. Наконец, можно посмотреть, что же там внутри.
Таинственный ad.jar
Для загрузки классов из этого jar-файла AdMob SDK использует DexClassLoader. Используется внутри это так:
DexClassLoader classLoader = new DexClassLoader(classFile, context.getCacheDir()), null, context.getClassLoader());
Class clazz = classLoader.loadClass(b(keyBytes, Base64Consts.getClassNameBase64()));
Method m = clazz.getMethod(b(keyBytes, Base64Consts.getMethodNameBase64()), new Class[0]);
После этого jar-файл удаляется. Имена классов и методов зашифрованы таким же способом, как и сам jar-файл (Base64 + AES), поэтому будет быстрее и проще сразу посмотреть внутрь jar-файла.
Вполне ожидаемо внутри оказался файл classes.dex. Прогнав его через dex2jar получился еще один jar-файл, на этот раз с классами.
Thank you Mario! But our princess is in another castle!
Вот тут меня поджидало разочарование. Внутри оказалось пять обфусцированных классов, которые не представляли собой ничего интересного. Например, такой класс:
public class a {
public static Long a() {
return Long.valueOf(Calendar.getInstance().getTime().getTime() / 1000L);
}
}
И вот такой:
public class d {
public static String a() {
new Build.VERSION();
return Build.VERSION.RELEASE;
}
}
Один из классов берет значение Settings.Secure.ANDROID_ID и считает его md5-хеш. Другой считает SHA-2 хеш всего apk-файла. Видимо, эти параметры используются в запросах, отправляемых на сервер.
В общем, ни секретных алгоритмов, ни скрытых посланий, ничего. Зачем так прятать такой тривиальной код — для меня загадка.
Иголка в яйце, яйцо в утке...
Хоть ничего интересного внутри не оказалось, AdMob использует интересный способ для защиты своего кода. Код компилируется, собирается в jar-файл, jar-файл конвертируется в dex-формат, dex-файл запаковывается снова в jar, jar-файл шифруется AES и наконец кодируется Base64. В принципе, неплохой способ, особенно если получать ключ с сервера.
Хотя может быть, что такой хитрый способ будет попадать под определение Dangerous Products из Google Play Developer Program Policies:
An app downloaded from Google Play may not modify, replace or update its own APK binary code using any method other than Google Play's update mechanism.
В принципе, код меняется — из воздуха образуется библиотека, из которой подгружаются классы. Но AdMob-у так делать точно можно.
Автор: andrei_mankevich