
Wprowadzenie
Pod koniec marca informowaliśmy Jakiś czas temu UC Browser był reklamowany i dystrybuowany w bardzo agresywny sposób: instalowano go na urządzeniach użytkowników za pomocą złośliwego oprogramowania, rozpowszechniano z różnych stron pod przykrywką plików wideo (tj. użytkownicy myśleli, że ściągają, na przykład, film pornograficzny, a otrzymywali w zamian APK z przeglądarką), używano przerażających banerów z komunikatami, że przeglądarka jest przestarzała, podatna na ataki i tym podobne. W oficjalnej grupie UC Browser w VK istnieje
, w której użytkownicy mogą zgłaszać nieuczciwą reklamę, jest tam wiele przykładów. W 2016 roku była nawet reklama wideo W momencie pisania tego artykułu UC Browser zebrał ponad 500 000 000 instalacji w Google Play. To imponujące — tylko Google Chrome ma więcej. Wśród recenzji można zobaczyć sporo skarg na reklamy i przekierowania do różnych aplikacji w Google Play. To skłoniło nas do zbadania: postanowiliśmy sprawdzić, czy UC Browser robi coś niedobrego. I okazało się, że tak!
W kodzie aplikacji odkryliśmy możliwość ładowania i uruchamiania kodu wykonywalnego,
co narusza zasady publikacji aplikacji Wszystko, co jest napisane poniżej, dotyczy wersji UC Browser, która była dostępna w Google Play w momencie przeprowadzania badania:
package: com.UCMobile.intl versionName: 12.10.8.1172 versionCode: 10598 sha1 pliku APK: f5edb2243413c777172f6362876041eb0c3a928c
Wektor atakuW manifeście UC Browser można znaleźć usługę o mówiącej nazwie
com.uc.deployment.UpgradeDeployService <service android_exported="false" android_name="com.uc.deployment.UpgradeDeployService" android_process=":deploy" \/>.
Podczas uruchamiania tej usługi przeglądarka wysyła żądanie POST do
puds.ucweb.com\/upgrade\/index.xhtml , który można zauważyć w ruchu po pewnym czasie od jego rozpoczęcia. W odpowiedzi może otrzymać polecenie załadowania jakiejś aktualizacji lub nowego modułu. W trakcie analizy serwer takich poleceń nie wydawał, ale zauważyliśmy, że próbując otworzyć PDF w przeglądarce, wysyła ponowne zapytanie pod wskazany powyżej adres, po czym pobiera natywną bibliotekę. Aby przeprowadzić atak, postanowiliśmy wykorzystać tę cechę UC Browser: zdolność do otwierania PDF za pomocą natywnej biblioteki, której nie ma w APK i którą on w razie potrzeby pobiera z Internetu. Należy zaznaczyć, że teoretycznie UC Browser można zmusić do pobrania czegoś bez interakcji z użytkownikiem – jeśli dostarczy się odpowiednio sformułowaną odpowiedź na żądanie, które wykonuje się po uruchomieniu przeglądarki. Jednak do tego potrzeba bardziej szczegółowego zbadania protokołu interakcji z serwerem, dlatego postanowiliśmy, że łatwiej jest edytować przechwyconą odpowiedź i zastąpić bibliotekę do obsługi PDF.
Zatem, gdy użytkownik chce otworzyć PDF bezpośrednio w przeglądarce, w ruchu można zobaczyć następujące zapytania:

Najpierw następuje POST-zapytanie do , po czym
pobierany jest archiwum z biblioteką do przeglądania PDF i formatów biurowych. Logicznym wnioskiem jest, że w pierwszym zapytaniu przekazywane są informacje o systemie (przynajmniej architektura, aby wydać odpowiednią bibliotekę), a w odpowiedzi przeglądarka otrzymuje pewne informacje o bibliotece, którą należy pobrać: adres i być może coś jeszcze. Problem w tym, że to zapytanie jest zaszyfrowane.
Fragment zapytania
Fragment odpowiedzi


Sama biblioteka jest spakowana w ZIP i nie jest zaszyfrowana.

