Szukam luk w przeglądarce UC

Szukam luk w przeglądarce UC

Wprowadzenie

Pod koniec marca my zgłoszone, że odkryli ukrytą możliwość ładowania i uruchamiania niezweryfikowanego kodu w przeglądarce UC. Dzisiaj przyjrzymy się szczegółowo, jak następuje to pobieranie i jak hakerzy mogą go wykorzystać do własnych celów.

Jakiś czas temu UC Browser była reklamowana i dystrybuowana bardzo agresywnie: była instalowana na urządzeniach użytkowników przy użyciu szkodliwego oprogramowania, dystrybuowanego z różnych stron pod przykrywką plików wideo (tzn. użytkownicy myśleli, że pobierają na przykład film pornograficzny, ale zamiast tego otrzymałem plik APK z tą przeglądarką), używał przerażających banerów z wiadomościami, że przeglądarka jest przestarzała, podatna na ataki i tym podobnymi. W oficjalnej grupie przeglądarek UC na VK jest motyw, w którym użytkownicy mogą skarżyć się na nieuczciwą reklamę, przykładów jest tam wiele. W 2016 roku było nawet reklama wideo w języku rosyjskim (tak, reklama przeglądarki blokującej reklamy).

W chwili pisania tego tekstu UC Browser ma ponad 500 000 000 instalacji w Google Play. To robi wrażenie – więcej ma tylko Google Chrome. Wśród recenzji można zobaczyć sporo skarg dotyczących reklam i przekierowań do niektórych aplikacji w Google Play. To był powód naszych badań: postanowiliśmy sprawdzić, czy przeglądarka UC robi coś złego. I okazało się, że tak!

W kodzie aplikacji odkryto możliwość pobrania i uruchomienia kodu wykonywalnego, co jest sprzeczne z zasadami publikowania wniosków w Google Play. Oprócz pobierania kodu wykonywalnego, UC Browser robi to w niebezpieczny sposób, który może zostać wykorzystany do przeprowadzenia ataku MitM. Zobaczmy, czy uda nam się przeprowadzić taki atak.

Wszystko, co napisano poniżej, dotyczy wersji przeglądarki UC, która była dostępna w Google Play w momencie badania:

package: com.UCMobile.intl
versionName: 12.10.8.1172
versionCode: 10598
sha1 APK-файла: f5edb2243413c777172f6362876041eb0c3a928c

Wektor ataku

W manifeście przeglądarki UC można znaleźć usługę o zrozumiałej nazwie com.uc.deployment.UpgradeDeployService.

    <service android_exported="false" android_name="com.uc.deployment.UpgradeDeployService" android_process=":deploy" />

Po uruchomieniu tej usługi przeglądarka wysyła żądanie POST do puds.ucweb.com/upgrade/index.xhtml, co można zobaczyć w ruchu ulicznym jakiś czas po starcie. W odpowiedzi może otrzymać polecenie pobrania jakiejś aktualizacji lub nowego modułu. Podczas analizy serwer nie wydawał takich poleceń, jednak zauważyliśmy, że gdy próbujemy otworzyć plik PDF w przeglądarce, wysyła drugie żądanie na podany powyżej adres, po czym pobiera natywną bibliotekę. Do przeprowadzenia ataku postanowiliśmy wykorzystać tę funkcję przeglądarki UC Browser: możliwość otwierania plików PDF przy użyciu natywnej biblioteki, której nie ma w pakiecie APK i którą w razie potrzeby pobiera z Internetu. Warto zaznaczyć, że teoretycznie UC Browser może zostać zmuszony do pobrania czegoś bez interakcji użytkownika - jeśli podasz dobrze sformułowaną odpowiedź na żądanie, które jest wykonywane po uruchomieniu przeglądarki. Ale aby to zrobić, musimy bardziej szczegółowo przestudiować protokół interakcji z serwerem, dlatego zdecydowaliśmy, że łatwiej będzie edytować przechwyconą odpowiedź i zastąpić bibliotekę do pracy z plikiem PDF.

Tak więc, gdy użytkownik chce otworzyć plik PDF bezpośrednio w przeglądarce, w ruchu można zobaczyć następujące żądania:

Szukam luk w przeglądarce UC

Najpierw jest żądanie POST do puds.ucweb.com/upgrade/index.xhtml, Następnie
Pobierane jest archiwum z biblioteką do przeglądania plików PDF i formatów biurowych. Logiczne jest założenie, że w pierwszym żądaniu przesyłane są informacje o systemie (przynajmniej o architekturze zapewniającej wymaganą bibliotekę), a w odpowiedzi przeglądarka otrzymuje informacje o bibliotece, którą należy pobrać: adres i ewentualnie , coś innego. Problem polega na tym, że to żądanie jest zaszyfrowane.

Poproś o fragment

Fragment odpowiedzi

Szukam luk w przeglądarce UC

Szukam luk w przeglądarce UC

Sama biblioteka jest spakowana w formacie ZIP i nie jest szyfrowana.

Szukam luk w przeglądarce UC

Wyszukaj kod deszyfrowania ruchu

Spróbujmy rozszyfrować odpowiedź serwera. Spójrzmy na kod klasy com.uc.deployment.UpgradeDeployService: z metody w poleceniu Start iść do com.uc.deployment.bx, i od tego do com.uc.browser.core.dcfe:

    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("]product:");
v4.append(arg9.iGX.ipR);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]version:");
v4.append(arg9.iGX.iEn);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]upgrade_type:");
v4.append(arg9.iGX.mMode);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]force_flag:");
v4.append(arg9.iGX.iEo);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_mode:");
v4.append(arg9.iGX.iDQ);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_type:");
v4.append(arg9.iGX.iEr);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_state:");
v4.append(arg9.iGX.iEp);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_file:");
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("]download_type:");
v4.append(arg9.mDownloadType);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]download_group:");
v4.append(arg9.mDownloadGroup);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]download_path:");
v4.append(arg9.iGH);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_child_version:");
v4.append(arg9.iGX.iEx);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_series:");
v4.append(arg9.iGX.iEw);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_cpu_arch:");
v4.append(arg9.iGX.iEt);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_cpu_vfp3:");
v4.append(arg9.iGX.iEv);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_cpu_vfp:");
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("]component_name:");
v5.append(((au)v4_1).getName());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_ver_name:");
v5.append(((au)v4_1).aDA());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_ver_code:");
v5.append(((au)v4_1).gBl);
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_req_type:");
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("os_ver", c.getRomInfo()));
v3_2.add(g.fs("processor_arch", com.uc.b.a.a.c.getCpuArch()));
v3_2.add(g.fs("cpu_arch", com.uc.b.a.a.c.Pb()));
String v4_3 = com.uc.b.a.a.c.Pd();
v3_2.add(g.fs("cpu_vfp", v4_3));
v3_2.add(g.fs("net_type", String.valueOf(com.uc.base.system.a.Jo())));
v3_2.add(g.fs("fromhost", arg9.iGX.iEm));
v3_2.add(g.fs("plugin_ver", arg9.iGX.iEn));
v3_2.add(g.fs("target_lang", arg9.iGX.iEs));
v3_2.add(g.fs("vitamio_cpu_arch", arg9.iGX.iEt));
v3_2.add(g.fs("vitamio_vfp", arg9.iGX.iEu));
v3_2.add(g.fs("vitamio_vfp3", arg9.iGX.iEv));
v3_2.add(g.fs("plugin_child_ver", arg9.iGX.iEx));
v3_2.add(g.fs("ver_series", arg9.iGX.iEw));
v3_2.add(g.fs("child_ver", 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("upgrade_log", i.bjt()));
v3_2.add(g.fs("silent_install", String.valueOf(arg9.iGX.iDQ)));
v3_2.add(g.fs("silent_state", String.valueOf(arg9.iGX.iEp)));
v3_2.add(g.fs("silent_file", arg9.iGX.iEq));
v3_2.add(g.fs("silent_type", String.valueOf(arg9.iGX.iEr)));
v3_2.add(g.fs("cpu_archit", com.uc.b.a.a.c.Pc()));
v3_2.add(g.fs("cpu_set", 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("cpu_cores", 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("totalram", 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("api_level", String.valueOf(Build$VERSION.SDK_INT)));
v3_2.add(g.fs("uc_apk_list", 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);
}

Widzimy tutaj tworzenie żądania POST. Zwracamy uwagę na utworzenie tablicy 16 bajtów i jej wypełnienie: 0x5F, 0, 0x1F, -50 (=0xCE). Zgadza się z tym, co widzieliśmy w powyższym żądaniu.

W tej samej klasie możesz zobaczyć klasę zagnieżdżoną, która ma jeszcze jedną interesującą metodę:

        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 pobiera tablicę bajtów jako dane wejściowe i sprawdza, czy bajt zerowy to 0x60 lub trzeci bajt to 0xD0, a drugi bajt to 1, 11 lub 0x1F. Patrzymy na odpowiedź z serwera: bajt zerowy to 0x60, drugi to 0x1F, trzeci to 0x60. Brzmi jak to, czego potrzebujemy. Sądząc po wierszach (na przykład „up_decrypt”) należy wywołać metodę, która odszyfruje odpowiedź serwera.
Przejdźmy do metody gj. Zauważ, że pierwszym argumentem jest bajt z offsetem 2 (tj. w naszym przypadku 0x1F), a drugi to odpowiedź serwera bez
pierwsze 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 wybieramy algorytm deszyfrowania i ten sam bajt, który jest w naszym
przypadek równy 0x1F, oznacza jedną z trzech możliwych opcji.

Kontynuujemy analizę kodu. Po kilku skokach znajdujemy się w metodzie o zrozumiałej nazwie odszyfrujBytesByKey.

Tutaj z naszej odpowiedzi oddzielane są jeszcze dwa bajty i uzyskiwany jest z nich ciąg znaków. Oczywiste 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 байта
System.arraycopy(bytes, 0, prefix, 0, prefix.length);
String keyId = c.ayR().d(ByteBuffer.wrap(prefix).getShort()); // Выбор ключа
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;
}

Patrząc w przyszłość zauważamy, że na tym etapie nie uzyskujemy jeszcze klucza, a jedynie jego „identyfikator”. Zdobycie klucza jest nieco bardziej skomplikowane.

W kolejnej metodzie do już istniejących dodawane są jeszcze dwa parametry, tworząc cztery z nich: magiczną liczbę 16, identyfikator klucza, zaszyfrowane dane oraz niezrozumiały ciąg znaków (w naszym przypadku pusty).

    public final byte[] l(String keyId, byte[] encrypted) throws SecException {
return this.ayJ().staticBinarySafeDecryptNoB64(16, keyId, encrypted, "");
}

Po serii przejść dochodzimy do metody staticBinarySafeDecryptNoB64 interfejs com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. W głównym kodzie aplikacji nie ma klas implementujących ten interfejs. W pliku jest taka klasa lib/armeabi-v7a/libsgmain.so, który w rzeczywistości nie jest plikiem .so, ale .jar. Interesująca nas metoda jest realizowana w następujący sposób:

package 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 < 19 && encrypted != null && encrypted.length > 0) {
return this.b(magicInt, keyId, encrypted, magicString);
}
throw new SecException("", 301);
}
//...
}

