Препарируем Viber. Мини-гид по анализу приложений для Android. Часть 2

Препарируем Viber. Мини-гид по анализу приложений для Android. Часть 2

https://t.me/xakep_1

saruman9

Да­лее я про­дол­жил иссле­дова­ние и выяс­нил, что некото­рые URL находят­ся в blacklist — для них preview не соз­дает­ся:

  • rakuten-viber.atlassian.net;
  • jira.vibelab.net.


$ timeout 5 curl jira.vibelab.net ; echo $?

124


Из любопытс­тва я раз­решил соз­дание preview для этих сай­тов с помощью Frida:

Java.perform(function () {

let LinkParser = Java.use("com.viber.liblinkparser.LinkParser");

const moduleName = "liblinkparser.so";

const moduleBaseAddress = Module.findBaseAddress(moduleName);

const functionRealAddress = moduleBaseAddress.add(0x0000000000013560);

Interceptor.attach(functionRealAddress, {

onEnter: function(args) {

console.log(`check_link_in_black_list = ${args[0]}`);

},

onLeave: function(retval) {

console.log(`return = ${retval}`);

retval.replace(0);

}

});

})

Но ничего инте­рес­ного из это­го не выш­ло, дос­тупа нет. Стран­но, зачем тог­да их нуж­но было бло­киро­вать? Этот воп­рос я оста­вил на потом, может быть, есть какой‑то спо­соб получить дос­туп к этим сай­там со сто­роны обслу­жива­ющих сер­веров Viber через кли­ент.

 

Фаззинг

Пот­ратив опре­делен­ное вре­мя на реверс‑инжи­ниринг, я при­шел к выводу, что гла­зами и инту­ицией быс­тро уяз­вимость най­ти не получит­ся, поэто­му надо при­менить дру­гие методы.


Раз вре­мени у нас мало, выбира­ем единс­твен­ный, но эффектив­ный спо­соб поис­ка уяз­вимос­тей — фаз­зинг. Кро­ме него, конеч­но, мож­но при­менить сра­зу нес­коль­ко спо­собов. Нап­ример, ста­тичес­кий ана­лиз на базе деком­пилиро­ван­ного кода с помощью Joern или дру­гих инс­тру­мен­тов, taint-ана­лиз и ана­лиз, осно­ван­ный на symbolic и concolic execution опас­ных фун­кций, которые исполь­зуют­ся в прог­рамме (я обыч­но поль­зуюсь встро­енны­ми воз­можнос­тями Ghidra на базе PCode, а так­же angrMiasmBAP и дру­гими). Не сле­дует забывать и о поис­ке 1-day-уяз­вимос­тей в open source коде с помощью ана­лиза binary diffing (я при­меняю Ghidra Version Tracking и BinDiff, а так­же руч­ной поиск в слу­чае хороше­го реверс‑инжи­нирин­га кода).

В голову при­ходит нес­коль­ко вари­антов фаз­зинга:

  1. AFL++ в эму­лято­ре с инс­тру­мен­таци­ей на базе Frida.
  2. AFL++ на Android-устрой­стве с инс­тру­мен­таци­ей на базе QEMU.
  3. AFL++ на хос­те с инс­тру­мен­таци­ей на базе QEMU.
  4. Honggfuzz/AFL++ и QBDI.
  5. LibAFL в эму­лято­ре с инс­тру­мен­таци­ей на базе Frida.

Пер­вый вари­ант не хотелось исполь­зовать из‑за пред­положе­ния, что сбор­ка AFL++ под Android зай­мет мно­го вре­мени (я имею в виду вре­мя, которое будет пот­рачено на изу­чение про­цес­са, а не на саму сбор­ку того же AOSP).

Рас­смот­рим вто­рой вари­ант. У меня име­лось тес­товое устрой­ство с архи­тек­турой AArch64. Я подумал, что здесь может воз­никнуть еще боль­ше проб­лем со сбор­кой. Кро­ме того, необ­ходимо было бы собирать обвязку для QEMU для Android AArch64, а пос­ледний мой печаль­ный опыт говорил, что сбор­ка обвязки под ARM быс­тро успе­хом не увен­чает­ся.

Ва­риант за номером три я рас­смат­ривал как вто­рос­тепен­ный, об основном я рас­ска­жу ниже. Если говорить о вари­анте четыре, то я не имел опы­та работы с QBDI, но каж­дый раз очень хочу поп­робовать. Видимо, не в этот раз.

На­конец, пятый вари­ант. Мне этот вари­ант показал­ся иде­аль­ным по нес­коль­ким при­чинам:

  • с LibAFL я уже работал;
  • я Rust-раз­работ­чик, а зна­чит, проб­лемы будут решать­ся быс­трее;
  • мне казалось, что проб­лем со сбор­кой Frida воз­никнет не мно­го;
  • у Rust луч­ше обсто­ят дела с кросс‑ком­пиляци­ей.

Для начала я решил соб­рать тес­товый фаз­зер, который идет в ком­плек­те с LibAFL. С ходу же у меня воз­никли проб­лемы, которых, к чес­ти раз­работ­чиков, было мало. Исправ­лялись они прос­тым пат­чем:

diff --git a/libafl/src/bolts/minibsod.rs b/libafl/src/bolts/minibsod.rs

index 59f6ae6b..40d8e3d5 100644

--- a/libafl/src/bolts/minibsod.rs

+++ b/libafl/src/bolts/minibsod.rs

@@ -10,7 +10,10 @@ use libc::siginfo_t;

use crate::bolts::os::unix_signals::{ucontext_t, Signal};

/// Write the content of all important registers

-#[cfg(all(target_os = "linux", target_arch = "x86_64"))]

+#[cfg(all(

+ any(target_os = "linux", target_os = "android"),

+ target_arch = "x86_64"

+))]

#[allow(clippy::similar_names)]

pub fn dump_registers<W: Write>(

writer: &mut BufWriter<W>,

@@ -408,7 +411,10 @@ fn dump_registers<W: Write>(

Ok(())

}

-#[cfg(all(target_os = "linux", target_arch = "x86_64"))]

+#[cfg(all(

+ any(target_os = "linux", target_os = "android"),

+ target_arch = "x86_64"

+))]