Szukanie kodu deszyfrującego ruch
Spróbujemy zdeszyfrować odpowiedź serwera. Sprawdzamy kod klasy <service android_exported="false" android_name="com.uc.deployment.UpgradeDeployService" android_process=":deploy" \/>: z metody onStartCommand przechodzimy do com.uc.deployment.b.x, a z niego do com.uc.browser.core.d.c.f.e:
public final void e(l arg9) {
int v4_5;
String v3_1;
byte[] v3;
byte[] v1 = null;
if(arg9 == null) {
v3 = v1;
}
else {
v3_1 = arg9.iGX.ipR;
StringBuilder v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]produkt:");
v4.append(arg9.iGX.ipR);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]wersja:");
v4.append(arg9.iGX.iEn);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]typ_aktualizacji:");
v4.append(arg9.iGX.mMode);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]flaga_wymuszenia:");
v4.append(arg9.iGX.iEo);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]tryb_cichy:");
v4.append(arg9.iGX.iDQ);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]typ_cichy:");
v4.append(arg9.iGX.iEr);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]stan_cichy:");
v4.append(arg9.iGX.iEp);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]plik_cichy:");
v4.append(arg9.iGX.iEq);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apk_md5:");
v4.append(arg9.iGX.iEl);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]typ_pobrania:");
v4.append(arg9.mDownloadType);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]grupa_pobierania:");
v4.append(arg9.mDownloadGroup);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]ścieżka_pobrania:");
v4.append(arg9.iGH);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]wersja_dziecka_apollo:");
v4.append(arg9.iGX.iEx);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]seria_apollo:");
v4.append(arg9.iGX.iEw);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]architektura_cpu_apollo:");
v4.append(arg9.iGX.iEt);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]vfp3_cpu_apollo:");
v4.append(arg9.iGX.iEv);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]vfp_cpu_apollo:");
v4.append(arg9.iGX.iEu);
ArrayList v3_2 = arg9.iGX.iEz;
if(v3_2 != null && v3_2.size() != 0) {
Iterator v3_3 = v3_2.iterator();
while(v3_3.hasNext()) {
Object v4_1 = v3_3.next();
StringBuilder v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]nazwa_komponentu:");
v5.append(((au)v4_1).getName());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]nazwa_wersji_komponentu:");
v5.append(((au)v4_1).aDA());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]kod_wersji_komponentu:");
v5.append(((au)v4_1).gBl);
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]typ_wymagań_komponentu:");
v5.append(((au)v4_1).gBq);
}
}
j v3_4 = new j();
m.b(v3_4);
h v4_2 = new h();
m.b(v4_2);
ay v5_1 = new ay();
v3_4.hS("");
v3_4.setImsi("");
v3_4.hV("");
v5_1.bPQ = v3_4;
v5_1.bPP = v4_2;
v5_1.yr(arg9.iGX.ipR);
v5_1.gBF = arg9.iGX.mMode;
v5_1.gBI = arg9.iGX.iEz;
v3_2 = v5_1.gAr;
c.aBh();
v3_2.add(g.fs("wersja_os", c.getRomInfo()));
v3_2.add(g.fs("architektura_procesora", com.uc.b.a.a.c.getCpuArch()));
v3_2.add(g.fs("architektura_cpu", com.uc.b.a.a.c.Pb()));
String v4_3 = com.uc.b.a.a.c.Pd();
v3_2.add(g.fs("vfp_cpu", v4_3));
v3_2.add(g.fs("typ_sieci", String.valueOf(com.uc.base.system.a.Jo())));
v3_2.add(g.fs("odhosta", arg9.iGX.iEm));
v3_2.add(g.fs("wersja_pluginu", arg9.iGX.iEn));
v3_2.add(g.fs("target_lang", arg9.iGX.iEs));
v3_2.add(g.fs("architektura_vitamio", arg9.iGX.iEt));
v3_2.add(g.fs("vfp_vitamio", arg9.iGX.iEu));
v3_2.add(g.fs("vfp3_vitamio", arg9.iGX.iEv));
v3_2.add(g.fs("wersja_dziecka_pluginu", arg9.iGX.iEx));
v3_2.add(g.fs("seria_wersji", arg9.iGX.iEw));
v3_2.add(g.fs("wersja_dziecka", r.aVw()));
v3_2.add(g.fs("cur_ver_md5", arg9.iGX.iEl));
v3_2.add(g.fs("cur_ver_signature", SystemHelper.getUCMSignature()));
v3_2.add(g.fs("dziennik_aktualizacji", i.bjt()));
v3_2.add(g.fs("cicha_instalacja", String.valueOf(arg9.iGX.iDQ)));
v3_2.add(g.fs("stan_cichy", String.valueOf(arg9.iGX.iEp)));
v3_2.add(g.fs("plik_cichy", arg9.iGX.iEq));
v3_2.add(g.fs("typ_cichy", String.valueOf(arg9.iGX.iEr)));
v3_2.add(g.fs("architektura_cpu", com.uc.b.a.a.c.Pc()));
v3_2.add(g.fs("zbiór_cpu", SystemHelper.getCpuInstruction()));
boolean v4_4 = v4_3 == null || !v4_3.contains("neon") ? false : true;
v3_2.add(g.fs("neon", String.valueOf(v4_4)));
v3_2.add(g.fs("rdzenie_cpu", String.valueOf(com.uc.b.a.a.c.Jl())));
v3_2.add(g.fs("ram_1", String.valueOf(com.uc.b.a.a.h.Po())));
v3_2.add(g.fs("całkowity_ram", String.valueOf(com.uc.b.a.a.h.OL())));
c.aBh();
v3_2.add(g.fs("rom_1", c.getRomInfo()));
v4_5 = e.getScreenWidth();
int v6 = e.getScreenHeight();
StringBuilder v7 = new StringBuilder();
v7.append(v4_5);
v7.append("*");
v7.append(v6);
v3_2.add(g.fs("ss", v7.toString()));
v3_2.add(g.fs("poziom_api", String.valueOf(Build$VERSION.SDK_INT)));
v3_2.add(g.fs("lista_apk_uc", SystemHelper.getUCMobileApks()));
Iterator v4_6 = arg9.iGX.iEA.entrySet().iterator();
while(v4_6.hasNext()) {
Object v6_1 = v4_6.next();
v3_2.add(g.fs(((Map$Entry)v6_1).getKey(), ((Map$Entry)v6_1).getValue()));
}
v3 = v5_1.toByteArray();
}
if(v3 == null) {
this.iGY.iGI.a(arg9, "up_encode", "yes", "fail");
return;
}
v4_5 = this.iGY.iGw ? 0x1F : 0;
if(v3 == null) {
}
else {
v3 = g.i(v4_5, v3);
if(v3 == null) {
}
else {
v1 = new byte[v3.length + 16];
byte[] v6_2 = new byte[16];
Arrays.fill(v6_2, 0);
v6_2[0] = 0x5F;
v6_2[1] = 0;
v6_2[2] = ((byte)v4_5);
v6_2[3] = -50;
System.arraycopy(v6_2, 0, v1, 0, 16);
System.arraycopy(v3, 0, v1, 16, v3.length);
}
}
if(v1 == null) {
this.iGY.iGI.a(arg9, "up_encrypt", "yes", "fail");
return;
}
if(TextUtils.isEmpty(this.iGY.mUpgradeUrl)) {
this.iGY.iGI.a(arg9, "up_url", "yes", "fail");
return;
}
StringBuilder v0 = new StringBuilder("[");
v0.append(arg9.iGX.ipR);
v0.append("]url:");
v0.append(this.iGY.mUpgradeUrl);
com.uc.browser.core.d.c.i v0_1 = this.iGY.iGI;
v3_1 = this.iGY.mUpgradeUrl;
com.uc.base.net.e v0_2 = new com.uc.base.net.e(new com.uc.browser.core.d.c.i$a(v0_1, arg9));
v3_1 = v3_1.contains("?") ? v3_1 + "&dataver=pb" : v3_1 + "?dataver=pb";
n v3_5 = v0_2.uc(v3_1);
m.b(v3_5, false);
v3_5.setMethod("POST");
v3_5.setBodyProvider(v1);
v0_2.b(v3_5);
this.iGY.iGI.a(arg9, "up_null", "yes", "success");
this.iGY.iGI.b(arg9);
}Widoczne jest tutaj tworzenie żądania POST. Zwracamy uwagę na tworzenie tablicy o rozmiarze 16 bajtów i jej wypełnienie: 0x5F, 0, 0x1F, -50 (=0xCE). Zgadza się to z tym, co widzieliśmy w powyższym żądaniu.
W tej samej klasie można zauważyć zagnieżdżoną klasę, w której znajduje się inna interesująca metoda:
public final void a(l arg10, byte[] arg11) {
f v0 = this.iGQ;
StringBuilder v1 = new StringBuilder("[");
v1.append(arg10.iGX.ipR);
v1.append("]:UpgradeSuccess");
byte[] v1_1 = null;
if(arg11 == null) {
}
else if(arg11.length < 16) {
}
else {
if(arg11[0] != 0x60 && arg11[3] != 0xFFFFFFD0) {
goto label_57;
}
int v3 = 1;
int v5 = arg11[1] == 1 ? 1 : 0;
if(arg11[2] != 1 && arg11[2] != 11) {
if(arg11[2] == 0x1F) {
}
else {
v3 = 0;
}
}
byte[] v7 = new byte[arg11.length - 16];
System.arraycopy(arg11, 16, v7, 0, v7.length);
if(v3 != 0) {
v7 = g.j(arg11[2], v7);
}
if(v7 == null) {
goto label_57;
}
if(v5 != 0) {
v1_1 = g.P(v7);
goto label_57;
}
v1_1 = v7;
}
label_57:
if(v1_1 == null) {
v0.iGY.iGI.a(arg10, "up_decrypt", "yes", "fail");
return;
}
q v11 = g.b(arg10, v1_1);
if(v11 == null) {
v0.iGY.iGI.a(arg10, "up_decode", "yes", "fail");
return;
}
if(v0.iGY.iGt) {
v0.d(arg10);
}
if(v0.iGY.iGo != null) {
v0.iGY.iGo.a(0, ((o)v11));
}
if(v0.iGY.iGs) {
v0.iGY.a(((o)v11));
v0.iGY.iGI.a(v11, "up_silent", "yes", "success");
v0.iGY.iGI.a(v11);
return;
}
v0.iGY.iGI.a(v11, "up_silent", "no", "success");
}
} Metoda przyjmuje tablicę bajtów i sprawdza, czy zerowy bajt jest równy 0x60, trzeci bajt jest równy 0xD0, a drugi bajt — 1, 11 lub 0x1F. Sprawdzamy odpowiedź od serwera: zerowy bajt — 0x60, drugi — 0x1F, trzeci — 0x60. Wygląda na to, że tego potrzebujemy. Sądząc po wierszach (na przykład „up_decrypt”), powinien być tutaj wywoływany sposób, który odszyfruje odpowiedź serwera.
Przechodzimy do metody g.j. Zauważamy, że jako pierwszy argument przekazywany jest bajt o przesunięciu 2 (tzn. 0x1F w naszym przypadku), a jako drugi — odpowiedź serwera bez
pierwszych 16 bajtów.
public static byte[] j(int arg1, byte[] arg2) {
if(arg1 == 1) {
arg2 = c.c(arg2, c.adu);
}
else if(arg1 == 11) {
arg2 = m.aF(arg2);
}
else if(arg1 != 0x1F) {
}
else {
arg2 = EncryptHelper.decrypt(arg2);
}
return arg2;
} Oczywiście, tutaj następuje wybór algorytmu deszyfrowania, a ten bajt, który w naszym przypadku
w przypadku wynosi 0x1F, oznacza jeden z trzech możliwych wariantów.
Kontynuujemy analizę kodu. Po kilku skokach trafiamy do metody o mówiącej nazwie decryptBytesByKey.
Tutaj od naszej odpowiedzi oddziela się jeszcze dwa bajty, z których otrzymujemy ciąg. Zrozumiałe jest, że w ten sposób wybierany jest klucz do odszyfrowania wiadomości.
private static byte[] decryptBytesByKey(byte[] bytes) {
byte[] v0 = null;
if(bytes != null) {
try {
if(bytes.length < EncryptHelper.PREFIX_BYTES_SIZE) {
}
else if(bytes.length == EncryptHelper.PREFIX_BYTES_SIZE) {
return v0;
}
else {
byte[] prefix = new byte[EncryptHelper.PREFIX_BYTES_SIZE]; // 2 bajty
System.arraycopy(bytes, 0, prefix, 0, prefix.length);
String keyId = c.ayR().d(ByteBuffer.wrap(prefix).getShort()); // Wybór klucza
if(keyId == null) {
return v0;
}
else {
a v2 = EncryptHelper.ayL();
if(v2 == null) {
return v0;
}
else {
byte[] enrypted = new byte[bytes.length - EncryptHelper.PREFIX_BYTES_SIZE];
System.arraycopy(bytes, EncryptHelper.PREFIX_BYTES_SIZE, enrypted, 0, enrypted.length);
return v2.l(keyId, enrypted);
}
}
}
}
catch(SecException v7_1) {
EncryptHelper.handleDecryptException(((Throwable)v7_1), v7_1.getErrorCode());
return v0;
}
catch(Throwable v7) {
EncryptHelper.handleDecryptException(v7, 2);
return v0;
}
}
return v0;
}Wyprzedzając fakty, zaznaczamy, że na tym etapie nie otrzymujemy jeszcze klucza, a jedynie jego „identyfikator”. Uzyskanie klucza jest nieco bardziej skomplikowane.
W następnej metodzie do istniejących parametrów dodawane są jeszcze dwa, co daje ich łącznie cztery: magiczna liczba 16, identyfikator klucza, zaszyfrowane dane oraz niejasny ciąg (w naszym przypadku pusty).
public final byte[] l(String keyId, byte[] encrypted) throws SecException {
return this.ayJ().staticBinarySafeDecryptNoB64(16, keyId, encrypted, "");
}Po serii przejść docieramy do metody staticBinarySafeDecryptNoB64 interfejsu com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. W głównym kodzie aplikacji nie ma klas implementujących ten interfejs. Taka klasa znajduje się w pliku lib/armeabi-v7a/libsgmain.so, który w rzeczywistości nie jest .so, lecz .jar. Interesująca nas metoda została zrealizowana w następujący sposób:
pakiet com.alibaba.wireless.security.a.i;
// ...
public class a implements IStaticDataEncryptComponent {
private ISecurityGuardPlugin a;
// ...
private byte[] a(int mode, int magicInt, int xzInt, String keyId, byte[] encrypted, String magicString) {
return this.a.getRouter().doCommand(10601, new Object[]{Integer.valueOf(mode), Integer.valueOf(magicInt), Integer.valueOf(xzInt), keyId, encrypted, magicString});
}
// ...
private byte[] b(int magicInt, String keyId, byte[] encrypted, String magicString) {
return this.a(2, magicInt, 0, keyId, encrypted, magicString);
}
// ...
public byte[] staticBinarySafeDecryptNoB64(int magicInt, String keyId, byte[] encrypted, String magicString) throws SecException {
if(keyId != null && keyId.length() > 0 && magicInt >= 0 && magicInt 0) {
return this.b(magicInt, keyId, encrypted, magicString);
}
throw new SecException("", 301);
}
//...
} Tutaj nasza lista parametrów jest uzupełniana o dwie liczby całkowite: 2 i 0. Z tego, co widać,
2 oznacza odszyfrowanie, jak w metodzie doFinal systemowej klasy javax.crypto.Cipher. I wszystko to jest przekazywane do jakiegoś Routera z liczbą 10601 — to prawdopodobnie numer polecenia.
Po kolejnej serii przejść znajdujemy klasę, która implementuje interfejs IRouterComponent i metodę doCommand:
pakiet com.alibaba.wireless.security.mainplugin;
import com.alibaba.wireless.security.framework.IRouterComponent;
import com.taobao.wireless.security.adapter.JNICLibrary;
public class a implements IRouterComponent {
public a() {
super();
}
public Object doCommand(int arg2, Object[] arg3) {
return JNICLibrary.doCommandNative(arg2, arg3);
}
}A także klasa JNICLibrary, w której zadeklarowana jest natywna metoda doCommandNative:
pakiet com.taobao.wireless.security.adapter;
public class JNICLibrary {
public static native Object doCommandNative(int arg0, Object[] arg1);
}W związku z tym musimy w natywnym kodzie znaleźć metodę doCommandNative. I tu zaczyna się prawdziwa zabawa.
Obfuskacja kodu maszynowego
W pliku libsgmain.so (który w rzeczywistości jest .jar i w którym nieco wyżej znaleźliśmy implementację niektórych interfejsów związanych z szyfrowaniem) jest jedna natywna biblioteka: libsgmainso-6.4.36.so. Otwieramy ją w IDA i otrzymujemy masę okien dialogowych z błędami. Problem w tym, że tabela sekcji (section header table) - jest nieważna. Zrobiono to specjalnie, aby utrudnić analizę.

