
Introduktion
I slutningen af marts opdagede vi , at der var en skjult mulighed for at downloade og køre uautoriseret kode i UC Browser. I dag vil vi gå i dybden med, hvordan denne download finder sted, og hvordan hackere kan bruge den til deres egne formål.
For noget tid siden blev UC Browser reklameret for og distribueret meget aggressivt: det blev installeret på brugernes enheder med malware, distribueret fra forskellige websteder under dække af videofiler (dvs. brugerne troede, de downloadede en pornofilm, men fik i stedet en APK med denne browser), og der blev brugt skræmmende bannere med meddelelser om, at browseren var forældet, sårbar osv. I den officielle UC Browser-gruppe på VK er der , hvor brugerne kan klage over uretfærdig reklamering, der er mange eksempler. I 2016 var der endda på russisk (ja, reklamering for en browser, der blokerer reklamer).
På tidspunktet for artiklens skrivning havde UC Browser omkring 500.000.000 installationer i Google Play. Det er imponerende – kun Google Chrome har flere. Blandt anmeldelserne kan man se en del klager over reklamer og omdirigeringer til nogle applikationer i Google Play. Dette blev anledning til en undersøgelse: Vi besluttede at se, om UC Browser gjorde noget negativt. Og det viste sig, at de faktisk gjorde!
I appens kode blev der opdaget en mulighed for at downloade og køre eksekverbar kode, i Google Play. Udover at UC Browser downloader eksekverbar kode, gør den det på en usikker måde, som kan bruges til at udføre en MitM-angreb. Lad os se, om vi kan udføre sådan et angreb.
Alt, hvad der er skrevet herunder, er relevant for den version af UC Browser, der var tilgængelig i Google Play på tidspunktet for undersøgelsen:
pakke: com.UCMobile.intl
versionName: 12.10.8.1172
versionCode: 10598
sha1 APK-fil: f5edb2243413c777172f6362876041eb0c3a928cAngrebsvektor
I manifestet for UC Browser kan man finde en tjeneste med et sigende navn com.uc.deployment.UpgradeDeployService.
<service android_exported="false" android_name="com.uc.deployment.UpgradeDeployService" android_process=":deploy" />
Ved opstart af denne tjeneste sender browseren en POST-anmodning til , som kan opdages i trafikken efter et stykke tid efter opstart. Som svar kan den modtage en kommando om at downloade en eller anden opdatering eller ny modul. Under analysen gav serveren ikke sådanne kommandoer, men vi bemærkede, at når man forsøger at åbne en PDF i browseren, foretager den en gentagen anmodning til den nævnte adresse, hvorefter den downloader det native bibliotek. For at udføre et angreb besluttede vi at udnytte denne egenskab ved UC Browser: evnen til at åbne PDF'er ved hjælp af et native bibliotek, som ikke findes i APK'en, og som den henter fra internettet efter behov. Det skal bemærkes, at UC Browser teoretisk set kan tvinges til at downloade noget uden brugerinteraktion - hvis den får et korrekt formuleret svar på anmodningen, som udføres efter browserens opstart. Men for at gøre dette skal man studere protokollen for interaktion med serveren mere detaljeret, så vi besluttede, at det ville være lettere at redigere det opsnappede svar og substituere biblioteket til arbejde med PDF.
Så når brugeren ønsker at åbne en PDF direkte i browseren, kan man se følgende anmodninger i trafikken:

Først sendes en POST-anmodning til , hvorefter
der downloades et arkiv med biblioteket til visning af PDF og kontorformater. Det er logisk at formode, at den første anmodning sender systeminformation (minimalt arkitektur for at returnere det nødvendige bibliotek), og som svar på den får browseren en eller anden information om biblioteket, der skal downloades: adressen og muligvis noget andet. Problemet er, at denne anmodning er krypteret.
Afsnit af anmodningen
Afsnit af svaret


Selve biblioteket er pakket i ZIP og ikke krypteret.