Tutaj nasza lista parametrów jest uzupełniona o dwie kolejne liczby całkowite: 2 i 0. Sądząc po
wszystko, 2 oznacza odszyfrowanie, jak w metodzie doFinał klasa systemu javax.crypto.Cipher. A wszystko to jest przesyłane do określonego routera o numerze 10601 - to najwyraźniej numer polecenia.

Po kolejnym łańcuchu przejść znajdujemy klasę implementującą interfejs Komponent IRoutera i metoda doPolecenie:

package 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 Biblioteka JNIC, w którym zadeklarowana jest metoda natywna doCommandNative:

package com.taobao.wireless.security.adapter;
public class JNICLibrary {
public static native Object doCommandNative(int arg0, Object[] arg1);
}

Oznacza to, że musimy znaleźć metodę w natywnym kodzie doCommandNative. I tu zaczyna się zabawa.

Zaciemnianie kodu maszynowego

W pliku libsgmain.so (który w rzeczywistości jest plikiem .jar i w którym tuż powyżej znaleźliśmy implementację niektórych interfejsów związanych z szyfrowaniem) istnieje jedna natywna biblioteka: libsgmainso-6.4.36.so. Otwieramy go w IDA i otrzymujemy kilka okien dialogowych z błędami. Problem polega na tym, że tabela nagłówków sekcji jest nieprawidłowa. Robi się to celowo, aby skomplikować analizę.

