Pull to refresh

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

Reading time 5 min
Views 11K
Началась эта история с новости о том, что летом в Минске открывается салон 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-у так делать точно можно.
Tags:
Hubs:
+27
Comments 12
Comments Comments 12

Articles