Søgning efter dekrypteringskode for trafikken
Lad os prøve at dekryptere serverens svar. Vi ser på klassens kode com.uc.deployment.UpgradeDeployService: fra metoden onStartCommand bevæg dig til com.uc.deployment.b.x, og derfra til com.uc.browser.core.d.c.f.e:
offentlig slutgiltig void e(l arg9) {
int v4_5;
String v3_1;
byte[] v3;
byte[] v1 = null;
hvis(arg9 == null) {
v3 = v1;
}
ellers {
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("]version:");
v4.append(arg9.iGX.iEn);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]opgraderings_type:");
v4.append(arg9.iGX.mMode);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]tvungen_flag:");
v4.append(arg9.iGX.iEo);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]stille_mode:");
v4.append(arg9.iGX.iDQ);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]stille_type:");
v4.append(arg9.iGX.iEr);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]stille_status:");
v4.append(arg9.iGX.iEp);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]stille_fil:");
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_gruppe:");
v4.append(arg9.mDownloadGroup);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]download_sti:");
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;
hvis(v3_2 != null && v3_2.size() != 0) {
Iterator v3_3 = v3_2.iterator();
mens(v3_3.hasNext()) {
Object v4_1 = v3_3.next();
StringBuilder v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]komponent_navn:");
v5.append(((au)v4_1).getName());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]komponent_ver_navn:");
v5.append(((au)v4_1).aDA());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]komponent_ver_kode:");
v5.append(((au)v4_1).gBl);
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]komponent_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();
mens(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();
}
hvis(v3 == null) {
this.iGY.iGI.a(arg9, "up_encode", "ja", "fejl");
return;
}
v4_5 = this.iGY.iGw ? 0x1F : 0;
hvis(v3 == null) {
}
ellers {
v3 = g.i(v4_5, v3);
hvis(v3 == null) {
}
ellers {
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);
}
}
hvis(v1 == null) {
this.iGY.iGI.a(arg9, "up_encrypt", "ja", "fejl");
return;
}
hvis(TextUtils.isEmpty(this.iGY.mUpgradeUrl)) {
this.iGY.iGI.a(arg9, "up_url", "ja", "fejl");
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", "ja", "succes");
this.iGY.iGI.b(arg9);
}Her ser vi dannelsen af en POST-forespørgsel. Vær opmærksom på oprettelsen af et array af 16 bytes og dets udfyldning: 0x5F, 0, 0x1F, -50 (=0xCE). Det svarer til det, vi så i den tidligere forespørgsel.
I denne klasse kan vi bemærke en indlejret klasse, hvor der er en anden interessant metode:
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");
}
} Metoden får et byte array som input og kontrollerer, at nul-bytet er lig med 0x60 eller at tredje byte er lig med 0xD0, og at anden byte er 1, 11 eller 0x1F. Lad os se på svaret fra serveren: nul-bytet er 0x60, anden er 0x1F, tredje er 0x60. Det ligner, hvad vi har brug for. Ifølge linjerne ("up_decrypt", for eksempel) burde der kaldes en metode, der dekrypterer svaret fra serveren.
Lad os gå videre til metoden g.j. Vi bemærker, at som første argument sendes en byte fra offset 2 (dvs. 0x1F i vores tilfælde), mens svaret fra serveren sendes uden
de første 16 bytes.
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;
} Åbenlyst vælger man her dekrypteringsalgoritmen, og den pågældende byte, der i vores
i tilfælde er lig med 0x1F, angiver en af tre mulige varianter.
Vi fortsætter kodens analyse. Efter et par spring ankommer vi til en metode med et beskrivende navn decryptBytesByKey.
Her adskilles endnu to bytes fra vores svar, og der dannes en streng ud fra disse. Det er klart, at denne metode bruges til at vælge en nøgle til dekryptering af meddelelsen.
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 bytes
System.arraycopy(bytes, 0, prefix, 0, prefix.length);
String keyId = c.ayR().d(ByteBuffer.wrap(prefix).getShort()); // Valg af nøgle
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;
}For at springe frem, er det værd at bemærke, at man på dette stadium ikke får nøglen, men kun dens "identifikator". At få nøglen er lidt mere kompliceret.
I den næste metode tilføjes der to flere parametre til de eksisterende, hvilket gør dem til fire: det magiske tal 16, nøgleidentifikatoren, de krypterede data og en uforståelig streng (i vores tilfælde tom).
public final byte[] l(String keyId, byte[] encrypted) throws SecException {
return this.ayJ().staticBinarySafeDecryptNoB64(16, keyId, encrypted, "");
}Efter en række overgange når vi frem til metoden staticBinarySafeDecryptNoB64 af interface com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. I applikationens hovedkode er der ingen klasser, der implementerer dette interface. En sådan klasse findes i filen lib/armeabi-v7a/libsgmain.so, som faktisk ikke er .so, men .jar. Den metode, vi er interesseret i, er implementeret på følgende måde:
pakke com.alibaba.wireless.security.a.i;
// ...
offentlig klasse a implementerer IStaticDataEncryptComponent {
privat ISecurityGuardPlugin a;
// ...
privat 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});
}
// ...
privat byte[] b(int magicInt, String keyId, byte[] encrypted, String magicString) {
return this.a(2, magicInt, 0, keyId, encrypted, magicString);
}
// ...
offentlig byte[] staticBinarySafeDecryptNoB64(int magicInt, String keyId, byte[] encrypted, String magicString) kaster SecException {
hvis(keyId != null && keyId.length() > 0 && magicInt >= 0 && magicInt 0) {
return this.b(magicInt, keyId, encrypted, magicString);
}
kast ny SecException("", 301);
}
//...
} Her suppleres vores parameterliste med to heltal: 2 og 0. Ifølge
alt at dømme, betyder 2 dekryptering, som i metoden doFinal af systemklassen javax.crypto.Cipher. Og alt dette sendes til en eller anden Router med tallet 10601 - det er tilsyneladende kommandonummeret.
Efter endnu et sæt overgangsforløb finder vi klassen, som implementerer interfacet IRouterComponent og metoden doCommand:
pakke com.alibaba.wireless.security.mainplugin;
import com.alibaba.wireless.security.framework.IRouterComponent;
import com.taobao.wireless.security.adapter.JNICLibrary;
offentlig klasse a implementerer IRouterComponent {
offentlig a() {
super();
}
offentlig Object doCommand(int arg2, Object[] arg3) {
return JNICLibrary.doCommandNative(arg2, arg3);
}
}Og også klassen JNICLibrary, hvor den native metode doCommandNative:
pakke com.taobao.wireless.security.adapter;
offentlig klasse JNICLibrary {
offentlig statisk native Object doCommandNative(int arg0, Object[] arg1);
}Så vi skal finde metoden i den native kode. doCommandNative. Og her begynder det sjove.
Obfuskation af maskinkoden
I filen libsgmain.so (som faktisk er .jar, og hvor vi ovenfor fandt implementeringen af nogle interface relateret til kryptering) er der et native bibliotek: libsgmainso-6.4.36.so. Vi åbner den i IDA og får en masse dialogvinduer med fejl. Problemet er, at sektionstabel (section header table) - ikke er gyldig. Dette er gjort med vilje for at komplicere analysen.

