Решил Android-таск на реверс от Delivery Club. Делюсь райтапом.
Перед нами Android-приложение с единственной кнопкой на экране. Запускаем в эмуляторе — при нажатии на кнопку приложение крашится. Внутри java-части приложения тоже ничего интересного: кнопка вызывает нативный метод collectMetrics и в зависимости от возвращаемого значения выводит одно из двух сообщений.
При беглом просмотре нативной части приложения в IDA Pro становится понятно, что код хорошо пообфусцирован техниками Control Flow Flattening, Opaque Predicate и многими другими. Идём в JNI_OnLoad
— функцию, которую дёргает JVM непосредственно при загрузке нативной библиотеки через System.loadLibrary
. Сразу проставляем тип JNIEnv*
везде, где есть indirect call'ы, так как обычно в них скрываются вызовы JNI. Из интересного там есть только JNIEnv->RegisterNatives
, который регистрирует нативный обработчик для Java-метода collectMetrics
(он вызывается при нажатии на кнопку), поэтому, судя по всему, вся логика проверок должна быть там.
Да, определённо это какие-то проверки на эмулятор
Так как из кода ничего не понятно, а писать деобфускатор пока что не хочется, будем ковырять приложение динамическими методами. Набрасываем первый вариант скрипта для DBI-фреймворка Frida. С помощью фриды я анализирую вызовы методов внутри приложения и, если требуется, могу изменить их поведение. В скрипте ниже я изменил реакцию приложения на нажатие кнопки и отредактировал поля класса android.os.Build
, чтобы приложение не могло по ним понять, что оно запускается в эмуляторе:
var hooked = false;
Java.perform(function() {
let MainActivity_a = Java.use("com.example.dc_challenge.MainActivity$a");
MainActivity_a.onClick.implementation = function (view) {
if (!hooked) {
disarmBuildChecks();
hooked = true;
}
this.onClick(view);
}
});
function disarmBuildChecks() {
var config = {
BOARD: "prada",
BOOTLOADER: "unknown",
BRAND: "Xiaomi",
DEVICE: "prada",
DISPLAY: "MMB29M",
FINGERPRINT: "Xiaomi/prada/prada:6.0.1/MMB29M/v8.0.3.0.0.MCECNDG:user/release-keys",
HARDWARE: "qcom",
HOST: "c3-miui-ota-bd20",
ID: "MMB29M",
MANUFACTURER: "Xiaomi",
MODEL: "Redmi 4",
PRODUCT: "prada",
RADIO: "unknown",
SERIAL: "17fc681d",
TAGS: "release-keys",
TIME: 1476359370000,
TYPE: "user",
USER: "builder",
};
var Build = Java.use('android.os.Build');
Object.keys(config).map(function (key) {
Build[key].value = config[key];
});
}
Пробуем нажать кнопку под фридой — и снова падение. Определённо, проверками build-параметров приложение не ограничивается. Копаем дальше.
В списке импортов нативной библиотеки ну очень много разных функций, и наиболее интересными видятся функции работы со строками. Возможно, какие-то из них используются для проверок окружения.
Пишем вспомогательную функцию для быстрого трейсинга функций работы со строками:
function hook(name, count) {
Interceptor.attach(Module.findExportByName("libc.so", name), {
onEnter: function(args) {
let bt = DebugSymbol.fromAddress(Thread.backtrace(this.context, Backtracer.ACCURATE)[0]);
let arg = [];
for (var i = 0; i < count; i++){
try {
arg.push(Memory.readCString(args[i]));
} catch (e) {}
}
if (bt.moduleName.indexOf("libchallenge.so") !== -1) {
console.log(name + '("' + arg.join('", "') + '") ' + bt);
}
}
});
}
И трейсим всё что нашли в импортах:
function makeHooks() {
hook("strcmp", 2);
hook("strncmp", 2);
hook("strncpy", 2);
hook("strcat", 2);
hook("strchr", 1);
hook("strcspn", 2);
hook("strcpy", 2);
hook("strlen", 1);
hook("strcasecmp", 2);
hook("snprintf", 8);
hook("strdup", 1);
hook("strncasecmp", 2);
hook("strrchr", 1);
hook("strspn", 2);
hook("strstr", 2);
hook("strtol", 1);
hook("strtoul", 1);
}