Szukam luk w przeglądarce UC

Ale nie jest to potrzebne: aby poprawnie załadować plik ELF i go przeanalizować, wystarczy tabela nagłówków programu. Dlatego po prostu usuwamy tabelę sekcji, zerując odpowiednie pola w nagłówku.

Szukam luk w przeglądarce UC

Otwórz plik ponownie w IDA.

Istnieją dwa sposoby poinformowania wirtualnej maszyny Java, gdzie dokładnie w bibliotece natywnej znajduje się implementacja metody zadeklarowanej w kodzie Java jako natywna. Pierwszym z nich jest nadanie mu nazwy gatunkowej Nazwa_pakietu Java_Nazwa_klasy_Nazwa metody.

Drugim jest zarejestrowanie go podczas ładowania biblioteki (w funkcji JNI_OnLoad)
za pomocą wywołania funkcji Zarejestruj się.

W naszym przypadku, jeśli zastosujemy pierwszą metodę, nazwa powinna wyglądać następująco: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

Wśród eksportowanych funkcji nie ma takiej funkcji, co oznacza, że ​​​​trzeba szukać połączenia Zarejestruj się.
Przejdźmy do funkcji JNI_OnLoad i widzimy ten obrazek:

Szukam luk w przeglądarce UC

Co tu się dzieje? Na pierwszy rzut oka początek i koniec funkcji są typowe dla architektury ARM. Pierwsza instrukcja na stosie przechowuje zawartość rejestrów, których funkcja będzie używać w swoim działaniu (w tym przypadku R0, R1 i R2), a także zawartość rejestru LR, który zawiera adres zwrotny z funkcji . Ostatnia instrukcja przywraca zapisane rejestry, a adres zwrotny jest natychmiast umieszczany w rejestrze PC - tym samym wracając z funkcji. Ale jeśli przyjrzysz się uważnie, zauważysz, że przedostatnia instrukcja zmienia adres zwrotny przechowywany na stosie. Obliczmy, jak to będzie wyglądać później
wykonanie kodu. Do R1 ładowany jest określony adres 0xB130, odejmowane jest od niego 5, następnie przesyłane do R0 i dodawane do niego 0x10. Okazuje się, że 0xB13B. Zatem IDA uważa, że ​​ostatnia instrukcja jest normalnym powrotem funkcji, choć w rzeczywistości kieruje się pod wyliczony adres 0xB13B.

Warto w tym miejscu przypomnieć, że procesory ARM posiadają dwa tryby i dwa zestawy instrukcji: ARM i Thumb. Najmniej znaczący bit adresu informuje procesor, który zestaw instrukcji jest używany. Oznacza to, że adres to w rzeczywistości 0xB13A, a jeden w najmniej znaczącym bicie wskazuje tryb kciuka.

Podobny „adapter” został dodany na początku każdej funkcji w tej bibliotece
kod śmieciowy. Nie będziemy się nad nimi szczegółowo rozwodzić - po prostu pamiętamy
że prawdziwy początek prawie wszystkich funkcji jest nieco dalej.

Ponieważ kod nie przeskakuje jawnie do 0xB13A, sama IDA nie rozpoznała, że ​​kod znajdował się w tej lokalizacji. Z tego samego powodu nie rozpoznaje większości kodu w bibliotece jako kodu, co nieco utrudnia analizę. Mówimy IDA, że to jest kod i dzieje się tak:

Szukam luk w przeglądarce UC

Tabela wyraźnie zaczyna się od 0xB144. Co jest w sub_494C?

Szukam luk w przeglądarce UC

Wywołując tę ​​funkcję w rejestrze LR otrzymujemy adres wspomnianej wcześniej tablicy (0xB144). W R0 - indeks w tej tabeli. Oznacza to, że wartość jest pobierana z tabeli, dodawana do LR i wynikiem jest
adres, pod który należy się udać. Spróbujmy to obliczyć: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Udajemy się pod otrzymany adres i widzimy dosłownie kilka przydatnych instrukcji i ponownie przechodzimy do 0xB140:

Szukam luk w przeglądarce UC

Teraz nastąpi przejście na offset z indeksem 0x20 z tabeli.