Men det er ikke nødvendigt: For at indlæse en ELF-fil korrekt og analysere den, er det ganske tilstrækkeligt med programheader tabel (program header table). Derfor fjerner vi ganske enkelt sektionstabellen ved at nulstille de relevante felter i headeren.

Vi åbner filen igen i IDA.
Der er to måder at informere den virtuelle Java-maskine om, hvor i den native biblioteksimplementering metoden, der er erklæret i Java-koden som native, findes. Den første - at give den et navn af typen Java_package_name_ClassName_methodName.
Den anden — at registrere det ved indlæsning af biblioteket (i funktionen JNI_OnLoad)
ved hjælp af et funktionskald RegisterNatives.
I vores tilfælde, hvis vi bruger den første metode, skal navnet være som følger: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.
Blandt de eksporterede funktioner findes sådanne ikke, så vi skal lede efter et kald RegisterNatives.
Vi går ind i funktionen JNI_OnLoad og ser et sådant billede:

Hvad foregår der her? Ved første øjekast er starten og slutningen af funktionen typisk for ARM-arkitekturen. Den første instruktion gemmer indholdet af de registre, som funktionen vil bruge i sit arbejde (i dette tilfælde R0, R1 og R2), samt indholdet af registret LR, hvor returneringsadressen fra funktionen er. Den sidste instruktion gendanner de gemte registre, hvor adressen til returen straks placeres i registret PC — således sker der en tilbagevenden fra funktionen. Men ved nærmere eftersyn kan man bemærke, at næstsidste instruktion ændrer returneringsadressen gemt på stakken. Lad os beregne, hvad den vil være efter
udførelsen af koden. I R1 indlæses en adresse 0xB130, der trækkes 5 fra, derefter flyttes den til R0, og der tilføjes 0x10. Det resulterer i 0xB13B. Således tror IDA, at den sidste instruktion udfører et almindeligt return fra funktionen, mens der faktisk sker et spring til den beregnede adresse 0xB13B.
Det skal nævnes, at ARM-processorer har to tilstande og to instruktionssæt: ARM og Thumb. Den laveste bit af adressen fortæller processoren, hvilket instruktionssæt der anvendes. Dvs. adressen er faktisk 0xB13A, og 1'eren i den laveste bit angiver Thumb-tilstanden.
I starten af hver funktion i dette bibliotek er der tilføjet en sådan "adapter" og
affaldskode. Lad os ikke dvæle mere ved dem – husk blot,
at den faktiske start af næsten alle funktioner ligger lidt længere fremme.
Da der ikke er et eksplicit spring til 0xB13A i koden, genkendte IDA ikke selv, at der er kode på dette sted. Af den samme årsag genkender den ikke størstedelen af koden i biblioteket som kode, hvilket gør analysen lidt vanskeligere. Vi fortæller IDA, at dette er kode, og her er hvad der sker:

Ved 0xB144 begynder en tabel tydeligt. Hvad med sub_494C?

Ved kald af denne funktion får vi adressen til den nævnte tabel (0xB144) i registret LR. I R0 — indekset i denne tabel. Dvs. værdien fra tabellen tages, tilføjes til LR, og resultatet opnås.
den adresse, som du skal navigere til. Vi prøver at beregne den: 0xB144 + [0xB144 + 8 * 4] = 0xB144 + 0x120 = 0xB264. Vi navigerer til den beregnede adresse og ser bogstaveligt talt et par nyttige instruktioner og igen en overgang til 0xB140:

Nu vil der være en overgang baseret på skiftet med indeks 0x20 fra tabellen.
I betragtning af tabellens størrelse vil der være mange sådanne overgange i koden. Spørgsmålet melder sig: kan vi på en eller anden måde automatisere dette, uden at skulle beregne adresser manuelt? Og til vores hjælp kommer scripts og muligheden for at patch'e koden i 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 "Forkert operandtype på", hex(ea1), "-", get_operand_type(ea1, 0), get_operand_type(ea1, 1)
table = None
if table is None:
print "Kunne ikke finde tabel"
else:
print "tabel =", hex(table)
offset = get_wide_dword(table + (index << 2))
put_unconditional_branch(ea, table + offset)
else:
print "Ukendt kode", get_operand_type(ea1, 0), get_operand_value(ea1, 0), get_operand_type(ea1, 1) == 2
else:
print "Kunne ikke opdage den første instruktion"Sæt markøren på linje 0xB26A, kør scriptet og se en overgang til 0xB4B0:

IDA genkendte igen ikke dette område som kode. Vi hjælper den og ser en anden konstruktion:

Instruktionerne efter BLX ser ikke særlig meningsfulde ud, det ligner mere et skift. Vi ser i sub_4964:

Og faktisk, her bliver der taget et dword fra den adresse, som ligger i LR, tilføjes til denne adresse, hvorefter værdien fra den resulterende adresse placeres på stakken. LR tilføjes også 4, så efter retur fra funktionen springes dette skift. Derefter henter kommandoen POP {R1} den resulterende værdi fra stakken. Hvis vi ser på, hvad der findes ved adressen 0xB4BA + 0xEA = 0xB5A4, kan vi se noget, der ligner en adresstabel:

For at modificere denne konstruktion skal to parametre hentes fra koden: offset og registreringsnummeret, hvor resultatet skal placeres. For hver mulig registrering skal der på forhånd forberedes et stykke kode.
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 "POP-instruktion ikke fundet"
else:
print "Forkert operandtype på +4:", get_operand_type(ea + 4, 0)
else:
print "Kan ikke registrere de første instruktioner"Placer markøren i starten af konstruktionen, som vi ønsker at erstatte — 0xB4B2 — og kør scriptet:

Ud over de allerede nævnte konstruktioner findes der også sådanne i koden:

Som i det foregående tilfælde er der et offset efter instruktionen BLX:

Tag offset fra adressen i LR, læg det til LR og gå dertil. 0x72044 + 0xC = 0x72050. Scriptet til denne konstruktion er ganske enkelt:
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 "Kan ikke registrere første instruktion"Resultatet af scriptets udførelse:

Når alt i funktionen er blevet patched, kan vi angive IDA til dens reelle begyndelse. Den vil samle hele funktionen i bidder, og den kan dekompileres med HexRays.
Dekryptering af strenge
Vi har lært at bekæmpe obfuskering af maskinkode i biblioteket libsgmainso-6.4.36.so fra UC Browser og har fået funktionen 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;
}Lad os se nærmere på følgende linjer:
sub_73E24(&unk_83EA6, &v6, 49);
clazz = (jclass)((int (__fastcall *)(JNIEnv *, int *))(*env)->FindClass)(env, &v6);I funktionen sub_73E24 findes der en åbenlys dekodning af klassens navn. Som parametre til denne funktion sendes en pegepind til data, der ligner krypterede data, en slags buffer og et tal. Det er tydeligt, at efter funktionen kaldes, vil bufferet indeholde den dekodede streng, da det sendes til funktionen FindClass, som tager klassens navn som det andet parameter. Det betyder, at tallet er størrelsen på bufferet eller længden af strengen. Lad os prøve at dekode klassens navn; det bør fortælle os, om vi er på rette vej. Lad os se nærmere på, hvad der sker i 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;
}Funktion sub_7AF78 opretter en beholderinstans til byte-arrays af den angivne størrelse (vi går ikke nærmere ind i disse beholdere). Her oprettes der to sådanne beholdere: den ene indeholder strengen «DcO/lcK+h?m3c*q@» (det er ikke svært at gætte, at dette er nøglen), mens den anden indeholder de krypterede data. Derefter placeres begge objekter i en struktur, som sendes til funktionen sub_6115C. Det skal også bemærkes, at der i denne struktur er et felt med værdien 3. Lad os se, hvad der sker med denne struktur bagefter.
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;
}Som er parameteren switch, sendes et felt i strukturen, som tidligere blev tildelt værdien 3. Ser vi på case 3: i funktionen sub_6364C sendes parametrene fra strukturen, som blev lagt der i den forrige funktion, dvs. nøgle og krypterede data. Hvis vi ser nøje på sub_6364C, kan vi finde algoritmen RC4 i den.
Vi har algoritmen og nøglen. Lad os forsøge at dekryptere klassens navn. Sådan gik det: com/taobao/wireless/security/adapter/JNICLibrary. Fantastisk! Vi er på rette spor.
Kommandotræet
Nu skal vi finde kaldet RegisterNatives, som vil pege os på funktionen doCommandNative. Vi gennemgår funktioner, der kaldes fra JNI_OnLoad, og finder den i 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;
}Og faktisk bliver der registreret en nativen metode med navnet doCommandNative. Nu kender vi dens adresse. Lad os se, hvad den gør.
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;
}Ud fra navnet kan man gætte, at her findes indgangspunktet for alle funktioner, som udviklerne har besluttet at overføre til den nativen biblioteks. Vi er interesserede i funktionen med nummer 10601.
Fra koden kan man se, at fra kommandoens nummer fås tre tal: command / 10000, command % 10000 / 100 og command % 10, dvs. i vores tilfælde 1, 6 og 1. Disse tre tal samt en peger på JNIEnv og de argumenter, der blev givet til funktionen, samles i en struktur og sendes videre. Ved hjælp af de tre opnåede tal (lad os kalde dem N1, N2 og N3) opbygges kommandotræet.
Cirka sådan:

Træet fyldes dynamisk i JNI_OnLoad.
Tre tal koderer stien i træet. Hver blad i træet indeholder den tilsvarende funktions adresser. Nøglen er i forælder-knuden. At finde stedet i koden, hvor den ønskede funktion tilføjes til træet, er ikke vanskeligt, hvis man forstår de anvendte strukturer (deres beskrivelse er ikke inkluderet for at undgå at udvide en allerede stor artikel).
Yderligere obfuskation
Vi har fået adressen til den funktion, der skal dekryptere trafikken: 0x5F1AC. Men glæden er kortlivet: udviklerne af UC Browser har forberedt en overraskelse til os.
Efter at have fået parametrene fra det array, der blev dannet i Java-koden, rammer vi
funktionen på adressen 0x4D070. Og her venter der os endnu en form for obfuskation af koden.
Vi lægger to indekser i R7 og R4:

Vi flytter den første indeks til R11:

For at få adressen fra tabellen bruger vi indekset:

Efter at have gået til den første adresse bruges det andet indeks, der er i R4. Der er 230 elementer i tabellen.
Hvad skal vi gøre med dette? Vi kan sige til IDA, at det er en switch: Rediger -> Andet -> Angiv switch idiom.

Den resulterende kode er skræmmende. Men ved at navigere gennem dens labyrinter kan man bemærke et kald til den allerede kendte funktion. sub_6115C:

Der var en switch, hvor der i case 3 var dekryptering ved hjælp af RC4-algoritmen. Og i dette tilfælde udfyldes den struktur, der videregives til funktionen, med parametrene, der er givet i doCommandNative. Vi husker, at vi havde der magicInt med værdien 16. Vi ser på den tilsvarende case – og efter flere overgange finder vi koden, hvor vi kan identificere algoritmen.

Det er AES!
Algoritmen er der, nu skal vi få dens parametre: tilstand, nøgle og muligvis initialiseringsvektoren (dens tilstedeværelse afhænger af, hvordan AES-algoritmen fungerer). Strukturen med disse må derfor dannes et sted før kaldet til funktionen sub_6115C, men denne del af koden er særlig godt obfuskert, så ideen opstår om at patche koden, så alle dekrypteringsfunktionens parametre dumpes til en fil.
Patch
For ikke at skulle skrive hele patchkoden i assemblysprog manuelt, kan man starte Android Studio, skrive en funktion, der tager de samme parametre som vores dekrypteringsfunktion, og skriver til en fil, hvorefter man kan copy-paste koden, som kompilatoren genererer.
Vore om bekvemmeligheden ved at tilføje kode har vores venner fra UC Browser-teamet også "taget sig af" det. Vi husker, at der i starten af hver funktion er noget skraldede kode, der nemt kan erstattes med en anden. Meget praktisk 🙂 Problemet er, at der i starten af den målrettede funktion ikke er meget plads til koden, der gemmer alle parametrene i filen. Vi måtte derfor opdele den i dele og bruge skraldebokse fra de nærliggende funktioner. Det blev til fire dele i alt.
Første del:

I ARM-arkitekturen overføres de første fire funktionsparametre gennem registrene R0-R3, de øvrige, hvis der er nogen — gennem stakken. Adressen til tilbagevenden sendes i LR-registeret. Alt dette skal gemmes for at funktionen kan køre efter vi har dumpet dens parametre. Vi skal også gemme alle de registre, vi vil bruge i processen, så vi udfører PUSH.W {R0-R10,LR}. I R7 får vi adressen til listen med parametre, der er sendt til funktionen via stakken.
Ved hjælp af funktionen fopen åbner vi filen /data/local/tmp/aes i "ab"-mode,
dvs. til tilføjelse. I R0 indlæser vi adressen til filnavnet, i R1 — adressen til strengen med angivelse af mode. Og her slutter skraldekoden, så vi går videre til den næste funktion. For at den kan fortsætte med at arbejde, sætter vi i starten et spring til den rigtige kode i funktionen, omgå skraldet, og i stedet for skraldet tilføjer vi fortsættelsen af patchen.

Vi kalder fopen.
De første tre parametre i funktionen aes har typen int. Da vi i starten gemte registrene i stakken, kan vi blot overføre funktionens fwrite adresser i stakken.

Derefter har vi tre strukturer, som indeholder datastørrelse og en pointer til data for nøglen, initialiseringsvektoren og de krypterede data.