fn write_crash<W: Write>(

writer: &mut BufWriter<W>,

signal: Signal,

По­доб­ного рода исправ­ления говорят о том, что поль­зовате­ли нечас­то исполь­зовали LibAFL на Android x86_64 или вооб­ще не исполь­зовали.

Для ком­пиляции LibAFL я взял Android NDK: мож­но ска­чать готовый либо соб­рать самому. Затем я соб­рал фаз­зер frida_libpng и успешно его про­тес­тировал в эму­лято­ре.

Harness

С harness все выш­ло не так глад­ко, как я ожи­дал, поэто­му я вынес его в самос­тоятель­ный раз­дел.


Про­дол­житель­ное вре­мя я про­водил реверс‑инжи­ниринг, что­бы понять, какую фун­кцию мож­но вызывать без пос­ледс­твий (то есть для нее не тре­бует­ся кон­текст выпол­нения), и при этом ее было бы инте­рес­но фаз­зить. Все детали ревер­са кода C++ я так­тично опу­щу, пос­коль­ку об этом в интерне­те и так написа­но немало. Могу толь­ко ска­зать, что для упро­щения работы с инс­тру­мен­тами я поль­зуюсь сво­ими скрип­тами для Ghidra (так­же не сто­ит забывать про собс­твен­ный мощ­ный репози­торий скрип­тов Ghidra) и снип­петами для Binary Ninja.

Бо́льшая часть работы с сетью про­исхо­дит в Java-коде. Но есть та, которая отве­чает за обра­бот­ку дан­ных, получен­ных от Java-кода (заг­рузка сай­тов для preview). К сожале­нию, эти фун­кции не пред­став­ляет­ся воз­можным как‑то про­ана­лизи­ровать в корот­кий про­межу­ток вре­мени, пос­коль­ку вся их ини­циали­зация про­исхо­дит впе­ремеш­ку с исполь­зовани­ем кода на Java. Необ­ходимо вре­мя, что­бы реали­зовать все заг­лушки при ини­циали­зации, а уже потом, нап­ример, мож­но фаз­зить фун­кции раз­бора дан­ных. Под общей ини­циали­заци­ей я имею в виду ини­циали­зацию пар­серов: для каж­дого типа дан­ных исполь­зует­ся свой пар­сер (встро­енные сай­ты, изоб­ражения, метадан­ные сай­та и тому подоб­ное).

Так­же есть конеч­ный авто­мат для пар­синга HTML. Тело сай­та счи­тыва­ется потоком и чан­ками пода­ется на вход конеч­ному авто­мату, который опре­деля­ет, какого типа дан­ные внут­ри HTML. Затем в зависи­мос­ти от типа дан­ных вызыва­ется тот или иной экс­трак­тор информа­ции:

  • BareTitleExtractor;
  • ImgTagExtractor;
  • LinkTagExtractor;
  • MetaTagExtractor и дру­гие.

Всё это весь­ма инте­рес­ные цели для ана­лиза, но их слож­ность и зависи­мость от Java-кода оста­нав­ливали меня от попыток про­ана­лизи­ровать их.

В ито­ге была най­дена фун­кция, которая занима­ется пар­сингом URL-стро­ки (parse_link). На пер­вый взгляд, она отлично под­ходила для ана­лиза и фаз­зинга.

Прис­тупим к написа­нию обер­тки для фун­кции‑цели. Я начал выс­читывать и под­бирать офсе­ты необ­ходимых мне фун­кций, затем занимал­ся допол­нитель­ным реверс‑инжи­нирин­гом нуж­ных струк­тур:

const ptrdiff_t ADDR_JNI_ONLOAD = 0x0000000000011640;

const ptrdiff_t ADDR_PARSE_LINK = 0x000000000002F870;

const ptrdiff_t ADDR_COPY_JNI_STRING_FROM_STR = 0x0000000000011160;

[...]

typedef struct ParserResult

{

struct String user_agent_string;

struct String user_agent_info_string;

struct String accept_string;

struct String mime_type_string;

} ParserResult;

[...]

При ана­лизе я выяс­нил, что перед вызовом целевой фун­кции ини­циали­зиру­ются дан­ные из дру­гой биб­лиоте­ки — libicuBinder.so. Вот тут я и стол­кнул­ся с проб­лемой, которая отня­ла у меня прак­тичес­ки два дня. В нед­рах фун­кции parse_link про­исхо­дит вызов фун­кции uidna_nameToASCII_UTF8 из биб­лиоте­ки libicuBinder.so. В harness же я, конеч­но, исполь­зовал фун­кции «в сыром виде»:

[...]

Functions *load_functions()

{

LIBC_SHARED = dlopen("/data/local/tmp/libc++_shared.so", RTLD_NOW | RTLD_GLOBAL);

LIBICU_BINDER = dlopen("/data/local/tmp/libicuBinder.so", RTLD_NOW | RTLD_GLOBAL);

LIBLINKPARSER = dlopen("/data/local/tmp/liblinkparser.so", RTLD_NOW | RTLD_GLOBAL);

if (LIBLINKPARSER != NULL && LIBC_SHARED != NULL && LIBICU_BINDER != NULL)

{

int (*JNI_OnLoad)(void *, void *) = dlsym(LIBLINKPARSER, "JNI_OnLoad");

void (*binder_init)() = dlsym(LIBICU_BINDER, "_ZN22IcuSqliteAndroidBinder4initEv");

if (JNI_OnLoad != NULL && binder_init != NULL /* && binder_getInstance != NULL */)

{

Dl_info jni_on_load_info;

dladdr(JNI_OnLoad, &jni_on_load_info);

size_t jni_on_load_addr = (size_t)jni_on_load_info.dli_saddr;

Dl_info binder_init_info;

dladdr(binder_init, &binder_init_info);

size_t binder_init_addr = (size_t)binder_init_info.dli_saddr;

int diff_parse_link = ADDR_PARSE_LINK - ADDR_JNI_ONLOAD;

int diff_copy_jni_string_from_str = ADDR_COPY_JNI_STRING_FROM_STR - ADDR_JNI_ONLOAD;

size_t parse_link_addr = jni_on_load_addr + diff_parse_link;

size_t copy_jni_string_from_str_addr = jni_on_load_addr + diff_copy_jni_string_from_str;

printf("[i] parse_link_addr: %zX\n", parse_link_addr);

printf("[i] copy_jni_string_from_str_addr: %zX\n", copy_jni_string_from_str_addr);

void (*parse_link)(ParserResult *, String *) = (void (*)(ParserResult *, String *))(parse_link_addr);

void (*copy_jni_string_from_str)(String *, const char *) = (void (*)(String *, const char *))(copy_jni_string_from_str_addr);

if (parse_link != NULL && copy_jni_string_from_str != NULL)

{

Functions *functions = (Functions *)malloc(sizeof(Functions));

functions->parse_link = parse_link;

functions->copy_jni_string_from_str = copy_jni_string_from_str;

return functions;

}

[...]

И каж­дый раз при запус­ке harness я получал segmentation fault. Для пер­вично­го ана­лиза я сна­чала исполь­зовал strace. Мне уда­лось понять, что libicuBinder.so при вызове фун­кции uidna_nameToASCII_UTF8 выпол­няет ини­циали­зацию, но что‑то идет не так, в резуль­тате чего внут­ри вызыва­емой фун­кции про­исхо­дит вызов дру­гой фун­кции по адре­су 0. В ито­ге приш­лось ревер­сить биб­лиоте­ку libicuBinder.so, как и по час­ти ини­циали­зации, так и по час­ти вызова фун­кции uidna_nameToASCII_UTF8.

За­тем я сна­чала попытал­ся отла­дить эту биб­лиоте­ку в GDB, потом перешел в IDA (исполь­зовал IDA android_x64_server), так как к тому вре­мени уже вос­ста­новил часть фун­кций и было бы глу­по не исполь­зовать эту информа­цию при отладке.

В ито­ге я понял, в чем дело. Сна­чала в сис­теме Android выпол­няет­ся поиск биб­лиотек, в которых реали­зова­но ICU, затем про­исхо­дит поиск необ­ходимых сим­волов export фун­кций, что­бы их в даль­нейшем исполь­зовать (поэто­му биб­лиоте­ка и называ­ется Binder). Осно­ву для имен сим­волов биб­лиоте­ка берет изнутри, а вот допол­нение (вер­сия ICU) получа­ет с помощью Java-вызова. Имен­но по этой при­чине не мог­ли прог­рузить­ся необ­ходимые сим­волы фун­кций. Я добавил в harness модифи­кацию вер­сии в памяти без вызова Java-кода (для каж­дого system image при­ходит­ся менять вер­сию, что­бы все работа­ло, в будущем мож­но, конеч­но, авто­мати­зиро­вать про­цесс):

void set_icu_version(ptrdiff_t binder_init_addr)

{

ptrdiff_t diff = g_ICU_VERSION - ICU_SQLITE_ANDROID_BINDER__INIT;

ptrdiff_t version_addr = binder_init_addr + diff;

printf("[i] original ICU_VERSION = %X\n", *(uint32_t *)version_addr);

*(uint32_t *)version_addr = ICU_VERSION;

return;

}

Пос­ле запус­ка фаз­зера с обновлен­ным harness начали слу­чать­ся мно­гочис­ленные кра­ши. Ста­ло ясно, что опять что‑то идет не так. В этот раз фун­кция std::codecvt<InternT,ExternT,StateT>::do_in в биб­лиоте­ке liblinkparser.so кида­ет исклю­чение из‑за того, что не может соз­дать wide-стро­ку из бай­тов. Я уже не стал про­верять (рекомен­дую сде­лать это тебе, читатель), есть ли воз­можность у ата­кующе­го отправ­лять сырые бай­ты в виде сооб­щения или нет, а прос­то испра­вил фаз­зер, что­бы тот генери­ровал валид­ные дан­ные UTF-8.

 

Эксперименты и улучшения

В конеч­ном сче­те пок­рытие кода очень низ­кое, генера­ции новых вход­ных дан­ных прак­тичес­ки не про­исхо­дит. По этой при­чине я захотел снять трас­су исполне­ния для ана­лиза. Сде­лать это мож­но нес­коль­кими спо­соба­ми, но на повер­хнос­ти, как мне казалось, был спо­соб с помощью того же LibAFL. К сожале­нию, метод сня­тия пок­рытия с помощью Frida работа­ет толь­ко на архи­тек­туре AArch64:




$ ./frida_fuzzer --help

[...]

--drcov Enable DrCov (AArch64 only)

[...]



По­это­му мне приш­ла в голову идея запус­тить фаз­зер на плат­форме AArch64. Заод­но исполь­зую отдель­ное физичес­кое устрой­ство вмес­то эму­лято­ра.

И тут сно­ва посыпа­лись проб­лемы, начав­шиеся со сбор­ки фаз­зера. Приш­лось нем­ного поиг­рать с toolchain для AArch64, да и в целом с Android NDK, так как пос­ледние вер­сии не хотят работать с Rust. Вся­кие гряз­ные трю­ки и пат­чи не помог­ли, поэто­му я прос­то стал исполь­зовать ста­рую вер­сию NDK.

За­тем ста­ла воз­никать ошиб­ка при сбор­ке frida-gum-sys crate. Суть ошиб­ки сос­тоит в том, что для сбор­ки Frida Gum исполь­зуют­ся заголо­воч­ные фай­лы из сис­темы (x86_64 в моем слу­чае), что несов­мести­мо с AArch64 (проб­лема с pthread.h). Я испра­вил это, скло­ниро­вав репози­торий зависи­мос­ти (frida-rust целиком), и руками испра­вил файл build.rs, добавив дирек­тиву для исполь­зования sysroot из Android NDK. Это сра­бота­ло. Но появи­лась дру­гая проб­лема: в Android NDK не было необ­ходимо­го заголо­воч­ного фай­ла frida-gum.h, что, в прин­ципе, понят­но. Тог­да я сно­ва добавил дирек­тиву, в которой говори­лось, где мож­но взять этот файл.

diff --git a/frida-gum-sys/build.rs b/frida-gum-sys/build.rs

index 6afbb737..adcb2c02 100644

--- a/frida-gum-sys/build.rs

+++ b/frida-gum-sys/build.rs

@@ -65,9 +65,11 @@ fn main() {

bindings.clang_arg("-Iinclude")

} else {

bindings

+ bindings.clang_arg("-Iinclude")

};

let bindings = bindings

+ .clang_arg("--sysroot=./ndk/22.1.7171670/toolchains/llvm/prebuilt/linux-x86_64/sysroot/")

.header_contents("gum.h", "#include "frida-gum.h"")

.header("event_sink.h")

.header("invocation_listener.h")

Даль­ше воз­никла дру­гая проб­лема: новая вер­сия frida-gum прос­то не собира­ется — видимо, раз­работ­чик недав­но что‑то сло­мал или выш­ла новая вер­сия Frida, и API изме­нил­ся. Я починил и это: ошиб­ка была ста­рая, это был фикс какой‑то дру­гой ошиб­ки, из‑за чего на более новых вер­сиях Frida фун­кция перес­тала работать.

diff --git a/frida-gum-sys/src/lib.rs b/frida-gum-sys/src/lib.rs

index d689106a..f8d5cbed 100644

--- a/frida-gum-sys/src/lib.rs

+++ b/frida-gum-sys/src/lib.rs

@@ -16,10 +16,4 @@ mod bindings {

pub use bindings::*;

-#[cfg(not(any(

- target_os = "macos",

- target_os = "ios",

- target_os = "windows",

- target_os = "android"

-)))]

pub use _frida_g_object_unref as g_object_unref;

Сно­ва посыпа­лись ошиб­ки, толь­ко в дру­гом мес­те: кон­флик­ты зависи­мос­тей. Нес­коль­ко крей­тов исполь­зовали раз­ные вер­сии зависи­мос­тей. Выяс­нилось, что в libafl_frida при­меня­ется ста­рая вер­сия frida-gum и frida-gum-sys. Здесь я оста­новил­ся, потому что пос­ле обновле­ния вер­сий зависи­мос­ти всплы­ла куча оши­бок в libafl_frida, которые исправ­лять я уже не хотел, пос­коль­ку вре­мени на это не оста­валось. Сей­час я уже занима­юсь исправ­лени­ем и попыт­кой соб­рать libafl_frida для AArch64, поэто­му о при­мене­нии LibAFL + Frida на AArch64 архи­тек­туре рас­ска­жу в сле­дующий раз.

В ито­ге я решил пой­ти дру­гим путем — всле­пую без пок­рытия пытать­ся улуч­шить фаз­зер. Мутации и сырые вход­ные дан­ные, которые исполь­зуют­ся в фаз­зере, не под­ходят для нашей цели, так как у нас сто­ит про­вер­ка на валид­ную стро­ку UTF-8. Я решил перепи­сать фаз­зер с исполь­зовани­ем токенай­зера. Что­бы сде­лать это гра­мот­но, необ­ходимо вре­мя, которо­го у меня нет, поэто­му я реали­зовал при­митив­ный токенай­зер, как в работе Tartiflette: Snapshot fuzzing with KVM and libAFL.

В ито­ге фаз­зер стал вес­ти себя зна­читель­но луч­ше и выдавать ожи­даемый от него резуль­тат.

 

ВЫВОДЫ

Та­ким обра­зом мы прош­ли бес­хитрос­тный путь с самого начала и до потен­циаль­ного нахож­дения уяз­вимос­ти. Мож­но пов­торить этот путь самому, а мес­тами мно­гое улуч­шить, рас­полагая сво­бод­ным вре­менем. К тому же мно­жес­тво инте­рес­ных момен­тов в ана­лизе я оста­вил на потом, поэто­му, дорогой читатель, изу­чай и про­буй! На GitHub ты смо­жешь най­ти исходные коды фаз­зера и harness.

Кро­ме того, такой под­ход к поис­ку уяз­вимос­тей мож­но при­менить ко мно­гим при­ложе­ниям, которые в сво­ем сос­таве име­ют раз­деля­емые биб­лиоте­ки.

Читайте ещё больше платных статей бесплатно: https://t.me/xakep_1



Report Page