Sądząc po wielkości tabeli, takich przejść w kodzie będzie wiele. Powstaje pytanie, czy można sobie z tym jakoś bardziej automatycznie poradzić, bez ręcznego obliczania adresów. Z pomocą przychodzą nam skrypty i możliwość łatania kodu w IDA:

def put_unconditional_branch(source, destination):
offset = (destination - source - 4) >> 1
if offset > 2097151 or offset < -2097152:
raise RuntimeError("Invalid offset")
if offset > 1023 or offset < -1024:
instruction1 = 0xf000 | ((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"

Umieść kursor na linii 0xB26A, uruchom skrypt i zobacz przejście do 0xB4B0:

Szukam luk w przeglądarce UC

IDA ponownie nie uznała tego obszaru za kod. Pomagamy jej i widzimy tam inny projekt:

Szukam luk w przeglądarce UC

Instrukcje po BLX nie wydają się mieć większego sensu, bardziej przypominają jakieś przemieszczenie. Spójrzmy na sub_4964:

Szukam luk w przeglądarce UC

I rzeczywiście, tutaj z adresu znajdującego się w LR pobierany jest dword, dodawany do tego adresu, po czym pobierana jest wartość z wynikowego adresu i umieszczana na stosie. Do LR dodaje się także 4, dzięki czemu po powrocie z funkcji to samo przesunięcie jest pomijane. Następnie polecenie POP {R1} pobiera wynikową wartość ze stosu. Jeśli spojrzysz na to, co znajduje się pod adresem 0xB4BA + 0xEA = 0xB5A4, zobaczysz coś podobnego do tabeli adresów:

Szukam luk w przeglądarce UC

Aby załatać ten projekt, będziesz musiał pobrać z kodu dwa parametry: przesunięcie i numer rejestru, w którym chcesz umieścić wynik. Dla każdego możliwego rejestru będziesz musiał 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 << i):
register = i
break
if register == -1:
print "Unable to detect register"
else:
address = get_wide_dword(ea + 8) + ea + 8
for b in patches[register]:
patch_byte(ea, b)
ea += 1
if ea % 4 != 0:
ea += 2
patch_dword(ea, address)
elif pop[:3] == 'x5dxf8x04':
register = ord(pop[3]) >> 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 "POP instruction not found"
else:
print "Wrong operand type on +4:", get_operand_type(ea + 4, 0)
else:
print "Unable to detect first instructions"

Ustawiamy kursor na początku struktury, którą chcemy zastąpić - 0xB4B2 - i uruchamiamy skrypt:

Szukam luk w przeglądarce UC

Oprócz wspomnianych już struktur, kod zawiera także:

Szukam luk w przeglądarce UC

Podobnie jak w poprzednim przypadku, po instrukcji BLX następuje przesunięcie:

Szukam luk w przeglądarce UC

Bierzemy offset do adresu z LR, dodajemy go do LR i tam idziemy. 0x72044 + 0xC = 0x72050. Skrypt tego projektu jest dość prosty:

def put_unconditional_branch(source, destination):
offset = (destination - source - 4) >> 1
if offset > 2097151 or offset < -2097152:
raise RuntimeError("Invalid offset")
if offset > 1023 or offset < -1024:
instruction1 = 0xf000 | ((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 "Unable to detect first instruction"

Wynik wykonania skryptu:

Szukam luk w przeglądarce UC

Gdy już wszystko zostanie załatane w funkcji, można wskazać IDA jej prawdziwy początek. Poskłada cały kod funkcji i można go zdekompilować za pomocą HexRays.

Dekodowanie ciągów

Nauczyliśmy się radzić sobie z zaciemnianiem kodu maszynowego w bibliotece libsgmainso-6.4.36.so z przeglądarki UC i otrzymałem 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 nazwa klasy jest wyraźnie odszyfrowana. Jako parametry tej funkcji przekazywany jest wskaźnik do danych podobnych do danych zaszyfrowanych, określony bufor i liczba. Oczywiście po wywołaniu funkcji w buforze będzie odszyfrowana linia, gdyż jest ona przekazywana do funkcji ZnajdźKlasę, który jako drugi parametr przyjmuje nazwę klasy. Dlatego liczba oznacza rozmiar bufora lub długość linii. Spróbujmy rozszyfrować nazwę klasy, powinna ona nam powiedzieć, czy idziemy w dobrym kierunku. Przyjrzyjmy się bliżej temu, 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;
}

Funkcja sub_7AF78 tworzy instancję kontenera na tablice bajtowe o określonym rozmiarze (nie będziemy się szczegółowo rozwodzić nad tymi kontenerami). Tutaj tworzone są dwa takie kontenery: jeden zawiera linię „DcO/lcK+h?m3c*q@” (łatwo domyślić się, że jest to klucz), drugi zawiera zaszyfrowane dane. Następnie oba obiekty umieszczane są w określonej strukturze, która przekazywana jest do funkcji sub_6115C. Oznaczmy w tej strukturze także pole o wartości 3. Zobaczmy co dalej stanie się z tą strukturą.

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 < 0x19 )
{
switch ( v4 )
{
case 0u:
v8 = sub_6419C(a1->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;
}

Parametr switch jest polem struktury, któremu wcześniej przypisano wartość 3. Spójrz na przypadek 3: do funkcji sub_6364C parametry przekazywane są ze struktury, która została tam dodana w poprzedniej funkcji, czyli klucz i zaszyfrowane dane. Jeśli przyjrzysz się uważnie sub_6364C, można w nim rozpoznać algorytm RC4.

Mamy algorytm i klucz. Spróbujmy rozszyfrować nazwę klasy. Oto co się stało: com/taobao/wireless/security/adapter/JNICLibrary. Świetnie! Jesteśmy na dobrej drodze.

Drzewo poleceń

Teraz musimy znaleźć wyzwanie Zarejestruj się, co wskaże nam funkcję doCommandNative. Przyjrzyjmy się funkcjom wywoływanym z JNI_OnLoad, i znajdujemy to 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, zarejestrowana jest tutaj natywna metoda o tej nazwie doCommandNative. Znamy już 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;
}

Po nazwie można się domyślić, że tutaj znajduje się punkt wejścia wszystkich funkcji, które programiści postanowili przenieść do natywnej biblioteki. Nas interesuje funkcja o numerze 10601.

Z kodu widać, że numer polecenia generuje trzy liczby: polecenie/10000, polecenie % 10000 / 100 и polecenie % 10, czyli w naszym przypadku 1, 6 i 1. Te trzy liczby oraz wskaźnik do JNIEnv a argumenty przekazane do funkcji są dodawane do struktury i przekazywane dalej. Korzystając z trzech uzyskanych liczb (oznaczmy je N1, N2 i N3), budowane jest drzewo poleceń.

Coś takiego:

Szukam luk w przeglądarce UC

Drzewo jest wypełniane dynamicznie JNI_OnLoad.
Trzy liczby kodują ścieżkę w drzewie. Każdy liść drzewa zawiera adres odpowiedniej funkcji. Klucz znajduje się w węźle nadrzędnym. Znalezienie miejsca w kodzie, w którym do drzewa zostanie dodana potrzebna nam funkcja, nie jest trudne, jeśli rozumie się wszystkie użyte struktury (nie opisujemy ich, żeby nie zanudzać i tak już dość dużego artykułu).

Więcej zaciemnienia

Otrzymaliśmy adres funkcji, która powinna odszyfrować ruch: 0x5F1AC. Ale jest za wcześnie na radość: twórcy UC Browser przygotowali dla nas kolejną niespodziankę.

Po otrzymaniu parametrów z tablicy utworzonej w kodzie Java otrzymujemy
do funkcji pod adresem 0x4D070. I tutaj czeka nas kolejny rodzaj zaciemniania kodu.

W R7 i R4 umieszczamy dwa indeksy:

Szukam luk w przeglądarce UC

Przesuwamy pierwszy indeks do R11:

Szukam luk w przeglądarce UC

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

Szukam luk w przeglądarce UC

Po przejściu pod pierwszy adres wykorzystywany jest drugi indeks, który znajduje się w R4. W tabeli znajduje się 230 elementów.

Co z tym zrobić? Możesz powiedzieć IDA, że jest to przełącznik: Edycja -> Inne -> Określ idiom przełącznika.

Szukam luk w przeglądarce UC

Wynikowy kod jest przerażający. Jednak przemierzając dżunglę, możesz zauważyć wywołanie znanej nam już funkcji sub_6115C:

Szukam luk w przeglądarce UC

Był przełącznik, w którym w przypadku 3 nastąpiło odszyfrowanie przy użyciu algorytmu RC4. I w tym przypadku struktura przekazana do funkcji jest wypełniana z przekazanych parametrów doCommandNative. Przypomnijmy sobie, co tam mieliśmy magiaInt o wartości 16. Rozpatrujemy odpowiedni przypadek - i po kilku przejściach znajdujemy kod, po którym można zidentyfikować algorytm.

Szukam luk w przeglądarce UC

To jest AES!

Algorytm istnieje, pozostaje tylko uzyskać jego parametry: tryb, klucz i ewentualnie wektor inicjujący (jego obecność zależy od trybu pracy algorytmu AES). Strukturę z nimi należy utworzyć gdzieś przed wywołaniem funkcji sub_6115C, ale ta część kodu jest szczególnie dobrze zaciemniona, więc pojawił się pomysł załatania kodu, tak aby wszystkie parametry funkcji deszyfrującej były zrzucane do pliku.

Łata

Aby nie pisać ręcznie całego kodu łatki w asemblerze, możesz uruchomić Android Studio, napisać tam funkcję, która otrzyma takie same parametry wejściowe jak nasza funkcja deszyfrująca i zapisze do pliku, a następnie skopiuj i wklej kod, który kompilator Generować.

O wygodę dodawania kodu zadbali także nasi przyjaciele z zespołu UC Browser. Pamiętajmy, że na początku każdej funkcji mamy kod śmieciowy, który można łatwo zastąpić dowolnym innym. Bardzo wygodne 🙂 Jednak na początku funkcji docelowej nie ma wystarczającej ilości miejsca na kod zapisujący wszystkie parametry do pliku. Musiałem podzielić go na części i użyć bloków śmieci z sąsiednich funkcji. W sumie powstały cztery części.

Pierwsza część:

Szukam luk w przeglądarce UC

W architekturze ARM pierwsze cztery parametry funkcji są przekazywane przez rejestry R0-R3, pozostałe, jeśli takie istnieją, są przekazywane przez stos. Rejestr LR przenosi adres zwrotny. Trzeba to wszystko zapisać, żeby funkcja mogła działać po zrzuceniu jej parametrów. Musimy także zapisać wszystkie rejestry, które będziemy wykorzystywać w procesie, więc wykonujemy PUSH.W {R0-R10,LR}. W R7 otrzymujemy adres listy parametrów przekazanych do funkcji poprzez stos.

Korzystanie z funkcji otwarty otwórzmy plik /data/local/tmp/aes w trybie „ab”.
czyli do dodania. W R0 ładujemy adres nazwy pliku, w R1 - adres linii wskazującej tryb. I tutaj kończy się kod śmieciowy, więc przechodzimy do następnej funkcji. Aby dalej działało, stawiamy na początek przejście do prawdziwego kodu funkcji z pominięciem śmieci, a zamiast śmieci dodajemy kontynuację łatki.

Szukam luk w przeglądarce UC

Powołanie otwarty.

Pierwsze trzy parametry funkcji AES mieć typ int. Ponieważ na początku zapisaliśmy rejestry na stosie, możemy po prostu przekazać funkcję napisz ich adresy na stosie.

Szukam luk w przeglądarce UC

Następnie mamy trzy struktury, które zawierają rozmiar danych i wskaźnik do danych dla klucza, wektora inicjującego i zaszyfrowanych danych.

Szukam luk w przeglądarce UC

Na koniec zamknij plik, przywróć rejestry i przenieś kontrolę do rzeczywistej funkcji AES.

Zbieramy plik APK z załataną biblioteką, podpisujemy go, przesyłamy na urządzenie/emulator i uruchamiamy. Widzimy, że nasz zrzut jest tworzony i zapisywanych jest tam mnóstwo danych. Przeglądarka wykorzystuje szyfrowanie nie tylko dla ruchu, a całe szyfrowanie przechodzi przez daną funkcję. Ale z jakiegoś powodu nie ma tam niezbędnych danych, a wymagane żądanie nie jest widoczne w ruchu. Aby nie czekać, aż UC Browser raczy wykonać niezbędne żądanie, pobierzmy otrzymaną wcześniej zaszyfrowaną odpowiedź z serwera i załatajmy aplikację ponownie: dodaj deszyfrację do onCreate głównego działania.

    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;)I

Montujemy, podpisujemy, instalujemy, uruchamiamy. Otrzymujemy wyjątek NullPointerException, ponieważ metoda zwróciła wartość null.

Podczas dalszej analizy kodu odkryto funkcję rozszyfrowującą interesujące linie: „META-INF/” i „.RSA”. Wygląda na to, że aplikacja weryfikuje swój certyfikat. Lub nawet generuje z niego klucze. Nie bardzo chcę się zajmować tym, co dzieje się z certyfikatem, więc po prostu dodamy mu właściwy certyfikat. Poprawmy zaszyfrowaną linię tak, aby zamiast „META-INF/” otrzymaliśmy „BLABLINF/”, utwórzmy w APK folder o tej nazwie i dodajmy tam certyfikat przeglądarki squirrel.

Montujemy, podpisujemy, instalujemy, uruchamiamy. Bingo! Mamy klucz!

Dzięki M.

Otrzymaliśmy klucz i wektor inicjujący równy kluczowi. Spróbujmy odszyfrować odpowiedź serwera w trybie CBC.

Szukam luk w przeglądarce UC

Widzimy adres URL archiwum, coś podobnego do MD5, „extract_unzipsize” i liczbę. Sprawdzamy: MD5 archiwum jest takie samo, rozmiar rozpakowanej biblioteki jest taki sam. Próbujemy załatać tę bibliotekę i przekazać ją przeglądarce. Aby pokazać, że nasza poprawiona biblioteka została załadowana, uruchomimy Intencję utworzenia SMS-a o treści „PWNED!” Zastąpimy dwie odpowiedzi z serwera: puds.ucweb.com/upgrade/index.xhtml i pobrać archiwum. W pierwszym podmieniamy MD5 (rozmiar nie zmienia się po rozpakowaniu), w drugim podajemy archiwum z załataną biblioteką.

Przeglądarka kilka razy próbuje pobrać archiwum, po czym wyświetla błąd. Najwyraźniej coś
on nie lubi. W wyniku analizy tego mętnego formatu okazało się, że serwer przesyła również rozmiar archiwum:

Szukam luk w przeglądarce UC

Jest zakodowany w LEB128. Po patchu zmienił się nieco rozmiar archiwum z biblioteką, więc przeglądarka uznała, że ​​archiwum zostało pobrane krzywo i po kilku próbach wyrzuciła błąd.

Dopasowujemy wielkość archiwum... I – zwycięstwo! 🙂 Efekt znajdziecie na filmie.

https://www.youtube.com/watch?v=Nfns7uH03J8

Konsekwencje i reakcja dewelopera

W ten sam sposób hakerzy mogą wykorzystać niebezpieczną funkcję przeglądarki UC do dystrybucji i uruchamiania złośliwych bibliotek. Biblioteki te będą działać w kontekście przeglądarki, więc otrzymają wszystkie jej uprawnienia systemowe. W rezultacie możliwość wyświetlania okien phishingowych, a także dostęp do plików roboczych pomarańczowej wiewiórki chińskiej, w tym loginów, haseł i plików cookie przechowywanych w bazie danych.

Skontaktowaliśmy się z twórcami UC Browser i poinformowaliśmy ich o znalezionym problemie, próbowaliśmy wskazać lukę i związane z nią niebezpieczeństwo, ale nie rozmawiali z nami o niczym. Tymczasem przeglądarka w dalszym ciągu obnosiła się ze swoją niebezpieczną funkcją na widoku. Kiedy jednak ujawniliśmy szczegóły luki, nie można było już jej ignorować tak jak wcześniej. 27 marca był
wydano nową wersję przeglądarki UC Browser 12.10.9.1193, która łączyła się z serwerem poprzez HTTPS: puds.ucweb.com/upgrade/index.xhtml.

Dodatkowo po „poprawce” i do czasu pisania tego artykułu, próba otwarcia pliku PDF w przeglądarce kończyła się komunikatem o błędzie o treści „Ups, coś poszło nie tak!” Podczas próby otwarcia pliku PDF nie wysłano żądania do serwera, ale wysłano żądanie po uruchomieniu przeglądarki, co wskazuje na ciągłą możliwość pobierania kodu wykonywalnego z naruszeniem zasad Google Play.

Źródło: www.habr.com

Dodaj komentarz