Реверс AdMob SDK или еще один способ защитить свой код

от автора

Началась эта история с новости о том, что летом в Минске открывается салон 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-у так делать точно можно.

ссылка на оригинал статьи http://habrahabr.ru/post/179723/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *