Защита Android приложений от взлома

    В данной статье мы кратко расскажем о том, как можно защитить свою программу от взлома, не интегрируя стандартное решение от Google и предоставим пример рабочего кода. Интересно? Просим под кат!



    В чём слабость 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. При наличии определённых средств есть смыл приобрести обфускатор для С кода, в этом случае взлом будет далеко не тривиальной задачей.

    Защита приложения при наличии серверной части.



    Если часть логики приложения реализована на сервере, есть смысл вынести проверку подлинности сертификата на сервер. Как вариант, можно использовать такой алгоритм действий:

    1. Сервер генерирует приватный и публичный RSA ключи;
    2. Клиент отправляет запрос на сервер и получает публичный ключ;
    3. На клиенте реализовываем нативную библиотеку с функцией вида String getInfo(String publicKey); функция должна считать сертификат, добавить к нему некоторую случайную величину, затем зашифровать полученную строку используя публичный RSA ключ;
    4. Клиент делает новый запрос на сервер, отправляя полученную строку. Сервер производит декодирование, отделение сертификата от случайной величины и проверку его на подлинность;
    5. В зависимости от результатов проверки сервер реагирует на все следующие запросы клиента.


    Надеемся, данный механизм поможет читателям Хабра повысить заработок со своих Android приложений.
    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 37
      +3
      К примеру, если у вас игра, то можно добавить силы соперникам или сделать игрока более уязвимым. Также можно добавить случайные падения, к примеру в 10% случаев.

      И получить тонну негативных отзывов в сети?
        0
        Это написано для примера. Каждый разработчик сам решает, что для него важнее и как бороться с пиратами.
          +4
          Был я как-то на конференции GameDev разработчиков в Ciklum Minsk… Там рассказывалось про игру «Демократия», что защиты практически не было — был только одноглазый пират с повязкой на глазу, который бегал по полю и мешал игроку… Так вот, некоторым игрокам хватало ума отправлять письма с вопросами «Как убрать пирата?!». И в отзывах подобное писали. По-моему, самый правильный способ защиты. Ну это так.
          А нестабильность работы можно спихнуть на кривость взлома.
          А про силы соперников… Эта техника была актуально еще очень давно, я где-то читал про то, что в какой-то игре для Денди при изменении хэша логотипа враги становились в несколько раз сильнее, а некоторые так и вообще непроходимыми.
          Чем плох такой достаточно оригинальный подход?
            0
            Неплохо, но всё равно не переплюнут habrahabr.ru/post/178339/
              0
              Подход отличный. Но статья о том как определить, что Android приложение было модифицировано, при этом усложнив жизнь потенциальным хакерам. Цель всей этой затеи — сделать так, чтобы купить приложение было дешевле, чем его взломать.
                +2
                Имхо, конкретно такой подход плох именно негативом в отзывах. Нелегалов будет (возможно) достаточно много. А потенциальные покупатели Вашей игры могут читать такие отзывы не разбираясь, пиратская версия или нет.

                Подход с пиратом — креативен и крут, плюсую=) Все-таки лучше визуально показать игроку, что что-то идет не так, попытаться давить на совесть. А нерегулярные падения считаю дурным тоном.
                  0
                  Дописал в статье, замечание про нерегулярные падения дельное.
                    +5
                    А разве можно оставлять отзывы к программам, которые установлены не из маркета?
                    0
                    Так же было и в игре Game Dev Tycoon, когда твою игру взламывали пираты. Только разрабы в шутку сами залили «пиратскую» версию на трекеры :)
                  0
                  Хотелось бы дать рекомендацию типа встройте проверку на время — и требуйте обновления когда время прошло — это практически не заденет легалов. Однако как оказалось — проверка времени может считаться незаконной.
                  +2
                  К примеру, если у вас игра, то можно добавить силы соперникам или сделать игрока более уязвимым. Также можно добавить случайные падения, к примеру в 10% случаев.

                  Взымать оплату по смс и выводить сообщение о том, какой пользователь плохой. =)
                  Логично, что делать такую проверку в Java не имеет смысла, так как аналогичный приём с использованием apktool похоронит вашу защиту за несколько минут. Поэтому данную проверку стоит перенести на нейтив уровень…
                  JNIEXPORT jint JNICALL Java_com_your_package_YourClass_getSomeValue(JNIEnv *env, jobject obj) {
                  if (isCertCorrect ) {
                  // всё по плану
                  } else {
                  // включаем фантазию тут
                  
                  }

                  В этом случае поправить переход не менее элементарно, чем декомпилировать байт-код java. Вообще где-то тут лежала статья про отладку в delphi, в том числе там рассматривался и реверс. Вся описанная там теория применима и для андроида, а практика с поправкой на платформу и архитектуру процессора.

                  Хотелось бы больше фокусов и зрелищ ;) И не могли бы подсказать по поводу обфускаторов?
                    0
                    Лично мне проверка по сертификату кажется наиболее верным решением, но не должно быть единственным. Чем больше методов, тем больше головной боли у реверсера.
                      0
                      В этом случае поправить переход не менее элементарно, чем декомпилировать байт-код java.

                      Позвольте мне не согласиться. Проводить реверс инжениринг нативной библиотеки многим не под силу.
                      Хотелось бы больше фокусов и зрелищ ;) И не могли бы подсказать по поводу обфускаторов?

                      Если у вас есть что защищать и есть на это средства, рекомендую глянуть в сторону этой тулзы
                      Это не просто обфускатор, а целый фреймворк, позволяющий реагировать на различные действия со стороны хакера.
                        0
                        Просто надо не в java писать if, а из нативного кода бросать исключение. А нативный код не вызывать руками(init), а писать его в JNI_OnLoad.
                        0
                        Хорошо, допустим, у нас не игра, а бизнес-приложение, на SDK, не требующее NDK для своей работы. Допустим, я сделал нативную проверку сертификата. Что мешает модифицировать ту часть Java кода которая будет вызывать эту самую нативную проверку?
                          0
                          В таком случае, вам необходимо перенести часть своей логики на нативный уровень. Никаких проверок на Java уровне не должно быть, об этом написано в статье.
                            0
                            Например, софт, запускающийся в tomcat-е на разных платформах, например, под AIX-ом на HP-шном кластере. :)
                              0
                              Уточню — если разговор идет не про ондроед-платформу как частность, а про общий случай, когда нейтив использовать проблематично из-за разных платформ для запуска.
                                0
                                Это уже другая история. Тут только про Android. Логично, что нельзя считать то, чего нет.
                            0
                            Как вам такой вариант защиты без NDK?
                            — В raw или assets кладем текстовый файл, содержащий исходный код (часть функциональности) зашифрованный ключом разработчика
                            — Когда требуется, приложение пытается расшифровать и выполнить его, используя информацию о текущем сертификате которым подписано приложение.

                            Пример выполнения кода из текста здесь.

                            Минусы, на мой взгляд:
                            — дополнительная библиотека
                            — «целостность» программы не контролируется т.к. часть кода в текстовом файле
                            — злоумышленник может вставить выполнение своего кода

                            Взломать можно, но для этого потребуется сначала «вычислить» ключ разработчика, затем расшифровать текстовый файл и заново зашифровать уже свой ключом, которым будет подписываться взломанная версия программы.
                              0
                              А какой алгоритм использовать для шифровки? Если делать упор на секретность ключа, то как только его найдут — защита упала.
                                0
                                1. Симметричный, например DES
                                2. В роли ключа «сигнатура подписи + солью», генерировать его динамически из нескольких кусочков
                                Если это найдут и потратят время на взлом, значит, ваша программа уже достаточно популярна. Можно больше не заморачиваться на защите и просто выпустить открытую версию. Иначе затраты на защиту (и поддержание ее в актуальном состоянии) в конечном итоге могут превысить стоимость разработки.
                                +1
                                Прочитают память приложения.
                                  0
                                  Правильный подход
                                  Я так делал еще в Винде на CBuilderе: Формы (*.dfm) держал в ресурсах в зашифрованном виде. Перед вызовом формы расшифровывал в памяти и грузил.
                                  0
                                  RSA ключ не желательно хранить в одной строке целиком. Будет лучше, если его собирать по кусочкам по ходу функции verifyCertificate()
                                    0
                                    Во-первых, никакой защиты в посте нет — никто менять ваш код не будет без особой необходимости, поэтому и с сертификатами никаких проблем не будет.

                                    Во-вторых, я считаю, что тратить свое время и деньги на борьбу с ветряными мельницами не имеет смысла — приложения на варезники выкладывают не олухи, поэтому делать защиту «от дурака» бессмысленно. Писать полноценную систему защиты? Это должен быть основной ваш бизнесс, но в итоге тоже сломают (:

                                    В-третьих, «защита» ваша настолько наивна, что вы бы постыдились такое писать — банальный grep по бинарику покажет серт. Имена ява функций в явном виде? Ох лол… Тут даже обсуждать нечего.
                                      0
                                      В-третьих, «защита» ваша настолько наивна, что вы бы постыдились такое писать — банальный grep по бинарику покажет серт. Имена ява функций в явном виде? Ох лол… Тут даже обсуждать нечего.

                                      Почитайте коментарии, там указана ссылка на правильную тулзу. Опять же греп покажет сертификат, но всеравно надо иметь скил его заменить.
                                        0
                                        Если вы используете ява функции для получения критичных данных, то весь скилл состоит в подкладывании модифицированного ява класса из сорсов андроида. Работы на пару минут времени, увы.
                                          +1
                                          Напишите статью, я думаю это будет интересно. Опять же, то что описано в статье — это базовый подход. Модифицируйте его, купите обфускатор и живите спокойно.
                                            0
                                            Опыт подсказывает, что это все бесполезно.
                                              0
                                              У меня есть противоположный опыт. Есть приложение, около миллиона активных пользователей. Защита реализована похожим методом + обфускатор нативной библиотеки. Уже больше года тишина. До этого версию с защитой от гугла взломали в день релиза.
                                                0
                                                И что за приложение?
                                                  0
                                                  в приват отписал
                                                    +1
                                                    пишите тут — ну что за детский сад?
                                                      +1
                                                      раз минусанули — то видимо не пишите :) но мне в личку напишите плиз :) забавно, конечно…
                                      0
                                      Вместо PackageInfo info =getPackageManager().getPackageInfo(getPackageName(), 0);
                                      возможно надо
                                      info = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES);

                                      Без этого флага падает, лично у меня.

                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                      Самое читаемое