В данной статье мы кратко расскажем о том, как можно защитить свою программу от взлома, не интегрируя стандартное решение от Google и предоставим пример рабочего кода. Интересно? Просим под кат!
Дело в том, что данный механизм хорошо известен разработчикам и его взлом не составляет большого труда. Всё что для этого нужно — скачать apktool, найти класс LicenseChecker и слегка подправить метод checkAccess.
Очевидно, что любую защиту можно сломать. Данный метод не является серебрянной пулей, но он имеет право на жизнь. Для проверки уникальности приложения есть смысл проверить сертификат, которым это приложение было подписано. Информацию о сертификате можно прочитать из PackageInfo:
Нулевой элемент массива будет содержать необходимую информацию о ключе, которым было подписано приложение. Сохраните сам ключ, он вам понадобится совсем скоро.
Логично, что делать такую проверку в Java не имеет смысла, так как аналогичный приём с использованием apktool похоронит вашу защиту за несколько минут. Поэтому данную проверку стоит перенести на нейтив уровень.
Необходимо вынести часть своего функционала в ту же библиотеку и перед тем как вернуть значение проверить сертификат на подлинность. И не забываем про фантазию, которая необходима для того, чтобы сбить с толку потенциального хакера. Допустим, вы определили, что данная копия приложения — не подлинная. Нет смысла явно говорить об этом, намного веселее добавить случайный элемент в действия программы. К примеру, если у вас игра, то можно добавить силы соперникам или сделать игрока более уязвимым. Также можно добавить случайные падения, к примеру в 10% случаев (справедливое замечание от хабраюзераzagayevskiy: случайные падения испортят карму вашей программы. ). Тут всё зависит только от вас.
Вот простой пример возможной реализации:
Для начала пишем класс, который делает вызов библиотеки для получения некоторых данных.
Метод init нужен для того, чтобы не вызывать проверку подлинности сертификата каждый раз.
Реализация нейтив методов:
Данный вариант защиты также может быть взломан путём дизасемблирования, но для этого нужен совсем другой уровень знаний и намного больше времени, чем в случае с реализацией защиты на уровне Java. При наличии определённых средств есть смыл приобрести обфускатор для С кода, в этом случае взлом будет далеко не тривиальной задачей.
Если часть логики приложения реализована на сервере, есть смысл вынести проверку подлинности сертификата на сервер. Как вариант, можно использовать такой алгоритм действий:
Надеемся, данный механизм поможет читателям Хабра повысить заработок со своих Android приложений.
В чём слабость Google Application Licensing?
Дело в том, что данный механизм хорошо известен разработчикам и его взлом не составляет большого труда. Всё что для этого нужно — скачать apktool, найти класс LicenseChecker и слегка подправить метод checkAccess.
Как реализовать свою собственную защиту?
Очевидно, что любую защиту можно сломать. Данный метод не является серебрянной пулей, но он имеет право на жизнь. Для проверки уникальности приложения есть смысл проверить сертификат, которым это приложение было подписано. Информацию о сертификате можно прочитать из PackageInfo:
PackageInfo info =getPackageManager().getPackageInfo(getPackageName(), 0);
Signature[] signatures = info.signatures;
Нулевой элемент массива будет содержать необходимую информацию о ключе, которым было подписано приложение. Сохраните сам ключ, он вам понадобится совсем скоро.
Логично, что делать такую проверку в Java не имеет смысла, так как аналогичный приём с использованием apktool похоронит вашу защиту за несколько минут. Поэтому данную проверку стоит перенести на нейтив уровень.
const char* rsa = "PUT_YOUR_RSA_KEY_HERE";
jint verifyCertificate(JNIEnv *env, jobject obj, jobject cnt) {
jclass cls = env->GetObjectClass(cnt);
jmethodID mid = env->GetMethodID(cls, "getPackageManager",
"()Landroid/content/pm/PackageManager;");
jmethodID pnid = env->GetMethodID(cls, "getPackageName",
"()Ljava/lang/String;");
if (mid == 0 || pnid == 0) {
return ERROR;
}
jobject pacMan_o = env->CallObjectMethod(cnt, mid);
jclass pacMan = env->GetObjectClass(pacMan_o);
jstring packName = (jstring) env->CallObjectMethod(cnt, pnid);
/*flags = PackageManager.GET_SIGNATURES*/
int flags = 0x40;
mid = env->GetMethodID(pacMan, "getPackageInfo",
"(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
if (mid == 0) {
return ERROR;
}
jobject pack_inf_o = (jobject) env->CallObjectMethod(pacMan_o, mid,
packName, flags);
jclass packinf = env->GetObjectClass(pack_inf_o);
jfieldID fid;
fid = env->GetFieldID(packinf, "signatures",
"[Landroid/content/pm/Signature;");
jobjectArray signatures = (jobjectArray) env->GetObjectField(pack_inf_o,
fid);
jobject signature0 = env->GetObjectArrayElement(signatures, 0);
mid = env->GetMethodID(env->GetObjectClass(signature0), "toByteArray",
"()[B");
jbyteArray cert = (jbyteArray) env->CallObjectMethod(signature0, mid);
if (cert == 0) {
return ERROR;
}
jclass BAIS = env->FindClass("java/io/ByteArrayInputStream");
if (BAIS == 0) {
return ERROR;
}
mid = env->GetMethodID(BAIS, "<init>", "([B)V");
if (mid == 0) {
return ERROR;
}
jobject input = env->NewObject(BAIS, mid, cert);
jclass CF = env->FindClass("java/security/cert/CertificateFactory");
mid = env->GetStaticMethodID(CF, "getInstance",
"(Ljava/lang/String;)Ljava/security/cert/CertificateFactory;");
jstring X509 = env->NewStringUTF("X509");
jobject cf = env->CallStaticObjectMethod(CF, mid, X509);
if (cf == 0) {
return ERROR;
}
//"java/security/cert/X509Certificate"
mid = env->GetMethodID(CF, "generateCertificate",
"(Ljava/io/InputStream;)Ljava/security/cert/Certificate;");
if (mid == 0) {
return ERROR;
}
jobject c = env->CallObjectMethod(cf, mid, input);
if (c == 0) {
return ERROR;
}
jclass X509Cert = env->FindClass("java/security/cert/X509Certificate");
mid = env->GetMethodID(X509Cert, "getPublicKey",
"()Ljava/security/PublicKey;");
jobject pk = env->CallObjectMethod(c, mid);
if (pk == 0) {
return ERROR;
}
mid = env->GetMethodID(env->GetObjectClass(pk), "toString",
"()Ljava/lang/String;");
if (mid == 0) {
return ERROR;
}
jstring all = (jstring) env->CallObjectMethod(pk, mid);
const char * all_char = env->GetStringUTFChars(all, NULL);
char * out = NULL;
if (all_char != NULL) {
char * startString = strstr(all_char, "modulus:");
char * end = strstr(all_char, "public exponent");
bool isJB = false;
if (startString == NULL) {
//4.1.x
startString = strstr(all_char, "modulus=");
end = strstr(all_char, ",publicExponent");
isJB = true;
}
if (startString != NULL && end != NULL) {
int len;
if (isJB) {
startString += strlen("modulus=");
len = end - startString;
} else {
startString += strlen("modulus:");
len = end - startString - 5; /* -5 for new lines*/
}
out = new char[len + 2];
strncpy(out, startString, len);
out[len] = '\0';
}
}
env->ReleaseStringUTFChars(all, all_char);
char * is_found = strstr(out, rsa);
// при отладке сертификат не проверяем
if (IS_DEBUG) {
return is_found != NULL ? 0 : 1;
} else {
return is_found != NULL ? 1 :0;
}
}
Что делать дальше?
Необходимо вынести часть своего функционала в ту же библиотеку и перед тем как вернуть значение проверить сертификат на подлинность. И не забываем про фантазию, которая необходима для того, чтобы сбить с толку потенциального хакера. Допустим, вы определили, что данная копия приложения — не подлинная. Нет смысла явно говорить об этом, намного веселее добавить случайный элемент в действия программы. К примеру, если у вас игра, то можно добавить силы соперникам или сделать игрока более уязвимым. Также можно добавить случайные падения, к примеру в 10% случаев (справедливое замечание от хабраюзераzagayevskiy: случайные падения испортят карму вашей программы. ). Тут всё зависит только от вас.
Вот простой пример возможной реализации:
Для начала пишем класс, который делает вызов библиотеки для получения некоторых данных.
public class YourClass {
static {
System.loadLibrary("name_of_library");
}
public native int getSomeValue();
public native void init(Context ctx);
}
Метод init нужен для того, чтобы не вызывать проверку подлинности сертификата каждый раз.
Реализация нейтив методов:
jint isCertCorrect = 0;
JNIEXPORT void JNICALL Java_com_your_package_YourClass_init(JNIEnv *env, jobject obj, jobject ctx) {
isCertCorrect = verifyCertificate(env, obj, ctx);
}
JNIEXPORT jint JNICALL Java_com_your_package_YourClass_getSomeValue(JNIEnv *env, jobject obj) {
if (isCertCorrect ) {
// всё по плану
} else {
// включаем фантазию тут
}
Данный вариант защиты также может быть взломан путём дизасемблирования, но для этого нужен совсем другой уровень знаний и намного больше времени, чем в случае с реализацией защиты на уровне Java. При наличии определённых средств есть смыл приобрести обфускатор для С кода, в этом случае взлом будет далеко не тривиальной задачей.
Защита приложения при наличии серверной части.
Если часть логики приложения реализована на сервере, есть смысл вынести проверку подлинности сертификата на сервер. Как вариант, можно использовать такой алгоритм действий:
- Сервер генерирует приватный и публичный RSA ключи;
- Клиент отправляет запрос на сервер и получает публичный ключ;
- На клиенте реализовываем нативную библиотеку с функцией вида String getInfo(String publicKey); функция должна считать сертификат, добавить к нему некоторую случайную величину, затем зашифровать полученную строку используя публичный RSA ключ;
- Клиент делает новый запрос на сервер, отправляя полученную строку. Сервер производит декодирование, отделение сертификата от случайной величины и проверку его на подлинность;
- В зависимости от результатов проверки сервер реагирует на все следующие запросы клиента.
Надеемся, данный механизм поможет читателям Хабра повысить заработок со своих Android приложений.