ZeroNights 2017 Crackme №3 — довольно простое задание, но, тем не менее, и в нём есть несколько интересных моментов и сова.


Под катом — дизассемблирование, декомпиляция, pull request в IntelliJ IDEA и ни единого запуска отладчика.


Файл задания доступен по адресу https://events.kaspersky.com/crackme/files/crackme3.zip
В архиве одиноко лежит Android-приложение app-release.apk. APK это, как и многое в современном мире, zip-архив. Достаём и распаковываем его.


Внутри много мусора от одного из стандартных шаблонов Android-приложения в /res, какие-то native библиотеки в /lib, файлы со странными именами в /assets и, самое главное, код приложения в classes.dex. Конвертируем classes.dex в jar-файл при помощи dex2jar.


Код внутри организован в несколько пакетов:


  • android.*
  • com.scottyab.rootbeer
  • com.googlecode.leptonica
  • com.googlecode.tesseract
  • com.kaspersky.zeronights2017.nightwatcher

Сохранять оригинальные имена пакетов — довольно сомнительная практика для crackme, но нам от этого только проще. Резонно предположить, что основная логика приложения находится в пакете com.kaspersky.zeronights2017.nightwatcher. Тем более, что именно там расположен класс MainActivity.


Декомпилируем и изучаем его:


MainActivity.java
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.kaspersky.zeronights2017.nightwatcher;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.Build.VERSION;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import com.googlecode.tesseract.android.TessBaseAPI;
import java.io.File;

public class MainActivity extends Activity {
    public static EditText a;
    public static EditText b;
    public static String c;
    private String d;
    private final String e;

    public MainActivity() {
        this.d = \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.q;
        this.e = \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF(\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.d);
    }

    private void a() {
        Bitmap var1 = BitmapFactory.decodeResource(this.getResources(), 2131099746);
        TessBaseAPI var2;
        if (VERSION.SDK_INT > 19) {
            this.d = this.getFilesDir() + \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.e;
            var2 = new TessBaseAPI();
            this.a(new File(this.d + this.e), \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.i);
            var2.a(this.d, \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.f);
            var2.a(var1);
            c = var2.a();
            this.a(new File(this.d + this.e), \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.j);
            var2.a(this.d, \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.g);
            var2.a(var1);
            c = c + var2.a();
            this.a(new File(this.d + this.e), \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.k);
            var2.a(this.d, \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.h);
            var2.a(var1);
            c = c + var2.a();
        } else {
            this.d = this.getFilesDir() + \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.e;
            var2 = new TessBaseAPI();
            this.a(new File(this.d + this.e), \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.i);
            var2.a(this.d, \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.f);
            var2.a(var1);
            c = var2.a().split("\n")[0];
            this.a(new File(this.d + this.e), \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.j);
            var2.a(this.d, \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.g);
            var2.a(var1);
            c = c + var2.a().split("\n")[0];
            this.a(new File(this.d + this.e), \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.k);
            var2.a(this.d, \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.h);
            var2.a(var1);
            c = c + var2.a().split("\n")[0];
        }

    }

    private void a(File var1, String var2) {
        if (!var1.exists() && var1.mkdirs()) {
            this.a(var2);
        }

        if (var1.exists() && !(new File(this.d + this.e + var2)).exists()) {
            this.a(var2);
        }

    }

    private void a(String param1) {
        // $FF: Couldn't be decompiled
    }

    protected void onCreate(Bundle var1) {
        super.onCreate(var1);
        this.requestWindowFeature(1);
        this.getWindow().setFlags(1024, 1024);
        this.setContentView(2131296283);
        Button var2 = (Button)this.findViewById(2131165217);
        if ((new \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF(this)).\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF()) {
            Toast.makeText(this, 2131361827, 1).show();
            this.finish();
        }

        var2.setOnClickListener(new OnClickListener() {
            public void onClick(View var1) {
                MainActivity.a = (EditText)MainActivity.this.findViewById(2131165230);
                MainActivity.b = (EditText)MainActivity.this.findViewById(2131165280);
                MainActivity.this.a();
                if (!MainActivity.a.getText().toString().matches(\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.q) && !MainActivity.b.getText().toString().toUpperCase().matches(\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.q)) {
                    if (\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF()) {
                        Toast.makeText(MainActivity.this, 2131361829, 1).show();
                    } else {
                        Toast.makeText(MainActivity.this, 2131361828, 1).show();
                    }
                } else {
                    Toast.makeText(MainActivity.this, 2131361828, 1).show();
                }

            }
        });
    }
}

Для удобства чтения в этом и последующих листингах все символы с кодами, превышающими 127, заменены на unicode escape-последовательности вида \uXXXX.


Часть имён классов, полей и методов обфусцирована, некоторые методы декомпилировать не удалось, но метод onCreate() прост и понятен. Из него мы узнаём, что у нас есть как минимум два поля ввода и одна кнопка, при нажатии на которую пользователю выдаётся одно из сообщений в зависимости от результата, возвращённого методом с нечитаемым именем.


Ориентируясь на количество символов в именах класса и метода (34 и 62 соответственно) находим этот метод:


Класс, проверяющий введённый ключ
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.kaspersky.zeronights2017.nightwatcher;

class \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF {
    public static boolean \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF() {
        boolean var0;
        if (
            \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.t 
            / \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.r 
            * \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.f 
            - \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.h 
            == MainActivity.b.getText().toString().toUpperCase().length() 

            && \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF
               .\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF(
                   \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF
                   .\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF(
                       \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF
                       .\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF(
                           MainActivity.c.getBytes()
                       ) 
                       + MainActivity.a.getText().toString()
                   )
               ).equals(MainActivity.b.getText().toString().toUpperCase())

           ) {
            var0 = true;
        } else {
            var0 = false;
        }

        return var0;
    }
}

На этом можно было бы и закончить.


Проверяется длина значения из второго поля ввода. К значению из первого поля ввода добавляется префикс, вероятно константный, по результирующей строке вычисляется какой-то хеш. Значение последнего сравнивается со значением второго поля ввода, приведённого к верхнему регистру. Всё.


Достаточно слегка модифицировать код в apk-файле, добавив логгирование вычисленного хеша перед сравнением, и crackme решён. Но это довольно скучно и на статью не тянет, а потому мы пойдём другим путём.


Иной путь


Вместо этого разберём логику работы и напишем ключегенератор.


Для начала стоит избавиться от обфускации идентификаторов. Часть из них просто однобуквенные, а часть — последовательности различной длины из символов \uFEFF. Чаще всего этот символ используется как byte order mark в начале текстовых файлов в одной из unicode-кодировок. Но у него есть и другое, устаревшее, значение — zero-width no-break space (ZWNBSP) или, если по-русски, пробел нулевой ширины. Именно благодаря этому свойству в диалоге открытия файла мы видим несколько class-файлов «без имён».



К слову, занятный факт: С точки зрения Java Language Specification символ \uFEFF никак особо не выделяется и может быть частью идентификатора начиная со второй позиции. Но у реализации компилятора из Oracle JDK/Open JDK свой взгляд на некоторые вопросы и при компиляции такие символы будут молча вырезаны из идентификатора и в итоговый class-файл не попадут. Мы ни слова не найдём про это в JLS, информация об этом есть только в исходных текстах javac и багтрекере: JDK-7144981.


Fernflower, декомпилятор идущий в составе IntelliJ IDEA, имеет поддержку автоматического переименования идентификаторов. Для этого при вызове декомпилятора из командной строки нужно указать параметр -ren=1.


Но при попытке декомпиляции мы терпим фиаско, декомпилятор не считает такие идентификаторы некорректными. Самое время заглянуть внутрь и узнать причину.


Вот как выглядел метод, определяющий нужно ли переименовывать идентификатор:


@Override
public boolean toBeRenamed(Type elementType, String className, String element, String descriptor) {
  String value = elementType == Type.ELEMENT_CLASS ? className : element;
  return value == null ||
         value.length() <= 2 ||
         Character.isDigit(value.charAt(0)) ||
         KEYWORDS.contains(value) ||
         elementType == Type.ELEMENT_CLASS && (
           RESERVED_WINDOWS_NAMESPACE.contains(value.toLowerCase(Locale.US)) ||
           value.length() > 255 - ".class".length());
}

Переименовываем слишком короткие идентификаторы, слишком длинные, начинающиеся с цифры или совпадающие с ключевым словом или зарезервированными именами файлов на Windows.


Заменяем проверку на число в первой позиции на проверку корректности идентификатора согласно JLS и отсутствии при этом в нём этих замечательных «ignorable chars», оформляем pull request.