Til sidst lukker vi filen, gendanner registrene og overfører kontrollen til den rigtige funktion. aes.
Vi bygger APK'en med den patchede biblioteksfil, signerer den, uploader den til enheden/emulatoren og kører den. Vi ser, at vores dump bliver oprettet, og der skrives mange data dertil. Browseren bruger kryptering ikke kun til trafikken, og al kryptering går gennem den betragtede funktion. Men de nødvendige data er der af en eller anden grund ikke, og den ønskede forespørgsel ses ikke i trafikken. For ikke at vente på, at UC Browser vil foretage den nødvendige forespørgsel, vil vi tage det krypterede svar fra serveren, som blev modtaget tidligere, og patch igen applikationen: tilføje dekrypteringen i onCreate af hovedaktiviteten.
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;)IVi samler, underskriver, installerer, starter. Vi får en NullPointerException, fordi metoden returnerede null.
I løbet af den videre analyse af koden blev der opdaget en funktion, der dekrypterer interessante strenge: «META-INF/» og «.RSA». Det ser ud til, at applikationen kontrollerer sit certifikat. Eller endda genererer nøgler ud fra det. At finde ud af, hvad der sker med certifikatet, har jeg absolut ingen lyst til, så vi vil bare give det det rigtige certifikat. Vi vil patch'e den krypterede streng, så i stedet for «META-INF/» bliver det «BLABLINF/», oprette en mappe med dette navn i APK'en og lægge certifikatet for webbrowseren der.
Vi samler, underskriver, installerer, starter. Bingo! Nøglen er vores!
MitM
Vi har fået nøglen og initialiseringsvektoren, der er lig med nøglen. Lad os prøve at dekryptere svaret fra serveren i CBC-modus.

Vi ser URL'en til arkivet, noget der ligner MD5, «extract_unzipsize» og et tal. Vi tjekker: MD5 for arkivet stemmer overens, størrelsen på den udpakkede biblioteker stemmer overens. Lad os prøve at patch'e dette bibliotek og give det til browseren. For at vise, at vores patched bibliotek er blevet indlæst, vil vi sende en Intent til at oprette en SMS med teksten «PWNED!». Vi vil ændre to svar fra serveren: og til download af arkivet. I den første ændrer vi MD5 (størrelsen efter udpakning ændres ikke), i den anden giver vi et arkiv med det patched bibliotek.
Browseren forsøger flere gange at downloade arkivet, hvorefter den viser en fejl. Åbenbart er der noget
den ikke kan lide. Som et resultat af analysen af dette uklare format viste det sig, at serveren også sender størrelsen af arkivet:

Den er kodet i LEB128. Efter patch'en ændrede størrelsen på arkivet med bibliotekerne sig en smule, så browseren mente, at arkivet var blevet downloadet fejlbehæftede, og efter flere forsøg viste den en fejl.
Vi retter størrelsen på arkivet… Og – sejr! 🙂 Resultatet på video.
Konsekvenserne og reaktionen fra udvikleren
På samme måde kunne hackere bruge den usikre funktion i UC Browser til at sprede og køre skadelige biblioteker. Disse biblioteker ville arbejde i browserens kontekst og dermed få alle dens systemtilladelser. Som følge heraf ville de have mulighed for at vise phishing-vinduer samt adgang til arbejdsfiler fra den orange kinesiske eger, inklusive loginoplysninger, adgangskoder og cookies lagret i databasen.
Vi kontaktede udviklerne af UC Browser og informede dem om det fundne problem, forsøgte at påpege sårbarheden og dens fare, men de ville ikke diskutere noget med os. I mellemtiden fortsatte browseren med at vise den farlige funktion tydeligt for alle. Men så snart vi afslørede detaljerne om sårbarheden, kunne det ikke længere ignoreres som før. Den 27. marts blev
en ny version af UC Browser 12.10.9.1193 frigivet, som henvendte sig til serveren via HTTPS: .
Derudover, efter 'rettelsen' og indtil skrivningen af denne artikel, førte et forsøg på at åbne en PDF i browseren til en fejlmeddelelse med teksten 'Ups, noget gik galt!'. Anmodningen til serveren ved forsøg på at åbne PDF'en blev ikke udført, men anmodningen ved opstart af browseren blev udført, hvilket antyder en bevaret mulighed for at uploade eksekverbar kode i strid med Google Plays regler.
Kilde: habr.com