Ale nie jest potrzebna: aby poprawnie załadować plik ELF i go przeanalizować, wystarczy tabela segmentów (program header table). Dlatego po prostu usuwamy tabelę sekcji, zerując odpowiednie pola w nagłówku.

Znowu otwieramy plik w IDA.
Istnieją dwa sposoby, aby poinformować wirtualną maszynę Java, gdzie dokładnie w natywnej bibliotece znajduje się implementacja metody zadeklarowanej w kodzie Java jako natywnej. Pierwszy - nadać mu nazwę w postaci Java_nazwa_pakietu_NazwaKlasy_nazwaMetody.
Po drugie — zarejestrować go podczas ładowania biblioteki (w funkcji JNI_OnLoad)
za pomocą wywołania funkcji RegisterNatives.
W naszym przypadku, jeśli użyjemy pierwszego sposobu, nazwa powinna być następująca: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.
Wśród eksportowanych funkcji takiej nie ma, więc trzeba szukać wywołania RegisterNatives.
Przechodzimy do funkcji JNI_OnLoad i widzimy taki obraz:

Co tu się dzieje? Na pierwszy rzut oka, początek i koniec funkcji są typowe dla architektury ARM. Pierwszą instrukcją na stosie zapisywane jest zawartość rejestrów, które funkcja będzie używać w swojej pracy (w tym przypadku R0, R1 i R2), a także zawartość rejestru LR, w którym znajduje się adres powrotu z funkcji. Ostatnią instrukcją przywracane są zapisane rejestry, przy czym adres powrotu natychmiast umieszczany jest w rejestrze PC — w ten sposób następuje powrót z funkcji. Ale jeśli się przyjrzeć, można zauważyć, że przedostatnia instrukcja zmienia adres powrotu, zapisany na stosie. Policzmy, jaki on będzie po
wykonaniu kodu. W R1 ładowany jest pewien adres 0xB130, odejmowane jest 5, następnie jest on przenoszony do R0 i dodawana jest do niego wartość 0x10. Otrzymujemy 0xB13B. W ten sposób IDA myśli, że w ostatniej instrukcji następuje zwykły powrót z funkcji, podczas gdy tak naprawdę następuje przejście do obliczonego adresu 0xB13B.
Warto przypomnieć, że procesory ARM mają dwa tryby i dwa zbiory instrukcji: ARM i Thumb. Młodszy bit adresu mówi procesorowi, który zbiór instrukcji jest używany. Tzn. adres w rzeczywistości to 0xB13A, a jedynka w młodszym bicie oznacza tryb Thumb.
Na początku każdej funkcji w tej bibliotece dodany jest podobny 'adapter' oraz
śmieciowy kod. Dalej nie będziemy się na nich szczegółowo zatrzymywać – po prostu pamiętajmy,
że prawdziwy początek prawie wszystkich funkcji znajduje się trochę dalej.
Ponieważ w kodzie nie ma wyraźnego przejścia do 0xB13A, IDA sama nie rozpoznała, że w tym miejscu znajduje się kod. Z tego samego powodu dużą część kodu w bibliotece nie rozpoznaje jako kod, co trochę utrudnia analizę. Mówimy IDA, że tutaj jest kod, i oto co się dzieje:

Na 0xB144 wyraźnie zaczyna się tabela. A co w sub_494C?

Przy wywołaniu tej funkcji w rejestrze LR otrzymamy adres wspomnianej wcześniej tabeli (0xB144). W R0 — indeks w tej tabeli. Tzn. brać wartość z tabeli, dodawać do LR i uzyskiwać.
adres, do którego należy przejść. Spróbujemy go obliczyć: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Przechodzimy pod uzyskany adres i widzimy dosłownie kilka przydatnych instrukcji i znowu przejście do 0xB140:

Teraz będzie przejście ze przesunięciem z indeksem 0x20 z tabeli.
Sądząc po rozmiarze tabeli, takich przejść w kodzie spotkamy wiele. Pojawia się pytanie, czy można jakoś z tym walczyć bardziej zautomatyzowanym sposobem, bez ręcznego obliczania adresów. I na pomoc przychodzą nam skrypty i możliwość patchowania kodu w IDA:
def put_unconditional_branch(source, destination):
offset = (destination - source - 4) >> 1
if offset > 2097151 or offset 1023 or offset > 11) & 0x7ff)
instruction2 = 0xb800 | (offset & 0x7ff)
patch_word(source, instruction1)
patch_word(source + 2, instruction2)
else:
instruction = 0xe000 | (offset & 0x7ff)
patch_word(source, instruction)
ea = here()
if get_wide_word(ea) == 0xb503: #PUSH {R0,R1,LR}
ea1 = ea + 2
if get_wide_word(ea1) == 0xbf00: #NOP
ea1 += 2
if get_operand_type(ea1, 0) == 1 and get_operand_value(ea1, 0) == 0 and get_operand_type(ea1, 1) == 2:
index = get_wide_dword(get_operand_value(ea1, 1))
print "index =", hex(index)
ea1 += 2
if get_operand_type(ea1, 0) == 7:
table = get_operand_value(ea1, 0) + 4
elif get_operand_type(ea1, 1) == 2:
table = get_operand_value(ea1, 1) + 4
else:
print "Wrong operand type on", hex(ea1), "-", get_operand_type(ea1, 0), get_operand_type(ea1, 1)
table = None
if table is None:
print "Unable to find table"
else:
print "table =", hex(table)
offset = get_wide_dword(table + (index << 2))
put_unconditional_branch(ea, table + offset)
else:
print "Unknown code", get_operand_type(ea1, 0), get_operand_value(ea1, 0), get_operand_type(ea1, 1) == 2
else:
print "Unable to detect first instruction"Umieszczamy kursor na linii 0xB26A, uruchamiamy skrypt i widzimy przejście do 0xB4B0:

IDA znowu nie rozpoznała tego fragmentu jako kod. Pomagamy jej i widzimy tam inną konstrukcję:

Instrukcje po BLX wyglądają dość bezsensownie, bardziej przypominają jakieś przesunięcie. Sprawdzamy w sub_4964:

I rzeczywiście, tutaj brany jest dword pod adresem, który leży w LR, dodawany jest do tego adres, a następnie brana jest wartość z uzyskanego adresu i kładzie się ją na stosie. LR jest również zwiększane o 4, aby po powrocie z funkcji przeskoczyć to przesunięcie. Następnie instrukcja POP {R1} odbiera uzyskaną wartość ze stosu. Jeśli spojrzymy, co znajduje się pod adresem 0xB4BA + 0xEA = 0xB5A4, można zobaczyć coś, co wygląda jak tabela adresów:

Aby zastosować tę łatkę, będziesz potrzebować dwóch parametrów z kodu: przesunięcia oraz numeru rejestru, w którym należy umieścić wynik. Dla każdego możliwego rejestru należy wcześniej przygotować fragment kodu.
patches = {}
patches[0] = (0x00, 0xbf, 0x01, 0x48, 0x00, 0x68, 0x02, 0xe0)
patches[1] = (0x00, 0xbf, 0x01, 0x49, 0x09, 0x68, 0x02, 0xe0)
patches[2] = (0x00, 0xbf, 0x01, 0x4a, 0x12, 0x68, 0x02, 0xe0)
patches[3] = (0x00, 0xbf, 0x01, 0x4b, 0x1b, 0x68, 0x02, 0xe0)
patches[4] = (0x00, 0xbf, 0x01, 0x4c, 0x24, 0x68, 0x02, 0xe0)
patches[5] = (0x00, 0xbf, 0x01, 0x4d, 0x2d, 0x68, 0x02, 0xe0)
patches[8] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0x80, 0xd8, 0xf8, 0x00, 0x80, 0x01, 0xe0)
patches[9] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0x90, 0xd9, 0xf8, 0x00, 0x90, 0x01, 0xe0)
patches[10] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0xa0, 0xda, 0xf8, 0x00, 0xa0, 0x01, 0xe0)
patches[11] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0xb0, 0xdb, 0xf8, 0x00, 0xb0, 0x01, 0xe0)
ea = here()
if (get_wide_word(ea) == 0xb082 #SUB SP, SP, #8
and get_wide_word(ea + 2) == 0xb503): #PUSH {R0,R1,LR}
if get_operand_type(ea + 4, 0) == 7:
pop = get_bytes(ea + 12, 4, 0)
if pop[1] == 'xbc':
register = -1
r = get_wide_byte(ea + 12)
for i in range(8):
if r == (1 <> 4
if register in patches:
address = get_wide_dword(ea + 8) + ea + 8
for b in patches[register]:
patch_byte(ea, b)
ea += 1
patch_dword(ea, address)
else:
print "Instrukcja POP nie znaleziona"
else:
print "Błędny typ operandu na +4:", get_operand_type(ea + 4, 0)
else:
print "Nie można wykryć pierwszych instrukcji"Umieszczamy kursor na początku fragmentu, który chcemy zastąpić — 0xB4B2 — i uruchamiamy skrypt:

Oprócz wspomnianych wcześniej fragmentów w kodzie spotykamy też takie:

Jak w poprzednim przypadku, po instrukcji BLX występuje przesunięcie:

Zabieramy przesunięcie z adresu w LR, dodajemy je do LR i przechodzimy tam. 0x72044 + 0xC = 0x72050. Skrypt dla tego fragmentu jest całkiem prosty:
def put_unconditional_branch(source, destination):
offset = (destination - source - 4) >> 1
if offset > 2097151 or offset 1023 or offset > 11) & 0x7ff)
instruction2 = 0xb800 | (offset & 0x7ff)
patch_word(source, instruction1)
patch_word(source + 2, instruction2)
else:
instruction = 0xe000 | (offset & 0x7ff)
patch_word(source, instruction)
ea = here()
if get_wide_word(ea) == 0xb503: #PUSH {R0,R1,LR}
ea1 = ea + 6
if get_wide_word(ea + 2) == 0xbf00: #NOP
ea1 += 2
offset = get_wide_dword(ea1)
put_unconditional_branch(ea, (ea1 + offset) & 0xffffffff)
else:
print "Nie można wykryć pierwszej instrukcji"Wynik wykonania skryptu:

Po załataniu funkcji można wskazać IDA na jej rzeczywisty początek. Zbiera ona cały kod funkcji kawałek po kawałku, który można następnie zdekompilować za pomocą HexRays.
Deszyfrowanie łańcuchów
Nauczyliśmy się walczyć z obfuskacją kodu maszynowego w bibliotece libsgmainso-6.4.36.so z UC Browser i otrzymaliśmy kod funkcji JNI_OnLoad.
int __fastcall real_JNI_OnLoad(JavaVM *vm)
{
int result; // r0
jclass clazz; // r0 MAPDST
int v4; // r0
JNIEnv *env; // r4
int v6; // [sp-40h] [bp-5Ch]
int v7; // [sp+Ch] [bp-10h]
v7 = *(_DWORD *)off_8AC00;
if ( !vm )
goto LABEL_39;
sub_7C4F4();
env = (JNIEnv *)sub_7C5B0(0);
if ( !env )
goto LABEL_39;
v4 = sub_72CCC();
sub_73634(v4);
sub_73E24(&unk_83EA6, &v6, 49);
clazz = (jclass)((int (__fastcall *)(JNIEnv *, int *))(*env)->FindClass)(env, &v6);
if ( clazz
&& (sub_9EE4(),
sub_71D68(env),
sub_E7DC(env) >= 0
&& sub_69D68(env) >= 0
&& sub_197B4(env, clazz) >= 0
&& sub_E240(env, clazz) >= 0
&& sub_B8B0(env, clazz) >= 0
&& sub_5F0F4(env, clazz) >= 0
&& sub_70640(env, clazz) >= 0
&& sub_11F3C(env) >= 0
&& sub_21C3C(env, clazz) >= 0
&& sub_2148C(env, clazz) >= 0
&& sub_210E0(env, clazz) >= 0
&& sub_41B58(env, clazz) >= 0
&& sub_27920(env, clazz) >= 0
&& sub_293E8(env, clazz) >= 0
&& sub_208F4(env, clazz) >= 0) )
{
result = (sub_B7B0(env, clazz) >> 31) | 0x10004;
}
else
{
LABEL_39:
result = -1;
}
return result;
}Przyjrzyjmy się bliżej następującym liniom:
sub_73E24(&unk_83EA6, &v6, 49);
clazz = (jclass)((int (__fastcall *)(JNIEnv *, int *))(*env)->FindClass)(env, &v6);W funkcji sub_73E24 wyraźnie odbywa się deszyfrowanie nazwy klasy. Jako parametry tej funkcji przekazywany jest wskaźnik do danych, przypominających zaszyfrowane, pewien bufor oraz liczba. Oczywiste jest, że po wywołaniu funkcji w buforze znajdzie się odszyfrowany łańcuch, ponieważ jest on przekazywany funkcji FindClass, która jako drugi parametr przyjmuje nazwę klasy. Oznacza to, że liczba to rozmiar bufora lub długość łańcucha. Spróbujmy odszyfrować nazwę klasy, powinna wskazać, czy zmierzamy w dobrym kierunku. Przyjrzyjmy się bliżej, co dzieje się w sub_73E24.
int __fastcall sub_73E56(unsigned __int8 *in, unsigned __int8 *out, size_t size)
{
int v4; // r6
int v7; // r11
int v8; // r9
int v9; // r4
size_t v10; // r5
int v11; // r0
struc_1 v13; // [sp+0h] [bp-30h]
int v14; // [sp+1Ch] [bp-14h]
int v15; // [sp+20h] [bp-10h]
v4 = 0;
v15 = *(_DWORD *)off_8AC00;
v14 = 0;
v7 = sub_7AF78(17);
v8 = sub_7AF78(size);
if ( !v7 )
{
v9 = 0;
goto LABEL_12;
}
(*(void (__fastcall **)(int, const char *, int))(v7 + 12))(v7, "DcO/lcK+h?m3c*q@", 16);
if ( !v8 )
{
LABEL_9:
v4 = 0;
goto LABEL_10;
}
v4 = 0;
if ( !in )
{
LABEL_10:
v9 = 0;
goto LABEL_11;
}
v9 = 0;
if ( out )
{
memset(out, 0, size);
v10 = size - 1;
(*(void (__fastcall **)(int, unsigned __int8 *, size_t))(v8 + 12))(v8, in, v10);
memset(&v13, 0, 0x14u);
v13.field_4 = 3;
v13.field_10 = v7;
v13.field_14 = v8;
v11 = sub_6115C(&v13, &v14);
v9 = v11;
if ( v11 )
{
if ( *(_DWORD *)(v11 + 4) == v10 )
{
qmemcpy(out, *(const void **)v11, v10);
v4 = *(_DWORD *)(v9 + 4);
}
else
{
v4 = 0;
}
goto LABEL_11;
}
goto LABEL_9;
}
LABEL_11:
sub_7B148(v7);
LABEL_12:
if ( v8 )
sub_7B148(v8);
if ( v9 )
sub_7B148(v9);
return v4;
}veth_xdp_flush_bq() sub_7AF78 tworzy instancję kontenera dla tablic bajtów o określonym rozmiarze (nie będziemy się szczegółowo zatrzymywać nad tymi kontenerami). Tworzymy tutaj dwa takie kontenery: jeden do przechowywania ciągu „DcO/lcK+h?m3c*q@” (nie trudno zgadnąć, że to klucz), a drugi — zaszyfrowane dane. Następnie oba obiekty są umieszczane w pewnej strukturze, która jest przekazywana do funkcji sub_6115C. Zauważamy także, że w tej strukturze znajduje się pole o wartości 3. Zobaczmy, co się z tą strukturą dzieje dalej.
int __fastcall sub_611B4(struc_1 *a1, _DWORD *a2)
{
int v3; // lr
unsigned int v4; // r1
int v5; // r0
int v6; // r1
int result; // r0
int v8; // r0
*a2 = 820000;
if ( a1 )
{
v3 = a1->field_14;
if ( v3 )
{
v4 = a1->field_4;
if ( v4 field_0, a1->field_10, v3);
goto LABEL_17;
case 3u:
v8 = sub_6364C(a1->field_0, a1->field_10, v3);
goto LABEL_17;
case 0x10u:
case 0x11u:
case 0x12u:
v8 = sub_612F4(
a1->field_0,
v4,
*(_QWORD *)&a1->field_8,
*(_QWORD *)&a1->field_8 >> 32,
a1->field_10,
v3,
a2);
goto LABEL_17;
case 0x14u:
v8 = sub_63A28(a1->field_0, v3);
goto LABEL_17;
case 0x15u:
sub_61A60(a1->field_0, v3, a2);
return result;
case 0x16u:
v8 = sub_62440(a1->field_14);
goto LABEL_17;
case 0x17u:
v8 = sub_6226C(a1->field_10, v3);
goto LABEL_17;
case 0x18u:
v8 = sub_63530(a1->field_14);
LABEL_17:
v6 = 0;
if ( v8 )
{
*a2 = 0;
v6 = v8;
}
return v6;
default:
LOWORD(v5) = 28032;
goto LABEL_5;
}
}
}
}
LOWORD(v5) = -27504;
LABEL_5:
HIWORD(v5) = 13;
v6 = 0;
*a2 = v5;
return v6;
}Jako parametr switch przekazywane jest pole struktury, któremu wcześniej przypisano wartość 3. Sprawdźmy case 3: w funkcji sub_6364C przekazywane są parametry ze struktury, które zostały tam umieszczone w poprzedniej funkcji, tzn. klucz i zaszyfrowane dane. Jeśli dokładnie przyjrzymy się sub_6364C, możemy poznać w niej algorytm RC4.
Mamy algorytm i klucz. Spróbujmy odszyfrować nazwę klasy. Oto co uzyskaliśmy: com/taobao/wireless/security/adapter/JNICLibrary. Doskonale! Jesteśmy na dobrej drodze.
Drzewo komend
Teraz należy znaleźć wywołanie RegisterNatives, które wskaże nam na funkcję doCommandNative. Przeglądamy funkcje wywoływane z JNI_OnLoad, i znajdujemy ją w sub_B7B0:
int __fastcall sub_B7F6(JNIEnv *env, jclass clazz)
{
char signature[41]; // [sp+7h] [bp-55h]
char name[16]; // [sp+30h] [bp-2Ch]
JNINativeMethod method; // [sp+40h] [bp-1Ch]
int v8; // [sp+4Ch] [bp-10h]
v8 = *(_DWORD *)off_8AC00;
decryptString((unsigned __int8 *)&unk_83ED9, (unsigned __int8 *)name, 0x10u); // doCommandNative
decryptString((unsigned __int8 *)&unk_83EEA, (unsigned __int8 *)signature, 0x29u); // (I[Ljava/lang/Object;)Ljava/lang/Object;
method.name = name;
method.signature = signature;
method.fnPtr = sub_B69C;
return ((int (__fastcall *)(JNIEnv *, jclass, JNINativeMethod *, int))(*env)->RegisterNatives)(env, clazz, &method, 1) >> 31;
}I rzeczywiście, rejestrowany jest natywny metod o nazwie doCommandNative. Teraz znamy jego adres. Zobaczmy, co robi.
int __fastcall doCommandNative(JNIEnv *env, jobject obj, int command, jarray args)
{
int v5; // r5
struc_2 *a5; // r6
int v9; // r1
int v11; // [sp+Ch] [bp-14h]
int v12; // [sp+10h] [bp-10h]
v5 = 0;
v12 = *(_DWORD *)off_8AC00;
v11 = 0;
a5 = (struc_2 *)malloc(0x14u);
if ( a5 )
{
a5->field_0 = 0;
a5->field_4 = 0;
a5->field_8 = 0;
a5->field_C = 0;
v9 = command % 10000 / 100;
a5->field_0 = command / 10000;
a5->field_4 = v9;
a5->field_8 = command % 100;
a5->field_C = env;
a5->field_10 = args;
v5 = sub_9D60(command / 10000, v9, command % 100, 1, (int)a5, &v11);
}
free(a5);
if ( !v5 && v11 )
sub_7CF34(env, v11, &byte_83ED7);
return v5;
}Z nazwy można wywnioskować, że tutaj znajduje się punkt wejścia wszystkich funkcji, które deweloperzy postanowili przenieść do natywnej biblioteki. Interesuje nas funkcja o numerze 10601.
Z kodu można zobaczyć, że z numeru komendy powstają trzy liczby: command / 10000, command % 10000 / 100 i command % 10, tzn. w naszym przypadku, 1, 6 i 1. Te trzy liczby, a także wskaźnik na JNIEnv i przekazane argumenty funkcji, są składane w strukturę i przekazywane dalej. Z pomocą tych trzech liczb (oznaczymy je jako N1, N2 i N3) budowane jest drzewo komend.
Mniej więcej takie:

Drzewo uzupełniane jest dynamicznie w JNI_OnLoad.
Trzy liczby kodują ścieżkę w drzewie. Każdy liść drzewa zawiera adres odpowiadającej funkcji. Klucz znajduje się w węźle rodzica. Znalezienie miejsca w kodzie, w którym do drzewa dodawana jest potrzebna nam funkcja, nie stanowi dużego trudu, jeśli zrozumiemy wszystkie używane struktury (ich opis pomijamy, aby nie rozrastać i tak już sporego artykułu).
Jeszcze obfuskacja
Otrzymaliśmy adres funkcji, która ma odszyfrować ruch: 0x5F1AC. Ale na radość jest jeszcze za wcześnie: twórcy UC Browser przygotowali dla nas kolejny niespodziankę.
Po uzyskaniu parametrów z tablicy, która została stworzona w kodzie Java, trafiamy
do funkcji pod adresem 0x4D070. I tutaj czeka nas kolejny rodzaj obfuskacji kodu.
Wkładamy do R7 i R4 dwa indeksy:

Przenosimy pierwszy indeks do R11:

Aby uzyskać adres z tabeli, używamy indeksu:

Po przejściu do pierwszego adresu używany jest drugi indeks, który znajduje się w R4. W tabeli jest 230 elementów.
Co z tym zrobić? Możemy powiedzieć IDA, że to taki switch: Edytuj -> Inne -> Określ idiom przełącznika.

Otrzymany kod jest przerażający. Ale przeszukując jego dżungle, można zauważyć wywołanie już znanej nam funkcji sub_6115C:

Był tam przełącznik, w którym w przypadku 3 znajdowało się odszyfrowanie z użyciem algorytmu RC4. A w tym przypadku struktura przekazywana do funkcji jest wypełniana z parametrów przekazanych do doCommandNative. Przypominamy sobie, że mieliśmy tam magicInt o wartości 16. Patrzymy na odpowiadający przypadek – i po kilku przejściach znajdujemy kod, po którym można rozpoznać algorytm.

To AES!
Algorytm jest, pozostaje tylko uzyskać jego parametry: tryb, klucz i, być może, wektor inicjalizacji (jego obecność zależy od trybu pracy algorytmu AES). Struktura z nimi powinna być formowana gdzieś przed wywołaniem funkcji sub_6115C, ale ta część kodu jest obfuskowana szczególnie dobrze, dlatego pojawia się pomysł, aby poprawić kod, aby wszystkie parametry funkcji odszyfrowania były zrzucane do pliku.
Łata
Aby nie pisać całego kodu łaty w języku assemblera ręcznie, można uruchomić Android Studio, napisać tam funkcję, która przyjmuje te same parametry, co nasza funkcja odszyfrowania, i zapisuje do pliku, po czym skopiować kod, który wygeneruje kompilator.
Nasi przyjaciele z zespołu UC Browser również zadbali o wygodę dodawania kodu. Przypominamy, że na początku każdej funkcji mamy kod śmieciowy, który łatwo można zastąpić dowolnym innym. Bardzo wygodne 🙂 Choć na początku docelowej funkcji miejsca na kod, który zapisuje wszystkie parametry do pliku, jest mało. Musieliśmy podzielić go na części i użyć śmieciowych bloków sąsiednich funkcji. Łącznie wyszły cztery części.
Pierwsza część:

W architekturze ARM pierwsze cztery parametry funkcji są przekazywane przez rejestry R0-R3, pozostałe, jeśli są, przez stos. W rejestrze LR przekazywany jest adres powrotu. Wszystko to trzeba zachować, aby funkcja mogła działać po zrzuceniu jej parametrów. Należy również zachować wszystkie rejestry, których będziemy używać w trakcie, więc wykonujemy PUSH.W {R0-R10,LR}. W R7 otrzymujemy adres listy parametrów przekazanych funkcji przez stos.
Za pomocą funkcji fopen otworzymy plik /data/local/tmp/aes w trybie „ab”,
tj. do dodawania. W R0 ładujemy adres nazwy pliku, w R1 — adres ciągu z określeniem trybu. I tutaj kończy się kod śmieciowy, dlatego przechodzimy do następnej funkcji. Aby mogła nadal działać, umieszczamy na początku przejście do prawdziwego kodu funkcji z pominięciem śmieci, a zamiast śmieci dodajemy kontynuację łaty.

Wywołujemy fopen.
Pierwsze trzy parametry funkcji aes mają typ int. Ponieważ na początku zapisaliśmy rejestry na stosie, możemy po prostu przekazać funkcji fwrite ich adresy na stosie.

Dalej mamy trzy struktury, które zawierają rozmiar danych i wskaźnik na dane dla klucza, wektora inicjalizacji i zaszyfrowanych danych.

Na końcu zamykamy plik, przywracamy rejestry i przekazujemy kontrolę prawdziwej funkcji. aes.
Składamy APK z patche'owaną biblioteką, podpisujemy, wrzucamy na urządzenie/emulator, uruchamiamy. Widzimy, że nasz zrzut się tworzy i zapisuje wiele danych. Przeglądarka używa szyfrowania nie tylko do ruchu, a całe szyfrowanie przebiega przez rozważaną funkcję. A potrzebnych danych jakoś nie ma, w ruchu nie widać potrzebnego żądania. Żeby nie czekać, aż UC Browser zechce wykonać potrzebne żądanie, weźmiemy zaszyfrowaną odpowiedź z serwera, uzyskaną wcześniej, i jeszcze raz załatwimy aplikację: dodamy deszyfrowanie w onCreate głównej aktywności.
const/16 v1, 0x62
new-array v1, v1, [B
fill-array-data v1, :encrypted_data
const/16 v0, 0x1f
invoke-static {v0, v1}, Lcom/uc/browser/core/d/c/g;->j(I[B)[B
move-result-object v1
array-length v2, v1
invoke-static {v2}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;
move-result-object v2
const-string v0, "ololo"
invoke-static {v0, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)IZbieramy, podpisujemy, instalujemy, uruchamiamy. Otrzymujemy NullPointerException, ponieważ metoda zwróciła null.
W trakcie dalszej analizy kodu odkryto funkcję, w której odszyfrowywane są interesujące linijki: „META-INF/” i „.RSA”. Wygląda na to, że aplikacja sprawdza swój certyfikat. Albo nawet generuje z niego klucze. Nie chcemy zajmować się tym, co się dzieje z certyfikatem, więc po prostu podstawimy mu właściwy certyfikat. Przepatchujemy zaszyfrowaną linijkę w taki sposób, aby zamiast „META-INF/” pojawiło się „BLABLINF/”, stworzymy folder o tej nazwie w APK i wrzucimy tam certyfikat przeglądarki.
Zbieramy, podpisujemy, instalujemy, uruchamiamy. Bingo! Mamy klucz!
MitM
Otrzymaliśmy klucz i wektor inicjalizacji, równy kluczowi. Spróbujemy odszyfrować odpowiedź serwera w trybie CBC.

Widzimy URL archiwum, coś podobnego do MD5, „extract_unzipsize” i liczbę. Sprawdzamy: MD5 archiwum się zgadza, rozmiar rozpakowanej biblioteki się zgadza. Spróbujemy przepatchować tę bibliotekę i oddać ją przeglądarce. Aby pokazać, że nasza przepatchowana biblioteka załadowała się, będziemy uruchamiać Intent do utworzenia SMS z tekstem „PWNED!”. Podmienimy dwa odpowiedzi od serwera: i na pobieranie archiwum. W pierwszym podmieniamy MD5 (rozmiar po rozpakowaniu nie zmienia się), w drugim oddajemy archiwum z przepatchowaną biblioteką.
Przeglądarka kilka razy próbuje pobrać archiwum, po czym wyświetla błąd. Najwyraźniej coś
nie podoba mu się. W wyniku analizy tego mętnego formatu ustalono, że serwer przesyła również rozmiar archiwum:

Jest zakodowany w LEB128. Po patchu rozmiar archiwum z biblioteką nieco się zmienił, dlatego przeglądarka uznała, że archiwum pobrało się źle, i po kilku próbach wyświetliła błąd.
Poprawiamy rozmiar archiwum… I – zwycięstwo! 🙂 Wynik na wideo.
Konsekwencje i reakcja dewelopera
Hakerzy mogli w ten sam sposób wykorzystać niebezpieczną funkcję UC Browser, aby rozpowszechniać i uruchamiać złośliwe biblioteki. Biblioteki te będą działać w kontekście przeglądarki, dlatego otrzymają wszystkie jej uprawnienia systemowe. W rezultacie - możliwość wyświetlania okien phishingowych oraz dostęp do plików roboczych pomarańczowej chińskiej wiewiórki, w tym przechowywanych w bazie danych loginów, haseł i ciasteczek.
Skontaktowaliśmy się z deweloperami UC Browser i poinformowaliśmy ich o wykrytym problemie, starając się wskazać na lukę i jej niebezpieczeństwo, ale nie chcieli z nami o tym rozmawiać. Tymczasem przeglądarka wciąż chwaliła się niebezpieczną funkcją, na którą wszyscy zwracali uwagę. Jednak gdy tylko ujawniliśmy szczegóły luki, nie można było już jej ignorować jak wcześniej. 27 marca została
wydana nowa wersja UC Browser 12.10.9.1193, która nawiązywała połączenie z serwerem przez HTTPS: .
Ponadto, po «naprawie» i do momentu napisania tego artykułu, próba otwarcia pliku PDF w przeglądarce prowadziła do wyświetlenia komunikatu o błędzie z tekstem «Ups, coś poszło nie tak!». Żądanie do serwera przy próbie otwarcia PDF nie było zrealizowane, ale żądanie przy uruchomieniu przeglądarki było wykonywane, co sugeruje, że istnieje możliwość załadowania wykonywalnego kodu wbrew zasadom Google Play.
Źródło: habr.com