Обфускация побеждена, но остались методы, которые не получилось декомпилировать. Займёмся ими.


Немного ассемблера


Для ассемблирования и дизассемблирования будем использовать asmtools из OpenJDK.


Начнём с единственного проблемного метода в MainActivity:


MainActivity.jasm - void a(String)
private Method a:"(Ljava/lang/String;)V"
    stack 4 locals 8
{
        aload_1;
        astore  4;

    try t0;
        new class java/lang/StringBuilder;
        astore  5;
    endtry t0;

        aload_1;
        astore  4;

    try t1;
        aload   5;
        invokespecial   Method java/lang/StringBuilder."<init>":"()V";
    endtry t1;

        aload_1;
        astore  4;

    try t2;
        aload   5;
        aload_0;
        getfield    Field d:"Ljava/lang/String;";
        invokevirtual   Method java/lang/StringBuilder.append:"(Ljava/lang/String;)Ljava/lang/StringBuilder;";
        aload_0;
        getfield    Field e:"Ljava/lang/String;";
        invokevirtual   Method java/lang/StringBuilder.append:"(Ljava/lang/String;)Ljava/lang/StringBuilder;";
        aload_1;
        invokestatic    Method \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF."\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF":"(Ljava/lang/String;)Ljava/lang/String;";
        invokevirtual   Method java/lang/StringBuilder.append:"(Ljava/lang/String;)Ljava/lang/StringBuilder;";
        invokevirtual   Method java/lang/StringBuilder.toString:"()Ljava/lang/String;";
        astore  5;
    endtry t2;

        aload_1;
        astore  4;

    try t3;
        aload_0;
        invokevirtual   Method getAssets:"()Landroid/content/res/AssetManager;";
        astore  6;
    endtry t3;

        aload_1;
        astore  4;

    try t4;
        new class java/lang/StringBuilder;
        astore  7;
    endtry t4;

        aload_1;
        astore  4;

    try t5;
        aload   7;
        invokespecial   Method java/lang/StringBuilder."<init>":"()V";
    endtry t5;

        aload_1;
        astore  4;

    try t6;
        aload   6;
        aload   7;
        aload_0;
        getfield    Field e:"Ljava/lang/String;";
        invokevirtual   Method java/lang/StringBuilder.append:"(Ljava/lang/String;)Ljava/lang/StringBuilder;";
        aload_1;
        invokevirtual   Method java/lang/StringBuilder.append:"(Ljava/lang/String;)Ljava/lang/StringBuilder;";
        invokevirtual   Method java/lang/StringBuilder.toString:"()Ljava/lang/String;";
        invokevirtual   Method android/content/res/AssetManager.open:"(Ljava/lang/String;)Ljava/io/InputStream;";
        astore_1;
    endtry t6;

        aload_1;
        astore  4;

    try t7;
        new class java/io/FileOutputStream;
        astore  7;
    endtry t7;

        aload_1;
        astore  4;

    try t8;
        aload   7;
        aload   5;
        invokespecial   Method java/io/FileOutputStream."<init>":"(Ljava/lang/String;)V";
    endtry t8;

        aload_1;
        astore  4;

    try t9;
        getstatic   Field \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.x:"I";
        newarray byte;
        astore  6;
    endtry t9;

    L125:
        aload_1;
        astore  4;

    try t10;
        aload_1;
        aload   6;
        invokevirtual   Method java/io/InputStream.read:"([B)I";
        istore_3;
    endtry t10;

        aload_1;
        astore  4;

    try t11;
        iload_3;
        getstatic   Field \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.b:"I";
        if_icmpeq   L202;
    endtry t11;

        aload_1;
        astore  4;

    try t12;
        getstatic   Field \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.a:"I";
        istore_2;
    endtry t12;

    L152:
        iload_2;
        iload_3;
        if_icmpge   L179;
        aload_1;
        astore  4;

    try t13;
        aload   6;
        iload_2;
        aload   6;
        iload_2;
        baload;
        getstatic   Field \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.l:"I";
        ixor;
        i2b;
        bastore;
    endtry t13;

        iinc    2, 1;
        goto    L152;

    L179:
        aload_1;
        astore  4;

    try t14;
        aload   7;
        aload   6;
        getstatic   Field \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.a:"I";
        iload_3;
        invokevirtual   Method java/io/OutputStream.write:"([BII)V";
    endtry t14;

        goto    L125;

    catch t0 java/io/IOException;
    catch t1 java/io/IOException;
    catch t2 java/io/IOException;
    catch t3 java/io/IOException;
    catch t4 java/io/IOException;
    catch t5 java/io/IOException;
    catch t6 java/io/IOException;
    catch t7 java/io/IOException;
    catch t8 java/io/IOException;
    catch t9 java/io/IOException;
    catch t10 java/io/IOException;
    catch t11 java/io/IOException;
    catch t12 java/io/IOException;
    catch t13 java/io/IOException;
    catch t14 java/io/IOException;
    catch t15 java/io/IOException;
    catch t16 java/io/IOException;
    catch t17 java/io/IOException;
    catch t18 java/io/IOException;
    catch t19 java/io/IOException;
    catch t20 java/io/IOException;
    catch t21 java/io/IOException;
    catch t22 java/io/IOException;
    catch t23 java/io/IOException;
        astore_1;
        aload_1;
        invokevirtual   Method java/io/IOException.printStackTrace:"()V";

    L201:
        return;
    L202:
        aload_1;
        astore  4;

    try t15;
        aload   7;
        invokevirtual   Method java/io/OutputStream.flush:"()V";
    endtry t15;

        aload_1;
        astore  4;

    try t16;
        aload   7;
        invokevirtual   Method java/io/OutputStream.close:"()V";
    endtry t16;

        aload_1;
        astore  4;

    try t17;
        aload_1;
        invokevirtual   Method java/io/InputStream.close:"()V";
    endtry t17;

        aload_1;
        astore  4;

    try t18;
        new class java/io/File;
        astore_1;
    endtry t18;

        aload_1;
        astore  4;

    try t19;
        aload_1;
        aload   5;
        invokespecial   Method java/io/File."<init>":"(Ljava/lang/String;)V";
    endtry t19;

        aload_1;
        astore  4;

    try t20;
        aload_1;
        invokevirtual   Method java/io/File.exists:"()Z";
        ifne    L201;
    endtry t20;

        aload_1;
        astore  4;

    try t21;
        new class java/io/FileNotFoundException;
        astore_1;
    endtry t21;

        aload_1;
        astore  4;

    try t22;
        aload_1;
        invokespecial   Method java/io/FileNotFoundException."<init>":"()V";
    endtry t22;

        aload_1;
        astore  4;

    try t23;
        aload_1;
        athrow;
    endtry t23;

}

Сразу видим кучу try-блоков, делящих между собой единственный catch-блок с банальным e.printStackTrace();, запрятанный в коде где-то между ними.


Это выглядит слишком сложно для кода, не показывавшего до сих пор никаких попыток серьёзной обфускации. Может dex2jar что-то напутал при конвертации?


Самое время взять apktool и посмотреть на оригинальный Dalvik-байткод до конвертации его в Java-байткод. Заодно и ресурсы достанем.


Подозрения подтвердились, при конвертации не всё прошло гладко. Хотя catch-блок и в самом деле общий, но try-блоков оказывается всего лишь два:


MainActivity.smali - void a(String)
.method private a(Ljava/lang/String;)V
        .locals 8
        .param p1, "arg0"    # Ljava/lang/String;

        .prologue

    :try_start_0
        new-instance v1, Ljava/lang/StringBuilder;

        invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V

        iget-object v2, p0, Lcom/kaspersky/zeronights2017/nightwatcher/MainActivity;->d:Ljava/lang/String;

        invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

        move-result-object v1

        iget-object v2, p0, Lcom/kaspersky/zeronights2017/nightwatcher/MainActivity;->e:Ljava/lang/String;

        invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

        move-result-object v1

        invoke-static {p1}, Lcom/kaspersky/zeronights2017/nightwatcher/\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF;->\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF(Ljava/lang/String;)Ljava/lang/String;

        move-result-object v2

        invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

        move-result-object v1

        invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

        move-result-object v2

        invoke-virtual {p0}, Lcom/kaspersky/zeronights2017/nightwatcher/MainActivity;->getAssets()Landroid/content/res/AssetManager;

        move-result-object v1

        new-instance v3, Ljava/lang/StringBuilder;

        invoke-direct {v3}, Ljava/lang/StringBuilder;-><init>()V

        iget-object v4, p0, Lcom/kaspersky/zeronights2017/nightwatcher/MainActivity;->e:Ljava/lang/String;

        invoke-virtual {v3, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

        move-result-object v3

        invoke-virtual {v3, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

        move-result-object v3

        invoke-virtual {v3}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

        move-result-object v3

        invoke-virtual {v1, v3}, Landroid/content/res/AssetManager;->open(Ljava/lang/String;)Ljava/io/InputStream;

        move-result-object p1

        new-instance v3, Ljava/io/FileOutputStream;

        invoke-direct {v3, v2}, Ljava/io/FileOutputStream;-><init>(Ljava/lang/String;)V

        sget v1, Lcom/kaspersky/zeronights2017/nightwatcher/\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF;->x:I

        new-array v4, v1, [B

    :goto_0
        invoke-virtual {p1, v4}, Ljava/io/InputStream;->read([B)I

        move-result v5

        sget v1, Lcom/kaspersky/zeronights2017/nightwatcher/\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF;->b:I

        if-eq v5, v1, :cond_2

        sget v1, Lcom/kaspersky/zeronights2017/nightwatcher/\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF;->a:I

    :goto_1
        if-ge v1, v5, :cond_0

        aget-byte v6, v4, v1

        sget v7, Lcom/kaspersky/zeronights2017/nightwatcher/\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF;->l:I

        xor-int/2addr v6, v7

        int-to-byte v6, v6

        aput-byte v6, v4, v1

        add-int/lit8 v1, v1, 0x1

        goto :goto_1

    :cond_0
        sget v1, Lcom/kaspersky/zeronights2017/nightwatcher/\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF;->a:I

        invoke-virtual {v3, v4, v1, v5}, Ljava/io/OutputStream;->write([BII)V

    :try_end_0

        .catch Ljava/io/IOException; {:try_start_0 .. :try_end_0} :catch_0

        goto :goto_0

    :catch_0
        move-exception v1

        move-object v0, p1

        .end local p1    # "arg0":Ljava/lang/String;
        .local v0, "arg0":Ljava/lang/String;
        move-object p1, v1

        .end local v0    # "arg0":Ljava/lang/String;
        .restart local p1    # "arg0":Ljava/lang/String;
        invoke-virtual {p1}, Ljava/io/IOException;->printStackTrace()V

    :cond_1
        return-void

    :cond_2

    :try_start_1
        invoke-virtual {v3}, Ljava/io/OutputStream;->flush()V

        invoke-virtual {v3}, Ljava/io/OutputStream;->close()V

        invoke-virtual {p1}, Ljava/io/InputStream;->close()V

        new-instance p1, Ljava/io/File;

        invoke-direct {p1, v2}, Ljava/io/File;-><init>(Ljava/lang/String;)V

        invoke-virtual {p1}, Ljava/io/File;->exists()Z

        move-result v1

        if-nez v1, :cond_1

        new-instance p1, Ljava/io/FileNotFoundException;

        invoke-direct {p1}, Ljava/io/FileNotFoundException;-><init>()V

        throw p1

    :try_end_1

        .catch Ljava/io/IOException; {:try_start_1 .. :try_end_1} :catch_0
.end method

Возвращаемся к листингу, полученному при помощи asmtools jdis MainActivity.class и слегка дорабатываем его. Тело catch-блока перемещаем в конец тела метода, все try-блоки удаляем, а вместо них добавляем один новый, который начинается там, где начинался первый и заканчивается там, где заканчивался последний try-блок в оригинальном листинге.


MainActivity.jasm - исправленный void a(String)
private Method a:"(Ljava/lang/String;)V"
    stack 4 locals 8
{
        aload_1;
        astore  4;

    try t0;
        new class java/lang/StringBuilder;
        astore  5;

        aload_1;
        astore  4;

        aload   5;
        invokespecial   Method java/lang/StringBuilder."<init>":"()V";

        aload_1;
        astore  4;

        aload   5;
        aload_0;
        getfield    Field d:"Ljava/lang/String;";
        invokevirtual   Method java/lang/StringBuilder.append:"(Ljava/lang/String;)Ljava/lang/StringBuilder;";
        aload_0;
        getfield    Field e:"Ljava/lang/String;";
        invokevirtual   Method java/lang/StringBuilder.append:"(Ljava/lang/String;)Ljava/lang/StringBuilder;";
        aload_1;
        invokestatic    Method "\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF"."\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF":"(Ljava/lang/String;)Ljava/lang/String;";
        invokevirtual   Method java/lang/StringBuilder.append:"(Ljava/lang/String;)Ljava/lang/StringBuilder;";
        invokevirtual   Method java/lang/StringBuilder.toString:"()Ljava/lang/String;";
        astore  5;

        aload_1;
        astore  4;

        aload_0;
        invokevirtual   Method getAssets:"()Landroid/content/res/AssetManager;";
        astore  6;

        aload_1;
        astore  4;

        new class java/lang/StringBuilder;
        astore  7;

        aload_1;
        astore  4;

        aload   7;
        invokespecial   Method java/lang/StringBuilder."<init>":"()V";

        aload_1;
        astore  4;

        aload   6;
        aload   7;
        aload_0;
        getfield    Field e:"Ljava/lang/String;";
        invokevirtual   Method java/lang/StringBuilder.append:"(Ljava/lang/String;)Ljava/lang/StringBuilder;";
        aload_1;
        invokevirtual   Method java/lang/StringBuilder.append:"(Ljava/lang/String;)Ljava/lang/StringBuilder;";
        invokevirtual   Method java/lang/StringBuilder.toString:"()Ljava/lang/String;";
        invokevirtual   Method android/content/res/AssetManager."open":"(Ljava/lang/String;)Ljava/io/InputStream;";
        astore_1;

        aload_1;
        astore  4;

        new class java/io/FileOutputStream;
        astore  7;

        aload_1;
        astore  4;

        aload   7;
        aload   5;
        invokespecial   Method java/io/FileOutputStream."<init>":"(Ljava/lang/String;)V";

        aload_1;
        astore  4;

        getstatic   Field "\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF".x:"I";
        newarray byte;
        astore  6;

    L125:
        aload_1;
        astore  4;

        aload_1;
        aload   6;
        invokevirtual   Method java/io/InputStream.read:"([B)I";
        istore_3;

        aload_1;
        astore  4;

        iload_3;
        getstatic   Field "\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF".b:"I";
        if_icmpeq   L202;

        aload_1;
        astore  4;

        getstatic   Field "\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF".a:"I";
        istore_2;

    L152:
        iload_2;
        iload_3;
        if_icmpge   L179;
        aload_1;
        astore  4;

        aload   6;
        iload_2;
        aload   6;
        iload_2;
        baload;
        getstatic   Field "\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF".l:"I";
        ixor;
        i2b;
        bastore;

        iinc    2, 1;
        goto    L152;
    L179:
        aload_1;
        astore  4;

        aload   7;
        aload   6;
        getstatic   Field "\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF".a:"I";
        iload_3;
        invokevirtual   Method java/io/OutputStream.write:"([BII)V";

        goto    L125;

    L202:
        aload_1;
        astore  4;

        aload   7;
        invokevirtual   Method java/io/OutputStream.flush:"()V";

        aload_1;
        astore  4;

        aload   7;
        invokevirtual   Method java/io/OutputStream.close:"()V";

        aload_1;
        astore  4;

        aload_1;
        invokevirtual   Method java/io/InputStream.close:"()V";

        aload_1;
        astore  4;

        new class java/io/File;
        astore_1;

        aload_1;
        astore  4;

        aload_1;
        aload   5;
        invokespecial   Method java/io/File."<init>":"(Ljava/lang/String;)V";

        aload_1;
        astore  4;

        aload_1;
        invokevirtual   Method java/io/File.exists:"()Z";
        ifne    L201;

        aload_1;
        astore  4;

        new class java/io/FileNotFoundException;
        astore_1;

        aload_1;
        astore  4;

        aload_1;
        invokespecial   Method java/io/FileNotFoundException."<init>":"()V";

        aload_1;
        astore  4;

        aload_1;
        athrow;
    endtry t0;

    catch t0 java/io/IOException;
        astore_1;
        aload_1;
        invokevirtual   Method java/io/IOException.printStackTrace:"()V";
    L201:
        return;

}

Ассемблируем исправленный код обратно в class-файл при помощи команды asmtools jasm MainActivity.jasm.


В этот момент обнаружился баг в asmtools: имена классов, содержащие unicode escape последовательности приходилось заключать в двойные кавычки и после этого ассемблер забывал добавить к таким именам классов имя пакета.


Багфикс оказался тривиальным:


--- a/src/org/openjdk/asmtools/jasm/Parser.java
+++ b/src/org/openjdk/asmtools/jasm/Parser.java
@@ -416,6 +416,12 @@
             case STRINGVAL:
                 v = scanner.stringValue;
                 scanner.scan();
+                if (uncond || (scanner.token == Token.FIELD)) {
+                    if ((!v.contains("/"))             // class identifier doesn't contain "/"
+                            && (!v.contains("["))){    // class identifier doesn't contain "["
+                        v = pkgPrefix + v; // add package
+                    }
+                }
                 return pool.FindCellAsciz(v);
                 // Some identifiers might coincide with token names.
                 // these should be OK to use as identifier names.

Теперь проблем с декомпиляцией этого метода не возникает:


Декомпилированный MainActivity::a(String)
private void a(String var1) {
    try {
        StringBuilder var5 = new StringBuilder();
        String var12 = var5.append(this.d).append(this.e).append(\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF(var1)).toString();
        AssetManager var6 = this.getAssets();
        StringBuilder var7 = new StringBuilder();
        InputStream var9 = var6.open(var7.append(this.e).append(var1).toString());
        FileOutputStream var14 = new FileOutputStream(var12);
        byte[] var13 = new byte[\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.x];

        while(true) {
            int var3 = var9.read(var13);
            if (var3 == \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.b) {
                var14.flush();
                var14.close();
                var9.close();
                File var10 = new File(var12);
                if (!var10.exists()) {
                    FileNotFoundException var11 = new FileNotFoundException();
                    throw var11;
                }
                break;
            }

            for(int var2 = \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.a; var2 < var3; ++var2) {
                var13[var2] = (byte)(var13[var2] ^ \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.l);
            }

            var14.write(var13, \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF.a, var3);
        }
    } catch (IOException var8) {
        var8.printStackTrace();
    }

}

Никаких особых секретов в методе не обнаружилось. Читаем данные из ресурсов приложения, XOR-им каждый прочитанный байт с константой, пишем результат во временный файл.


Второй класс с ошибкой декомпиляции в этом же пакете исправляем аналогичным способом.
В нём нас ждал единственный try-блок с двумя catch-блоками, причём один из catch-блоков содержал безусловный переход в тело второго, а тот, в свою очередь, прыгал на код, выполняющийся сразу после выхода из try {} catch {} блока с единственной целью вызова инструкции возврата из метода.


.method public static \uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF\uFEFF(Ljava/lang/String;)Ljava/lang/String;
    .locals 7
    .param p0, "\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff\ufeff"    # Ljava/lang/String;

    .prologue
  :try_start_0

    // Здесь простыня скучных манипуляций над java.security.MessageDigest

  :cond_0
    invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
  :try_end_0

    .catch Ljava/security/NoSuchAlgorithmException; {:try_start_0 .. :try_end_0} :catch_0
    .catch Ljava/io/UnsupportedEncodingException; {:try_start_0 .. :try_end_0} :catch_1

    move-result-object p0

  :goto_1
    return-object p0

  :catch_0
    move-exception p0

  :goto_2
    invoke-virtual {p0}, Ljava/lang/Exception;->printStackTrace()V

    const/4 p0, 0x0

    goto :goto_1

  :catch_1
    move-exception p0

    goto :goto_2
.end method

Классы с ошибками декомпиляции в других пакетах (android.*, и т. п.) игнорируем, вряд ли есть смысл тратить на них силы.


Время для рефакторинга


Теперь, когда от обфускации не осталось и следа, самое время заняться исследованием логики приложения.


Декомпилируем все классы приложения, не забыв подменить оба подправленных class-файла и указать -ren=1 в параметрах декомпилятора. При этом примерно в 32 классах встретились проблемы декомпиляции. Но благодаря тому, что авторы сохранили структуру и даже имена пакетов верхних уровней, мы с высокой долей вероятности знаем, что нам эти классы не интересны.


Текущий шаг на GitHub


На основе результатов декомпиляции создаём проект в Android Studio.


Для начала пройдёмся по MainActivity и выясним, какие использованные в ней resource id чему соответствуют. Воспользуемся для этого сгенерированными apktool файлами res/values/public.xml, res/values/strings.xml и res/values/ids.xml.


В результате мы узнаём, что одно из полей ввода имеет id «email», второе — «serial», из onCreate() нам может прилететь сообщение «Your Android device is rooted!», а метод method_1297() загружает изображение совы из ресурса owl.png:



Переименовываем поля, содержащие ссылки на EditText в emailEditText и serialEditText, идентификаторы строковых ресурсов заменяем их значениями:


protected void onCreate(Bundle var1) {
    super.onCreate(var1);
    this.requestWindowFeature(1);
    this.getWindow().setFlags(1024, 1024);
    this.setContentView(0x7f09001b);
    Button var2 = (Button)this.findViewById(0x7f070021);
    if ((new class_108(this)).method_946()) {
        Toast.makeText(this, "Your Android device is rooted!", 1).show();
        this.finish();
    }

    var2.setOnClickListener(new OnClickListener() {
        public void onClick(View var1) {
            MainActivity.emailEditText = (EditText)MainActivity.this.findViewById(0x7f07002e);
            MainActivity.serialEditText = (EditText)MainActivity.this.findViewById(0x7f070060);
            MainActivity.this.method_1297();
            if (!MainActivity.emailEditText.getText().toString().matches(class_170.field_1366) && !MainActivity.serialEditText.getText().toString().toUpperCase().matches(class_170.field_1366)) {
                if (class_46.method_502()) {
                    Toast.makeText(MainActivity.this, "Congratulations! You did it!", 1).show();
                } else {
                    Toast.makeText(MainActivity.this, "Login Failed.", 1).show();
                }
            } else {
                Toast.makeText(MainActivity.this, "Login Failed.", 1).show();
            }

        }
    });
}

Текущий шаг на GitHub


Root check


Для начала разберёмся с проверкой на рутованность устройства. Класс class_108 оказывается простой обёрткой, маскирующей обращение к методу method_350 класса class_33 в пакете com.scottyab.rootbeer.


class class_108 {
    // $FF: renamed from: a android.content.Context
    private final Context field_937;

    public class_108(Context var1) {
        this.field_937 = var1;
    }

    public boolean method_946() {
        return (new class_33(this.field_937)).method_350();
    }
}

По имени пакета без труда находим библиотеку на GitHub, а оригинальное имя класса и метода опознаём по встречающимся в последнем строкам «su» и «busybox». Этим методом оказался com.scottyab.rootbeer.RootBeer::isRooted()


Убедившись, что никаких побочных эффектов этот код не производит, удаляем и сам if, и класс class_108, и весь пакет com.scottyab.rootbeer, в них больше нет ничего интересного.


Текущий шаг на GitHub


Строковые константы


Продолжаем разбираться с onCreate() и переходим к обработчику нажатия на кнопку.
При нажатии на кнопку содержимое обоих полей ввода сначала проверяется на соответствие регулярному выражению, хранящемуся в поле class_170::field_1366, а затем происходит дополнительная проверка вызовом class_46::method_502(). Начнём по-порядку.


Класс class_170 оказывается набором зашифрованных строк вместе с методами для их расшифровки.


Выглядит это всё примерно так:


Фрагмент оригинального класса class_170
// $FF: renamed from: q java.lang.String
public static String field_1366 = method_1453(method_1452(method_1455(method_1457(method_1451(method_1456(method_1454(method_1458(""))))))));
// $FF: renamed from: r java.lang.String
public static String field_1367 = method_1453(method_1452(method_1455(method_1457(method_1451(method_1456(method_1454(method_1458("#81]A"))))))));

// ...

public static String method_1451(String var0) {
    char[] var2 = var0.toCharArray();

    for(int var1 = 0; var1 < var2.length; ++var1) {
        var2[var1] = (char)(var2[var1] ^ 55);
    }

    return new String(var2);
}

public static String method_1452(String var0) {
    char[] var2 = var0.toCharArray();

    for(int var1 = 0; var1 < var2.length; ++var1) {
        var2[var1] = (char)(var2[var1] ^ 22);
    }

    return new String(var2);
}

public static String method_1453(String var0) {
    char[] var2 = var0.toCharArray();

    for(int var1 = 0; var1 < var2.length; ++var1) {
        var2[var1] = (char)(var2[var1] ^ 11);
    }

    return new String(var2);
}

public static String method_1454(String var0) {
    char[] var2 = var0.toCharArray();

    for(int var1 = 0; var1 < var2.length; ++var1) {
        var2[var1] = (char)(var2[var1] ^ 77);
    }

    return new String(var2);
}

public static String method_1455(String var0) {
    char[] var2 = var0.toCharArray();

    for(int var1 = 0; var1 < var2.length; ++var1) {
        var2[var1] = (char)(var2[var1] ^ 33);
    }

    return new String(var2);
}

public static String method_1456(String var0) {
    char[] var2 = var0.toCharArray();

    for(int var1 = 0; var1 < var2.length; ++var1) {
        var2[var1] = (char)(var2[var1] ^ 66);
    }

    return new String(var2);
}

public static String method_1457(String var0) {
    char[] var2 = var0.toCharArray();

    for(int var1 = 0; var1 < var2.length; ++var1) {
        var2[var1] = (char)(var2[var1] ^ 44);
    }

    return new String(var2);
}

public static String method_1458(String var0) {
    char[] var2 = var0.toCharArray();

    for(int var1 = 0; var1 < var2.length; ++var1) {
        var2[var1] = (char)(var2[var1] ^ 88);
    }

    return new String(var2);
}

Все методы статические и без побочных эффектов, так что просто добавляем в этот класс метод main() который сгенерирует нам расшифрованную версию этого класса:


class_170::main()
public static void main(String... args) {

    System.out.print("package com.kaspersky.zeronights2017.nightwatcher;\n\n");

    System.out.print("public class class_170 {\n\n");

    System.out.printf("    public static final String field_1350 = \"%s\";%n", field_1350.replaceAll("\n", "\\\\n"));
    System.out.printf("    public static final String field_1351 = \"%s\";%n", field_1351);
    System.out.printf("    public static final String field_1352 = \"%s\";%n", field_1352);
    System.out.printf("    public static final String field_1353 = \"%s\";%n", field_1353);
    System.out.printf("    public static final String field_1354 = \"%s\";%n", field_1354);
    System.out.printf("    public static final String field_1355 = \"%s\";%n", field_1355);
    System.out.printf("    public static final String field_1356 = \"%s\";%n", field_1356);
    System.out.printf("    public static final String field_1357 = \"%s\";%n", field_1357);
    System.out.printf("    public static final String field_1358 = \"%s\";%n", field_1358);
    System.out.printf("    public static final String field_1359 = \"%s\";%n", field_1359);
    System.out.printf("    public static final String field_1360 = \"%s\";%n", field_1360);
    System.out.printf("    public static final String field_1361 = \"%s\";%n", field_1361);
    System.out.printf("    public static final String field_1362 = \"%s\";%n", field_1362);
    System.out.printf("    public static final String field_1363 = \"%s\";%n", field_1363);
    System.out.printf("    public static final String field_1364 = \"%s\";%n", field_1364);
    System.out.printf("    public static final String field_1365 = \"%s\";%n", field_1365);
    System.out.printf("    public static final String field_1366 = \"%s\";%n", field_1366);
    System.out.printf("    public static final String field_1367 = \"%s\";%n", field_1367);
    System.out.printf("    public static final String field_1368 = \"%s\";%n", field_1368);

    System.out.print("\n}\n\n");

}

Текущий шаг на GitHub


В результате выполнения этого кода получаем:


Расшифрованный class_170
package com.kaspersky.zeronights2017.nightwatcher;

public class class_170 {

    public static final String field_1350 = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam et maximus eros. Mauris laoreet molestie semper. Aliquam erat volutpat. Sed euismod neque ac ante viverra pellentesque. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Cras efficitur ac augue a mattis. Nunc facilisis elementum lobortis. Ut vehicula malesuada pharetra. Pellentesque a interdum eros, nec pulvinar sem. Morbi dapibus in quam ut ullamcorper. Vivamus volutpat vehicula condimentum.\n\nNullam commodo vestibulum cursus. Aliquam semper ligula ac tortor rutrum molestie. Vivamus nec orci auctor quam vehicula tempor et id neque. Suspendisse fermentum velit ut ipsum pellentesque hendrerit. Fusce convallis aliquam ante non elementum. Donec at scelerisque lacus, eu eleifend nulla. Pellentesque congue nisi in dolor dignissim, non lobortis neque luctus. Mauris ac cursus turpis. Phasellus finibus facilisis justo, convallis mattis lorem lobortis et. Nam ac interdum est. Etiam ac molestie ligula. Maecenas vel purus a odio accumsan venenatis. Nunc ligula dui, pharetra id arcu laoreet, mollis semper leo. Phasellus id magna molestie, semper risus nec, vehicula sem.\n\nDonec eget nisl purus. Phasellus non nisi felis. Aliquam ut odio sit amet neque euismod vulputate non vel mauris. Suspendisse sit amet ligula sed leo condimentum vulputate convallis eu dui. Vestibulum tortor lacus, maximus quis tincidunt a, pretium sed lacus. Maecenas porta dui nisi, vel molestie tortor luctus at. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam odio ante, sollicitudin sed diam quis, ultricies pretium ex. Proin nisi arcu, tristique id leo et, lacinia semper nisi. Fusce posuere lacus at ex pretium venenatis. Sed porta nibh mauris, ut vestibulum nulla aliquam non. Nunc id tortor non nisi blandit volutpat. Phasellus malesuada hendrerit orci a semper. Nam a risus ac arcu dictum imperdiet.\n\nCras ut risus eget leo ultricies dictum vitae et enim. Aliquam erat volutpat. Fusce ut porttitor nisi. Praesent vel pulvinar orci. Aenean ac libero sed tortor sagittis tristique commodo in velit. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam eu enim orci. Nullam consectetur lobortis tellus, ac facilisis ex dapibus sed.\n\nAliquam mollis eget turpis id vestibulum. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque vitae lectus non metus convallis aliquam. Donec rutrum tincidunt quam, iaculis gravida nisl dictum id. Donec malesuada dapibus magna tristique convallis. Curabitur at turpis sed tellus accumsan condimentum. Nam accumsan nisi a ex hendrerit, in sagittis turpis feugiat. Morbi congue et est eu venenatis. Phasellus erat dui, rutrum sit amet fringilla sit amet, imperdiet in nulla. Quisque laoreet neque ac augue sodales, at mollis magna mollis. Mauris nec consectetur purus, quis aliquam leo. Sed lobortis magna efficitur vehicula finibus.";
    public static final String field_1351 = "ABCDEFGHIJKLONMPQRSTVUWXYZabcdefghijlkmnopqrstuvwxyz5193406782+/";
    public static final String field_1352 = "0123456789ABCDEF";
    public static final String field_1353 = "dGUzc9RhdGEv";
    public static final String field_1354 = "/whoami/";
    public static final String field_1355 = "who";
    public static final String field_1356 = "am";
    public static final String field_1357 = "i";
    public static final String field_1358 = "d9hvLnRyYWkuZWRlYXRh";
    public static final String field_1359 = "YW5udHJhaW0kZGRhdGE=";
    public static final String field_1360 = "aS05cmFpbmUlZGF5YQ==";
    public static final String field_1361 = "[^";
    public static final String field_1362 = "=]";
    public static final String field_1363 = "=";
    public static final String field_1364 = "AA";
    public static final String field_1365 = "A";
    public static final String field_1366 = "";
    public static final String field_1367 = "SHA-1";
    public static final String field_1368 = "UTF-8";

}

Текущий шаг на GitHub


Атипичный BASE64


Большинство строк короткие и используются в коде один-два раза, для лучшей читаемости вклеиваем их при помощи рефакторинга «Inline all and remove the field».


Здесь можно обратить внимание на класс class_87: В нём используются строковые константы "AA", "A", "=", и 64-символьная строковая константа class_170::field_1351, а строки "dGUzc9RhdGEv", "d9hvLnRyYWkuZWRlYXRh", "YW5udHJhaW0kZGRhdGE=" и "aS05cmFpbmUlZGF5YQ==" выступают в роли параметров, поступающих на вход метода class_87::method_802(). Всё это наводит нас на мысль, что class_87 это BASE64 encoder/decoder с нестандартным алфавитом. Нестандартность алфавита проявляется в том, что в class_170::field_1351 символы U и V перепутаны местами, а цифры вообще идут не по порядку.


Те из констант, что используются более одного раза или достаточно длинны, переносим в использующие их классы и даём им осмысленные имена. Благо, статические члены класса IDEA/Android Studio переносить умеет. С виртуальными методами и нестатическими полями не всё так радужно.


В результате class_170 оказывается пустым и мы его удаляем.


Текущий шаг на GitHub


Коль скоро у нас есть BASE64-декодер, на вход которому приходят заранее известные константы, логично декодировать их и явно подставить результат в исходники.


Вызов декодера встречается в двух местах: при инициализации поля field_1181 в конструкторе MainActivity и внутри метода MainActivity::method_1300(). Причём в последнем строка для декодирования приходит в качестве параметра и вначале используется для формирования имени входного файла в ресурсах приложения, а затем декодируется и результат декодирования используется в качестве имени выходного файла.


private void method_1300(String resourceName) {
    try {
        StringBuilder var5 = new StringBuilder();
        String var12 = var5.append(this.field_1180).append(this.field_1181).append(CustomBase64.decode(resourceName)).toString();
        AssetManager var6 = this.getAssets();
        StringBuilder var7 = new StringBuilder();
        InputStream var9 = var6.open(var7.append(this.field_1181).append(resourceName).toString());
        FileOutputStream var14 = new FileOutputStream(var12);

   // ...

Разделяем значения до и после декодирования и оба передаём в качестве параметров:


private void method_1299(File destDir, String resourceName, String outputFileName) {
    if (!destDir.exists() && destDir.mkdirs()) {
        this.method_1300(resourceName, outputFileName);
    }

    if (destDir.exists() && !(new File(this.field_1180 + this.field_1181 + resourceName)).exists()) {
        this.method_1300(resourceName, outputFileName);
    }

}

private void method_1300(String resourceName, String outputFileName) {
    try {
        String var12 = this.field_1180 + this.field_1181 + outputFileName;
        AssetManager assets = this.getAssets();
        InputStream inputStream = assets.open(this.field_1181 + resourceName);
        FileOutputStream outputStream = new FileOutputStream(var12);

// ...

Затем декодируем константы и подставляем их, попутно избавляясь от вызовов декодера. Для декодирования используем тот же подход, что и с классом class_170:


public static void main(String... args) {

    System.out.printf("\"%s\"\t\"%s\"%n", "dGUzc9RhdGEv"        , decode("dGUzc9RhdGEv"        ));
    System.out.printf("\"%s\"\t\"%s\"%n", "d9hvLnRyYWkuZWRlYXRh", decode("d9hvLnRyYWkuZWRlYXRh"));
    System.out.printf("\"%s\"\t\"%s\"%n", "YW5udHJhaW0kZGRhdGE=", decode("YW5udHJhaW0kZGRhdGE="));
    System.out.printf("\"%s\"\t\"%s\"%n", "aS05cmFpbmUlZGF5YQ==", decode("aS05cmFpbmUlZGF5YQ=="));

}

Получаем следующее соответствие:


Исходное значение Декодированное значение
dGUzc9RhdGEv tessdata/
d9hvLnRyYWkuZWRlYXRh who.traineddata
YW5udHJhaW0kZGRhdGE= am.traineddata
aS05cmFpbmUlZGF5YQ== i.traineddata

После этого мест, где используется декодирующий метод, не остаётся, удаляем его.


Текущий шаг на GitHub


Целочисленные константы


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


Этот класс, как и class_170, оказывается набором констант, но на этот раз целочисленных. Чтобы никто не догадался, константы заданы комбинациями арифметических операций и вызовов определённого тут же метода method_465():


public static int field_368 = 
(
 (
  (
   (
    (
     (
      (
       (
        (
         (
          (
           (
            (
             (
              (
               method_465(0, 0) + method_465(0, 0)
              ) * method_465(1, 0) * method_465(1, 0) + method_465(0, 0)
             ) * method_465(1, 0) * method_465(1, 0) + method_465(0, 0)
            ) * method_465(1, 0) * method_465(1, 0) + method_465(0, 0)
           ) * method_465(1, 0) * method_465(1, 0) + method_465(0, 0)
          ) * method_465(1, 0) * method_465(1, 0) + method_465(0, 0)
         ) * method_465(1, 0) * method_465(1, 0) + method_465(0, 0)
        ) * method_465(1, 0) * method_465(1, 0) + method_465(0, 0)
       ) * method_465(1, 0) * method_465(1, 0) + method_465(0, 0)
      ) * method_465(1, 0) * method_465(1, 0) + method_465(0, 0)
     ) * method_465(1, 0) * method_465(1, 0) + method_465(0, 0)
    ) * method_465(1, 0) * method_465(1, 0) + method_465(0, 0)
   ) * method_465(1, 0) * method_465(1, 0) + method_465(0, 0)
  ) * method_465(1, 0) * method_465(1, 0) + method_465(0, 0)
 ) * method_465(1, 0) * method_465(1, 0) + method_465(0, 0)
) * method_465(1, 0) * method_465(1, 0);

// ...

public static int method_465(int var0, int var1) {
    return var0 + var1;
}

Да, этот метод — простая обёртка над операцией целочисленного сложения. После рефакторинга «Inline all and remove the method» выражения укорачиваются, но не исчезают совсем:


public static int field_368 = 
(
 (
  (
   (
    (
     (
      (
       (
        (
         (
          (
           (
            (
             (
              (
               0 + 0 + 0 + 0
              ) * (1 + 0) * (1 + 0) + 0 + 0
             ) * (1 + 0) * (1 + 0) + 0 + 0
            ) * (1 + 0) * (1 + 0) + 0 + 0
           ) * (1 + 0) * (1 + 0) + 0 + 0
          ) * (1 + 0) * (1 + 0) + 0 + 0
         ) * (1 + 0) * (1 + 0) + 0 + 0
        ) * (1 + 0) * (1 + 0) + 0 + 0
       ) * (1 + 0) * (1 + 0) + 0 + 0
      ) * (1 + 0) * (1 + 0) + 0 + 0
     ) * (1 + 0) * (1 + 0) + 0 + 0
    ) * (1 + 0) * (1 + 0) + 0 + 0
   ) * (1 + 0) * (1 + 0) + 0 + 0
  ) * (1 + 0) * (1 + 0) + 0 + 0
 ) * (1 + 0) * (1 + 0) + 0 + 0
) * (1 + 0) * (1 + 0);

К счастью, IntelliJ IDEA/Android Studio умеет вычислять константные выражения. Достаточно переместить каретку на такое выражение и нажать Alt-Enter, в контекстном меню будет пункт «Compute constant value of ...». Единственное неудобство — так придётся сделать с каждым выражением по отдельности.


Наконец, получаем целочисленные константы. Километровое выражение оказалось нулём:


public static int field_368 = 0;

Все поля класса class_44 инлайним, а сам опустевший класс удаляем.


Текущий шаг на GitHub


Проверка ключа


Вернёмся к вызову метода class_46::method_502() в обработчике нажатия на кнопку. Это тот самый метод проверки правильности введённого ключа из начала статьи, но теперь, после декодирования констант и переименования идентификаторов, он выглядит чуть получше:


class class_46 {

    public static boolean method_502() {
        boolean var0;
        if (2949 / 240 * 4 - 8 == MainActivity.serialEditText.getText().toString().toUpperCase().length() && 
            class_135.method_1132(
              class_135.method_1133(
                CustomBase64.encode(MainActivity.field_1179.getBytes()) + 
                MainActivity.emailEditText.getText().toString()
              )
            ).equals(MainActivity.serialEditText.getText().toString().toUpperCase())) {
            var0 = true;
        } else {
            var0 = false;
        }

        return var0;
    }

}

Добавим ему читаемости:


class SerialValidator {

    public static boolean validateSerial() {

        String email = MainActivity.emailEditText.getText().toString();
        String serial = MainActivity.serialEditText.getText().toString().toUpperCase();
        String expectedSerial = class_135.method_1132(
            class_135.method_1133(
                CustomBase64.encode(MainActivity.field_1179.getBytes()) + email
            )
        );

        return
            (serial.length() == 40) &&
            serial.equals(expectedSerial);

    }

}

Текущий шаг на GitHub


Серийный номер имеет длину в 40 символов, все символы в верхнем регистре. Проверка верности серийного номера производится сравнением с предварительно вычисленным правильным значением. Иными словами, crackme содержит готовый код генерации серийных номеров, бери и пользуйся.


Вначале к строке, введённой в поле «email» добавляется префикс, закодированный нашим альтернативным BASE64. Затем полученная строка подвергается трансформациям в методе class_135::method_1133(), вникать в суть которых никакого смысла нет. Наконец, от полученной на предыдущем шаге строки считается SHA-1 хеш и записывается в шестнадцатеричной форме.


public static String method_1132(String var0) {
    try {
        MessageDigest var2 = MessageDigest.getInstance("SHA-1");
        StringBuilder var3 = new StringBuilder(var2.digest(var0.getBytes("UTF-8")).length);

        for(int var1 = 0; var1 < var2.digest(var0.getBytes("UTF-8")).length; ++var1) {
          var3
            .append(HEX_DIGITS.charAt(var2.digest(var0.getBytes("UTF-8"))[var1] >> 4 & 15))
            .append(HEX_DIGITS.charAt(var2.digest(var0.getBytes("UTF-8"))[var1] & 15));
        }

        var0 = var3.toString();
        return var0;
    } catch (UnsupportedEncodingException | NoSuchAlgorithmException var4) {
        var4.printStackTrace();
        return null;
    }
}

Единственной неизвестной величиной остаётся значение префикса, вычисляемого из значения поля MainActivity::field_1179.


Поставщик соли


Значение поля вычисляется в методе MainActivity::method_1297():


private void method_1297() {
    // 0x7f060062: type="drawable" name="owl"
    Bitmap var1 = BitmapFactory.decodeResource(this.getResources(), 0x7f060062);
    TessBaseAPI var2;
    if (VERSION.SDK_INT > 19) {
        this.field_1180 = this.getFilesDir() + field_1354;
        var2 = new TessBaseAPI();
        this.method_1299(new File(this.field_1180 + this.field_1181), "d9hvLnRyYWkuZWRlYXRh", "who.traineddata");
        var2.method_997(this.field_1180, field_1355);
        var2.method_996(var1);
        field_1179 = var2.method_995();
        this.method_1299(new File(this.field_1180 + this.field_1181), "YW5udHJhaW0kZGRhdGE=", "am.traineddata");
        var2.method_997(this.field_1180, field_1356);
        var2.method_996(var1);
        field_1179 = field_1179 + var2.method_995();
        this.method_1299(new File(this.field_1180 + this.field_1181), "aS05cmFpbmUlZGF5YQ==", "i.traineddata");
        var2.method_997(this.field_1180, field_1357);
        var2.method_996(var1);
        field_1179 = field_1179 + var2.method_995();
    } else {
        this.field_1180 = this.getFilesDir() + field_1354;
        var2 = new TessBaseAPI();
        this.method_1299(new File(this.field_1180 + this.field_1181), "d9hvLnRyYWkuZWRlYXRh", "who.traineddata");
        var2.method_997(this.field_1180, field_1355);
        var2.method_996(var1);
        field_1179 = var2.method_995().split("\n")[0];
        this.method_1299(new File(this.field_1180 + this.field_1181), "YW5udHJhaW0kZGRhdGE=", "am.traineddata");
        var2.method_997(this.field_1180, field_1356);
        var2.method_996(var1);
        field_1179 = field_1179 + var2.method_995().split("\n")[0];
        this.method_1299(new File(this.field_1180 + this.field_1181), "aS05cmFpbmUlZGF5YQ==", "i.traineddata");
        var2.method_997(this.field_1180, field_1357);
        var2.method_996(var1);
        field_1179 = field_1179 + var2.method_995().split("\n")[0];
    }
}

Вначале из ресурсов загружается сова, затем над ней совершаются какие-то магические ритуалы, в результате которых получается значение поля field_1179.


Судя по манифесту приложения, на работу при (VERSION.SDK_INT <= 19) оно не рассчитано, так что else-ветку можно смело проигнорировать.


Перейдём к магии. Вся магия вершится путём серии вызовов методов класса com.googlecode.tesseract.android.TessBaseAPI. По имени класса и его native-методам опознаём Android-версию OCR-библиотеки Tesseract.


Возможно, что это tess-two. Во всяком случае, по API подходит.


Имена методов и полей в TessBaseAPI обфусцированы, но есть строковые константы, native-методы, иерархия классов и исходники на GitHub. Используя всё это можно восстановить оригинальные имена полей и методов. Занятие несложное, но нудное. Если заниматься этим регулярно, то стоит потратить время на автоматизацию. Но единственный класс в crackme явно не стоит таких усилий, проще всё сделать вручную.


Результат:


private void computeSalt() {
    // 0x7f060062: type="drawable" name="owl"
    Bitmap bitmap = BitmapFactory.decodeResource(this.getResources(), 0x7f060062);
    TessBaseAPI tessBaseAPI;

    if (VERSION.SDK_INT > 19) {

        this.dataPath = this.getFilesDir() + DIRNAME_WHOAMI;
        tessBaseAPI = new TessBaseAPI();

        this.method_1299(new File(this.dataPath + this.DIRNAME_TESSDATA), "d9hvLnRyYWkuZWRlYXRh", "who.traineddata");
        tessBaseAPI.init(this.dataPath, LANGANAME_WHO);
        tessBaseAPI.setImage(bitmap);
        SALT = tessBaseAPI.getUTF8Text();

        this.method_1299(new File(this.dataPath + this.DIRNAME_TESSDATA), "YW5udHJhaW0kZGRhdGE=", "am.traineddata");
        tessBaseAPI.init(this.dataPath, LANGANAME_AM);
        tessBaseAPI.setImage(bitmap);
        SALT = SALT + tessBaseAPI.getUTF8Text();

        this.method_1299(new File(this.dataPath + this.DIRNAME_TESSDATA), "aS05cmFpbmUlZGF5YQ==", "i.traineddata");
        tessBaseAPI.init(this.dataPath, LANGANAME_I);
        tessBaseAPI.setImage(bitmap);
        SALT = SALT + tessBaseAPI.getUTF8Text();

    } else {
        // ...
    }
}

Благодаря тому, что мы теперь знаем назначение методов и их параметров появляется возможность дать осмысленные имена некоторым переменным и константам. В частности, строковые константы со значениями "who", "am", "i" оказались именами языков распознания текста, созданных специально для этого crackme и декодируемые вызовами метода MainActivity::method_1299().


Текущий шаг на GitHub


Метод MainActivity::method_1299() выглядит несколько странно:


private void method_1299(File destDir, String resourceName, String outputFileName) {
    if (!destDir.exists() && destDir.mkdirs()) {
        this.method_1300(resourceName, outputFileName);
    }

    if (destDir.exists() && !(new File(this.dataPath + this.DIRNAME_TESSDATA + resourceName)).exists()) {
        this.method_1300(resourceName, outputFileName);
    }
}

Если целевая директория отсутствует, то мы создаём её и декодируем файл в первом if, а если директория по каким-то причинам уже существует, но файла, который нужно декодировать, в ней нет, то декодируем его во втором if. Почему бы сначала не убедиться, что директория создана, а потом один раз вызвать метод декодирования — не ясно.


Позднее, уже во время подготовки данной статьи, оказалось, что аналогичный код встречается во многих примерах использования Tesseract на Android.


Вот один из них, сравните сами:


private void checkFile(File dir) {
    //directory does not exist, but we can successfully create it
    if (!dir.exists()&& dir.mkdirs()){
        copyFiles();
    }
    //The directory exists, but there is no data file in it
    if(dir.exists()) {
        String datafilepath = datapath+ "/tessdata/eng.traineddata";
        File datafile = new File(datafilepath);
        if (!datafile.exists()) {
            copyFiles();
        }
    }
}

Метод MainActivity::method_1300() копирует указанный файл из ресурсов в целевую директорию, делая xor 0x14 каждому байту, это его мы лечили от запутанных try-catch блоков.


После преобразования в файле становятся различимы текстовые включения, так что мы на верном пути:



Декодированные файлы *.traineddata на GitHub


Птичий язык


Коль скоро у нас есть и сова, и языковые файлы для её распознания, попробуем узнать, что спрятано внутри. Нам нужен Tesseract версии 4.xx, предыдущие версии имеющиеся у нас файлы *.traineddata не понимают.


Собранные бинарники можно поискать здесь:



Вначале ничего, кроме жалоб на некорректное разрешение в 0 DPI, добиться от программы не получалось. Но после применения метода научного тыка было выяснено, что явное задание режима сегментации изображения «Raw line» исправляет ситуацию, хотя жалобы на DPI никуда не исчезают.


$ tesseract --tessdata-dir ./tessdata -psm 13 -l who owl.png stdout
Warning. Invalid resolution 0 dpi. Using 70 instead.
7F6B45B

$ tesseract --tessdata-dir ./tessdata -psm 13 -l am owl.png stdout
Warning. Invalid resolution 0 dpi. Using 70 instead.
6E8AC25D6A9

$ tesseract --tessdata-dir ./tessdata -psm 13 -l i owl.png stdout
Warning. Invalid resolution 0 dpi. Using 70 instead.
9E7DB916E40FD0

Позднее я нашёл эти строки в явном виде внутри соответствующих *.traineddata файлов, так что сова — это неделимое целое, эдакий петроглиф на стене опенспейса в «Лаборатории Касперского».


Теперь мы можем подставить секретную строку "7F6B45B6E8AC25D6A99E7DB916E40FD0" в качестве значения константы SALT, а пакет com.googlecode.tesseract и весь код, его использующий, удалить.


Текущий шаг на GitHub


Keygen


Всё готово к написанию генератора ключей.


Создадим в корневом пакете класс K2017Crackme3Keygen, в котором будем запрашивать ввод E-Mail, как и в оригинальном коде crackme, а генерацию ключа возьмём из метода SerialValidator::validateSerial():


import com.kaspersky.zeronights2017.nightwatcher.CustomBase64;
import com.kaspersky.zeronights2017.nightwatcher.MainActivity;
import com.kaspersky.zeronights2017.nightwatcher.class_135;

public class K2017Crackme3Keygen {

    public static void main(String... args) {

        System.out.print("E-Mail: ");
        String email = System.console().readLine();

        String expectedSerial = class_135.method_1132(
            class_135.method_1133(
                CustomBase64.encode(MainActivity.SALT.getBytes()) + email
            )
        );

        System.out.printf("Serial: %s", expectedSerial);

    }

}

Для красоты значение выражения CustomBase64.encode(MainActivity.SALT.getBytes()) вычислим и положим в поле TRUE_SALT, а все используемые методы переместим в класс ключегенератора, сделав его самодостаточным:


K2017Crackme3Keygen.java
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class K2017Crackme3Keygen {

    public static final String LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam et maximus eros. Mauris laoreet molestie semper. Aliquam erat volutpat. Sed euismod neque ac ante viverra pellentesque. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Cras efficitur ac augue a mattis. Nunc facilisis elementum lobortis. Ut vehicula malesuada pharetra. Pellentesque a interdum eros, nec pulvinar sem. Morbi dapibus in quam ut ullamcorper. Vivamus volutpat vehicula condimentum.\n\nNullam commodo vestibulum cursus. Aliquam semper ligula ac tortor rutrum molestie. Vivamus nec orci auctor quam vehicula tempor et id neque. Suspendisse fermentum velit ut ipsum pellentesque hendrerit. Fusce convallis aliquam ante non elementum. Donec at scelerisque lacus, eu eleifend nulla. Pellentesque congue nisi in dolor dignissim, non lobortis neque luctus. Mauris ac cursus turpis. Phasellus finibus facilisis justo, convallis mattis lorem lobortis et. Nam ac interdum est. Etiam ac molestie ligula. Maecenas vel purus a odio accumsan venenatis. Nunc ligula dui, pharetra id arcu laoreet, mollis semper leo. Phasellus id magna molestie, semper risus nec, vehicula sem.\n\nDonec eget nisl purus. Phasellus non nisi felis. Aliquam ut odio sit amet neque euismod vulputate non vel mauris. Suspendisse sit amet ligula sed leo condimentum vulputate convallis eu dui. Vestibulum tortor lacus, maximus quis tincidunt a, pretium sed lacus. Maecenas porta dui nisi, vel molestie tortor luctus at. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam odio ante, sollicitudin sed diam quis, ultricies pretium ex. Proin nisi arcu, tristique id leo et, lacinia semper nisi. Fusce posuere lacus at ex pretium venenatis. Sed porta nibh mauris, ut vestibulum nulla aliquam non. Nunc id tortor non nisi blandit volutpat. Phasellus malesuada hendrerit orci a semper. Nam a risus ac arcu dictum imperdiet.\n\nCras ut risus eget leo ultricies dictum vitae et enim. Aliquam erat volutpat. Fusce ut porttitor nisi. Praesent vel pulvinar orci. Aenean ac libero sed tortor sagittis tristique commodo in velit. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam eu enim orci. Nullam consectetur lobortis tellus, ac facilisis ex dapibus sed.\n\nAliquam mollis eget turpis id vestibulum. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque vitae lectus non metus convallis aliquam. Donec rutrum tincidunt quam, iaculis gravida nisl dictum id. Donec malesuada dapibus magna tristique convallis. Curabitur at turpis sed tellus accumsan condimentum. Nam accumsan nisi a ex hendrerit, in sagittis turpis feugiat. Morbi congue et est eu venenatis. Phasellus erat dui, rutrum sit amet fringilla sit amet, imperdiet in nulla. Quisque laoreet neque ac augue sodales, at mollis magna mollis. Mauris nec consectetur purus, quis aliquam leo. Sed lobortis magna efficitur vehicula finibus.";
    public static final String ALPHABET = "ABCDEFGHIJKLONMPQRSTVUWXYZabcdefghijlkmnopqrstuvwxyz5193406782+/";
    public static final String HEX_DIGITS = "0123456789ABCDEF";
    public static final String TRUE_SALT = "N5Y9QjQ1QjZFMEFDOjUENlE0MVV3REI0OTZFNDBGRDA=";

    public static void main(String... args) {

        System.out.print("E-Mail: ");
        String email = System.console().readLine();

        String expectedSerial = calculateSha1Hex(method_1133(TRUE_SALT + email));

        System.out.printf("Serial: %s", expectedSerial);

    }

    private static String method_1131(String s1, String s2) {
        StringBuilder stringBuilder = new StringBuilder("");
        for(int i = 0; i < 256; i++) {
            stringBuilder.append(String.valueOf(s1.charAt(i) ^ s2.charAt(i % s2.length())));
        }

        return encodeCustomBase64(stringBuilder.toString().getBytes());
    }

    public static String method_1133(String s) {
        StringBuilder stringBuilder = new StringBuilder("");

        for(int i = 0; i < 256; i++) {
            int var2 = ((i << 4 | i) << 240 * i) % 2000000 % 2949;
            stringBuilder.append(LOREM_IPSUM.substring(var2, 1 + var2));
        }

        return method_1131(stringBuilder.toString(), s);
    }

    public static String calculateSha1Hex(String s) {
        try {
            byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
            MessageDigest messageDigest = MessageDigest.getInstance("SHA-1");
            StringBuilder stringBuilder = new StringBuilder(messageDigest.digest(bytes).length);

            for(int i = 0; i < messageDigest.digest(bytes).length; i++) {
                stringBuilder
                    .append(HEX_DIGITS.charAt(messageDigest.digest(bytes)[i] >> 4 & 0xf))
                    .append(HEX_DIGITS.charAt(messageDigest.digest(bytes)[i] & 0xf));
            }

            return stringBuilder.toString();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        }
    }

    public static String encodeCustomBase64(byte[] bytes) {
        char[] alphabetChars = ALPHABET.toCharArray();
        StringBuilder stringBuilder = new StringBuilder();
        int var1 = 0;

        int var2;
        for(int i = 0; i < bytes.length; i += 3) {
            var2 = (bytes[i] & 0xff) << 16 & 0xffffff;
            if (1 + i < bytes.length) {
                var2 |= (bytes[1 + i] & 0xff) << 8;
            } else {
                var1++;
            }

            if (2 + i < bytes.length) {
                var2 |= bytes[2 + i] & 0xff;
            } else {
                var1++;
            }

            int var4 = var2;

            for(var2 = 0; var2 < 4 - var1; var2++) {
                stringBuilder.append(alphabetChars[(0xfc0000 & var4) >> 18]);
                var4 <<= 6;
            }
        }

        for(var2 = 0; var2 < var1; var2++) {
            stringBuilder.append("=");
        }

        return stringBuilder.toString();
    }
}

Менее ста строк.


Текущий шаг на GitHub


Проверка


В качестве проверки сгенерируем ключ для строки Habrahabr.
Хотя поле в crackme и называется «email», никакой валидации вводимого текста нет.


$ java K2017Crackme3Keygen
E-Mail: Habrahabr
Serial: 3C882D5EC917F92D6637F986BA6DAE02CD7E49E3

Пришло время запустить crackme:



Ключ верный, дело сделано.

Комментарии (2)


  1. sbh
    15.03.2018 03:08

    Сколько по времени занял реверс и написание кейгена?


    1. Maccimo Автор
      15.03.2018 03:33

      Пару вечеров, на написание статьи ушло значительно больше времени.