Hledání zranitelností v prohlížeči UC

Hledání zranitelností v prohlížeči UC

úvod

Na konci března jsme hlášeno, že objevili skrytou schopnost načíst a spustit neověřený kód v prohlížeči UC. Dnes se podrobně podíváme na to, jak k tomuto stahování dochází a jak jej mohou hackeři využít pro své vlastní účely.

Před časem byl UC Browser propagován a distribuován velmi agresivně: byl instalován na zařízení uživatelů pomocí malwaru, distribuovaného z různých stránek pod rouškou video souborů (tj. uživatelé si mysleli, že stahují například porno video, ale místo toho obdržel APK s tímto prohlížečem), používal děsivé bannery se zprávami, že prohlížeč je zastaralý, zranitelný a podobně. V oficiální skupině UC Browser na VK existuje téma, ve kterých si uživatelé mohou stěžovat na nekalou reklamu, příkladů je tam mnoho. V roce 2016 došlo dokonce videoreklama v ruštině (ano, reklama na prohlížeč blokující reklamy).

V době psaní tohoto článku má UC Browser na Google Play více než 500 000 000 instalací. To je působivé – pouze Google Chrome má více. Mezi recenzemi můžete vidět poměrně hodně stížností na reklamu a přesměrování na některé aplikace na Google Play. To byl důvod našeho výzkumu: rozhodli jsme se zjistit, zda UC Browser nedělá něco špatného. A ukázalo se, že ano!

V kódu aplikace byla objevena možnost stáhnout a spustit spustitelný kód, což je v rozporu s pravidly pro publikování aplikací na Google Play. Kromě stahování spustitelného kódu to UC Browser dělá nezabezpečeným způsobem, který lze použít ke spuštění útoku MitM. Uvidíme, zda dokážeme provést takový útok.

Vše napsané níže je relevantní pro verzi prohlížeče UC, která byla v době studie k dispozici na Google Play:

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

Vektor útoku

V manifestu prohlížeče UC můžete najít službu se samovysvětlujícím názvem com.uc.deployment.UpgradeDeployService.

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

Když se tato služba spustí, prohlížeč odešle požadavek POST puds.ucweb.com/upgrade/index.xhtml, který je vidět v provozu nějakou dobu po startu. Jako odpověď může obdržet příkaz ke stažení nějaké aktualizace nebo nového modulu. Během analýzy server takové příkazy nedával, ale všimli jsme si, že když se pokusíme otevřít PDF v prohlížeči, provede druhý požadavek na výše uvedenou adresu a poté stáhne nativní knihovnu. K provedení útoku jsme se rozhodli využít tuto funkci prohlížeče UC: možnost otevřít PDF pomocí nativní knihovny, která není v APK a kterou si v případě potřeby stáhne z internetu. Za zmínku stojí, že teoreticky může být UC Browser nucen něco stáhnout bez interakce uživatele - pokud poskytnete dobře formovanou odpověď na požadavek, který se provede po spuštění prohlížeče. K tomu ale potřebujeme podrobněji prostudovat protokol interakce se serverem, takže jsme se rozhodli, že bude jednodušší upravit zachycenou odpověď a nahradit knihovnu pro práci s PDF.

Když tedy uživatel chce otevřít PDF přímo v prohlížeči, mohou být v provozu vidět následující požadavky:

Hledání zranitelností v prohlížeči UC

Nejprve je požadavek POST na puds.ucweb.com/upgrade/index.xhtml, pak
Je stažen archiv s knihovnou pro prohlížení PDF a kancelářských formátů. Je logické předpokládat, že první požadavek přenese informace o systému (alespoň architektuře pro poskytnutí požadované knihovny) a jako odpověď na něj prohlížeč obdrží nějakou informaci o knihovně, kterou je potřeba stáhnout: adresu a popř. , něco jiného. Problém je v tom, že tento požadavek je šifrovaný.

Fragment žádosti

Fragment odpovědi

Hledání zranitelností v prohlížeči UC

Hledání zranitelností v prohlížeči UC

Samotná knihovna je zabalena do ZIP a není šifrována.

Hledání zranitelností v prohlížeči UC

Vyhledejte kód pro dešifrování provozu

Zkusme dešifrovat odpověď serveru. Podívejme se na kód třídy com.uc.deployment.UpgradeDeployService: z metody onStartCommand jít do com.uc.deployment.bxa od toho 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);
}

Vidíme zde vytvoření požadavku POST. Dbáme na vytvoření pole 16 bajtů a jeho naplnění: 0x5F, 0, 0x1F, -50 (=0xCE). Shoduje se s tím, co jsme viděli v žádosti výše.

Ve stejné třídě můžete vidět vnořenou třídu, která má další zajímavou metodu:

        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 bere jako vstup pole bajtů a kontroluje, zda je nulový bajt 0x60 nebo třetí bajt je 0xD0 a druhý bajt je 1, 11 nebo 0x1F. Podíváme se na odpověď ze serveru: nulový bajt je 0x60, druhý je 0x1F, třetí je 0x60. Zní to jako to, co potřebujeme. Soudě podle řádků (například „up_decrypt“) by zde měla být zavolána metoda, která dešifruje odpověď serveru.
Přejděme k metodě gj. Všimněte si, že první argument je bajt na offsetu 2 (tj. 0x1F v našem případě) a druhý je odpověď serveru bez
prvních 16 bajtů.

     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;
}

Je zřejmé, že zde vybereme dešifrovací algoritmus a stejný bajt, který je v našem
případ rovný 0x1F, označuje jednu ze tří možných možností.

Pokračujeme v analýze kódu. Po pár skocích se ocitáme v metodě se samozřejmým názvem decryptBytesByKey.

Zde jsou od naší odpovědi odděleny další dva bajty a z nich je získán řetězec. Je jasné, že tímto způsobem je vybrán klíč pro dešifrování zprávy.

    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;
}

Při pohledu do budoucna si všimneme, že v této fázi ještě nezískáváme klíč, ale pouze jeho „identifikátor“. Získání klíče je trochu složitější.

V další metodě se ke stávajícím přidají další dva parametry, čímž se vytvoří čtyři: magické číslo 16, identifikátor klíče, zašifrovaná data a nesrozumitelný řetězec (v našem případě prázdný).

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

Po sérii přechodů se dostáváme k metodě staticBinarySafeDecryptNoB64 rozhraní com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. V kódu hlavní aplikace nejsou žádné třídy, které implementují toto rozhraní. V souboru je taková třída lib/armeabi-v7a/libsgmain.so, což ve skutečnosti není .so, ale .jar. Metoda, která nás zajímá, je implementována takto:

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);
}
//...
}

Zde je náš seznam parametrů doplněn o další dvě celá čísla: 2 a 0. Soudě podle
všechno, 2 znamená dešifrování, jako v metodě doFinal systémová třída javax.crypto.Cipher. A to vše se přenese na jistý Router s číslem 10601 – to je zřejmě číslo příkazu.

Po dalším řetězci přechodů najdeme třídu, která implementuje rozhraní IRouterComponent a způsob doCommand:

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é třída JNICLibrary, ve kterém je deklarována nativní metoda doCommandNative:

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

To znamená, že musíme najít metodu v nativním kódu doCommandNative. A tady začíná zábava.

Zatemnění strojového kódu

V souboru libsgmain.so (což je ve skutečnosti .jar a ve kterém jsme našli implementaci některých rozhraní souvisejících se šifrováním výše) existuje jedna nativní knihovna: libsgmainso-6.4.36.so. Otevřeme jej v IDA a získáme spoustu dialogových oken s chybami. Problém je v tom, že tabulka záhlaví oddílu je neplatná. To se provádí záměrně, aby se analýza zkomplikovala.

Hledání zranitelností v prohlížeči UC

Není to ale potřeba: ke správnému načtení souboru ELF a jeho analýze stačí tabulka záhlaví programu. Proto jednoduše vymažeme tabulku oddílů a vynulujeme odpovídající pole v záhlaví.

Hledání zranitelností v prohlížeči UC

Znovu otevřete soubor v IDA.

Existují dva způsoby, jak sdělit virtuálnímu stroji Java, kde přesně se v nativní knihovně nachází implementace metody deklarované v kódu Java jako nativní. První je dát mu druhové jméno Java_package_name_ClassName_MethodName.

Druhým je zaregistrovat ji při načítání knihovny (ve funkci JNI_OnLoad)
pomocí volání funkce RegisterNatives.

V našem případě, pokud použijeme první metodu, název by měl být takto: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

Mezi exportovanými funkcemi žádná taková funkce není, což znamená, že musíte hledat volání RegisterNatives.
Pojďme k funkci JNI_OnLoad a vidíme tento obrázek:

Hledání zranitelností v prohlížeči UC

Co se tam děje? Na první pohled je začátek a konec funkce typický pro architekturu ARM. První instrukce na zásobníku ukládá obsah registrů, které funkce použije při své činnosti (v tomto případě R0, R1 a R2), a také obsah registru LR, který obsahuje návratovou adresu z funkce. . Poslední instrukce obnoví uložené registry a návratová adresa je okamžitě umístěna do registru PC - tedy návrat z funkce. Ale když se podíváte pozorně, všimnete si, že předposlední instrukce mění návratovou adresu uloženou v zásobníku. Pojďme si spočítat, jaké to bude potom
provádění kódu. Do R1 se načte určitá adresa 0xB130, od ní se odečte 5, poté se přenese do R0 a k ní se přičte 0x10. Ukázalo se, že 0xB13B. IDA si tedy myslí, že poslední instrukce je normální návrat funkce, ale ve skutečnosti jde na vypočítanou adresu 0xB13B.

Zde stojí za to připomenout, že procesory ARM mají dva režimy a dvě sady instrukcí: ARM a Thumb. Nejméně významný bit adresy říká procesoru, která instrukční sada je používána. To znamená, že adresa je ve skutečnosti 0xB13A a jeden v nejméně významném bitu označuje režim Thumb.

Podobný „adaptér“ byl přidán na začátek každé funkce v této knihovně a
odpadkový kód. Nebudeme se jimi dále podrobně zabývat - jen si vzpomínáme
že skutečný začátek téměř všech funkcí je o něco dále.

Vzhledem k tomu, že kód explicitně neskočí na 0xB13A, IDA sama nerozpoznala, že se kód nachází na tomto místě. Ze stejného důvodu nerozpozná většinu kódu v knihovně jako kód, což poněkud ztěžuje analýzu. Říkáme IDA, že toto je kód, a stane se toto:

Hledání zranitelností v prohlížeči UC

Tabulka jasně začíná na 0xB144. Co je v sub_494C?

Hledání zranitelností v prohlížeči UC

Při volání této funkce v registru LR získáme adresu dříve zmíněné tabulky (0xB144). V R0 - index v této tabulce. To znamená, že se hodnota vezme z tabulky, přičte se k LR a výsledek je
adresu, kam jít. Zkusme to spočítat: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Jdeme na přijatou adresu a vidíme doslova pár užitečných pokynů a znovu jdeme na 0xB140:

Hledání zranitelností v prohlížeči UC

Nyní bude přechod na offset s indexem 0x20 z tabulky.

Soudě podle velikosti tabulky bude takových přechodů v kódu mnoho. Nabízí se otázka, zda je možné se s tím nějak vypořádat automatickyji, bez ručního počítání adres. A skripty a schopnost opravovat kód v IDA nám pomáhají:

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"

Umístěte kurzor na řádek 0xB26A, spusťte skript a uvidíte přechod na 0xB4B0:

Hledání zranitelností v prohlížeči UC

IDA opět nerozpoznala tuto oblast jako kód. Pomáháme jí a vidíme tam další design:

Hledání zranitelností v prohlížeči UC

Návod po BLX jako by nedával moc smysl, spíš je to takový nějaký posun. Podívejme se na sub_4964:

Hledání zranitelností v prohlížeči UC

A skutečně, zde se vezme dword na adrese ležící v LR, přidá se k této adrese, načež se vezme hodnota na výsledné adrese a uloží se do zásobníku. K LR je také přidána 4, takže po návratu z funkce je stejný offset přeskočen. Poté příkaz POP {R1} převezme výslednou hodnotu ze zásobníku. Pokud se podíváte na to, co se nachází na adrese 0xB4BA + 0xEA = 0xB5A4, uvidíte něco podobného jako tabulka adres:

Hledání zranitelností v prohlížeči UC

Pro opravu tohoto návrhu budete muset z kódu získat dva parametry: offset a číslo registru, do kterého chcete vložit výsledek. Pro každý možný registr si budete muset předem připravit kus kódu.

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"

Umístíme kurzor na začátek struktury, kterou chceme nahradit - 0xB4B2 - a spustíme skript:

Hledání zranitelností v prohlížeči UC

Kromě již zmíněných struktur obsahuje kód také následující:

Hledání zranitelností v prohlížeči UC

Stejně jako v předchozím případě je po instrukci BLX offset:

Hledání zranitelností v prohlížeči UC

Vezmeme offset na adresu z LR, přidáme do LR a jedeme tam. 0x72044 + 0xC = 0x72050. Skript pro tento design je poměrně jednoduchý:

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"

Výsledek spuštění skriptu:

Hledání zranitelností v prohlížeči UC

Jakmile je vše ve funkci záplatováno, můžete nasměrovat IDA na její skutečný začátek. Dá dohromady veškerý funkční kód a lze jej dekompilovat pomocí HexRays.

Dekódovací řetězce

Naučili jsme se vypořádat se s matením strojového kódu v knihovně libsgmainso-6.4.36.so z prohlížeče UC a obdrželi kód funkce 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;
}

Podívejme se blíže na následující řádky:

  sub_73E24(&unk_83EA6, &v6, 49);
clazz = (jclass)((int (__fastcall *)(JNIEnv *, int *))(*env)->FindClass)(env, &v6);

Ve funkci sub_73E24 název třídy je jasně dešifrován. Jako parametry této funkce je předán ukazatel na data podobná šifrovaným datům, určitá vyrovnávací paměť a číslo. Je zřejmé, že po zavolání funkce bude ve vyrovnávací paměti dešifrovaný řádek, protože je předán funkci FindClass, který má jako druhý parametr název třídy. Číslo je tedy velikost vyrovnávací paměti nebo délka řádku. Zkusme rozluštit název třídy, měl by nám napovědět, zda jdeme správným směrem. Podívejme se blíže na to, co se děje v 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;
}

Funkce sub_7AF78 vytvoří instanci kontejneru pro bajtová pole zadané velikosti (těmito kontejnery se nebudeme podrobně zabývat). Zde jsou vytvořeny dva takové kontejnery: jeden obsahuje řádek "DcO/lcK+h?m3c*q@" (lze snadno uhodnout, že se jedná o klíč), druhý obsahuje zašifrovaná data. Dále jsou oba objekty umístěny do určité struktury, která je předána funkci sub_6115C. Označme v této struktuře také pole s hodnotou 3. Podívejme se, co se s touto strukturou stane dále.

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 přepínače je pole struktury, kterému byla dříve přiřazena hodnota 3. Podívejte se na případ 3: funkce sub_6364C parametry se předávají ze struktury, která tam byla přidána v předchozí funkci, tedy klíč a zašifrovaná data. Pokud se podíváte pozorně sub_6364C, můžete v něm rozpoznat algoritmus RC4.

Máme algoritmus a klíč. Zkusme rozluštit název třídy. Co se stalo: com/taobao/wireless/security/adapter/JNICLibrary. Skvělý! Jsme na správné cestě.

Příkazový strom

Nyní musíme najít výzvu RegisterNatives, která nás navede na funkci doCommandNative. Podívejme se na funkce volané z JNI_OnLoad, a najdeme to v 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;
}

A skutečně je zde registrována nativní metoda s názvem doCommandNative. Nyní známe jeho adresu. Uvidíme, co udělá.

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;
}

Podle názvu můžete hádat, že zde je vstupní bod všech funkcí, které se vývojáři rozhodli přenést do nativní knihovny. Máme zájem o funkci číslo 10601.

Z kódu můžete vidět, že číslo příkazu vytváří tři čísla: příkaz/10000, příkaz % 10000 / 100 и příkaz % 10, tedy v našem případě 1, 6 a 1. Tato tři čísla, stejně jako ukazatel na JNIEnv a argumenty předané funkci jsou přidány do struktury a předány dále. Pomocí tří získaných čísel (označme je N1, N2 a N3) se sestaví strom příkazů.

Něco takového:

Hledání zranitelností v prohlížeči UC

Strom se dynamicky vyplňuje JNI_OnLoad.
Tři čísla kódují cestu ve stromu. Každý list stromu obsahuje popsanou adresu odpovídající funkce. Klíč je v nadřazeném uzlu. Najít v kódu místo, kde je do stromu přidána funkce, kterou potřebujeme, není těžké, pokud rozumíte všem použitým strukturám (nepopisujeme je, abychom nenadýmali už tak dost rozsáhlý článek).

Více zmatku

Obdrželi jsme adresu funkce, která by měla dešifrovat provoz: 0x5F1AC. Ale je příliš brzy na to, abychom se radovali: vývojáři UC Browser pro nás připravili další překvapení.

Po obdržení parametrů z pole, které bylo vytvořeno v kódu Java, dostaneme
do funkce na adrese 0x4D070. A zde nás čeká další typ zamlžování kódu.

Do R7 a R4 jsme vložili dva indexy:

Hledání zranitelností v prohlížeči UC

Přesuneme první index na R11:

Hledání zranitelností v prohlížeči UC

Chcete-li získat adresu z tabulky, použijte index:

Hledání zranitelností v prohlížeči UC

Po přechodu na první adresu se použije druhý index, který je v R4. V tabulce je 230 prvků.

co s tím dělat? IDA můžete říci, že se jedná o přepínač: Upravit -> Jiné -> Zadat idiom přepínače.

Hledání zranitelností v prohlížeči UC

Výsledný kód je děsivý. Ale když se prodíráte jeho džunglí, můžete si všimnout volání funkce, která je nám již známá sub_6115C:

Hledání zranitelností v prohlížeči UC

Byl tam přepínač, ve kterém v případě 3 došlo k dešifrování pomocí algoritmu RC4. A v tomto případě je struktura předaná funkci vyplněna z parametrů předávaných do doCommandNative. Pojďme si připomenout, co jsme tam měli magicInt s hodnotou 16. Podíváme se na odpovídající případ - a po několika přechodech najdeme kód, podle kterého lze algoritmus identifikovat.

Hledání zranitelností v prohlížeči UC

Toto je AES!

Algoritmus existuje, zbývá pouze získat jeho parametry: režim, klíč a případně inicializační vektor (jeho přítomnost závisí na provozním režimu algoritmu AES). Struktura s nimi musí být vytvořena někde před voláním funkce sub_6115C, ale tato část kódu je obzvláště dobře zamlžená, takže vyvstává nápad opravit kód tak, aby všechny parametry dešifrovací funkce byly uloženy do souboru.

Náplast

Abyste nemuseli psát celý opravný kód v jazyce symbolických instrukcí ručně, můžete spustit Android Studio, napsat tam funkci, která obdrží stejné vstupní parametry jako naše dešifrovací funkce a zapíše do souboru, a poté zkopírujte a vložte kód, který kompilátor vytvoří generovat.

O pohodlí při přidávání kódu se postarali i naši přátelé z týmu UC Browser. Připomeňme si, že na začátku každé funkce máme odpadkový kód, který lze snadno nahradit jakýmkoli jiným. Velmi pohodlné 🙂 Na začátku cílové funkce však není dostatek místa pro kód, který ukládá všechny parametry do souboru. Musel jsem to rozdělit na části a použít bloky odpadu ze sousedních funkcí. Celkem byly čtyři díly.

K první části:

Hledání zranitelností v prohlížeči UC

V architektuře ARM jsou první čtyři parametry funkce předávány přes registry R0-R3, zbývající, pokud existují, jsou předávány přes zásobník. Registr LR nese zpáteční adresu. To vše je potřeba uložit, aby funkce mohla fungovat poté, co vyklopíme její parametry. Musíme také uložit všechny registry, které budeme v procesu používat, takže uděláme PUSH.W {R0-R10,LR}. V R7 získáme adresu seznamu parametrů předávaných funkci přes zásobník.

Pomocí funkce otevřít otevřeme soubor /data/local/tmp/aes v režimu "ab".
tedy pro doplnění. V R0 načteme adresu názvu souboru, v R1 - adresu řádku označujícího režim. A zde odpadkový kód končí, takže přejdeme k další funkci. Aby to dál fungovalo, dáme na začátek přechod do reálného kódu funkce, obcházení smetí a místo smetí přidáme pokračování patche.

Hledání zranitelností v prohlížeči UC

voláme otevřít.

První tři parametry funkce aes mít typ int. Jelikož jsme registry na začátku uložili do zásobníku, můžeme funkci jednoduše předat fwrite jejich adresy v zásobníku.

Hledání zranitelností v prohlížeči UC

Dále máme tři struktury, které obsahují datovou velikost a ukazatel na data pro klíč, inicializační vektor a zašifrovaná data.

Hledání zranitelností v prohlížeči UC

Na konci soubor zavřete, obnovte registry a přeneste řízení do skutečné funkce aes.

Shromáždíme soubor APK s opravenou knihovnou, podepíšeme jej, nahrajeme do zařízení/emulátoru a spustíme. Vidíme, že se vytváří náš výpis a zapisuje se tam spousta dat. Prohlížeč používá šifrování nejen pro provoz a veškeré šifrování prochází danou funkcí. Ale z nějakého důvodu tam nejsou potřebná data a požadovaný požadavek není v provozu viditelný. Abychom nemuseli čekat, až se UC Browser rozhodne provést nezbytný požadavek, vezmeme zašifrovanou odpověď ze serveru přijatou dříve a znovu aplikaci opravíme: přidejte dešifrování do onCreate hlavní aktivity.

    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

Sestavíme, podepíšeme, nainstalujeme, spustíme. Obdržíme výjimku NullPointerException, protože metoda vrátila hodnotu null.

Při další analýze kódu byla objevena funkce, která dešifruje zajímavé řádky: „META-INF/“ a „.RSA“. Vypadá to, že aplikace ověřuje svůj certifikát. Nebo z něj dokonce generuje klíče. Nechci se opravdu zabývat tím, co se s certifikátem děje, takže mu prostě vložíme správný certifikát. Opravme zašifrovaný řádek tak, že místo „META-INF/“ dostaneme „BLABLINF/“, vytvoříme složku s tímto názvem v APK a přidáme tam certifikát prohlížeče veverka.

Sestavíme, podepíšeme, nainstalujeme, spustíme. Bingo! Máme klíč!

WithM

Obdrželi jsme klíč a inicializační vektor rovný klíči. Zkusme dešifrovat odpověď serveru v režimu CBC.

Hledání zranitelností v prohlížeči UC

Vidíme URL archivu, něco podobného jako MD5, „extract_unzipsize“ a číslo. Zkontrolujeme: MD5 archivu je stejný, velikost rozbalené knihovny je stejná. Snažíme se tuto knihovnu opravit a dát ji do prohlížeče. Abychom ukázali, že se naše opravená knihovna načetla, spustíme záměr vytvořit SMS s textem „PWNED!“ Nahradíme dvě odpovědi ze serveru: puds.ucweb.com/upgrade/index.xhtml a stáhnout archiv. V prvním nahradíme MD5 (velikost se po rozbalení nemění), ve druhém dáme archiv s opravenou knihovnou.

Prohlížeč se několikrát pokusí stáhnout archiv, poté zobrazí chybu. Zřejmě něco
nemá rád. V důsledku analýzy tohoto temného formátu se ukázalo, že server také přenáší velikost archivu:

Hledání zranitelností v prohlížeči UC

Je zakódován v LEB128. Po opravě se velikost archivu s knihovnou trochu změnila, takže prohlížeč usoudil, že archiv byl stažen křivě a po několika pokusech vyhodil chybu.

Upravujeme velikost archivu... A – vítězství! 🙂 Výsledek je ve videu.

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

Důsledky a reakce vývojáře

Stejným způsobem by hackeři mohli využít nezabezpečenou funkci prohlížeče UC k distribuci a spouštění škodlivých knihoven. Tyto knihovny budou fungovat v kontextu prohlížeče, takže získají všechna jeho systémová oprávnění. Výsledkem je možnost zobrazení phishingových oken a také přístup k pracovním souborům oranžové čínské veverky, včetně přihlašovacích údajů, hesel a cookies uložených v databázi.

Oslovili jsme vývojáře UC Browseru a informovali je o nalezeném problému, pokusili jsme se poukázat na zranitelnost a její nebezpečnost, ale o ničem se s námi nebavili. Mezitím prohlížeč dál předváděl svou nebezpečnou funkci na očích. Ale jakmile jsme odhalili podrobnosti o zranitelnosti, již nebylo možné ji ignorovat jako dříve. Byl 27. březen
byla vydána nová verze prohlížeče UC 12.10.9.1193, který přistupoval k serveru přes HTTPS: puds.ucweb.com/upgrade/index.xhtml.

Navíc po „opravě“ a do doby psaní tohoto článku se při pokusu o otevření PDF v prohlížeči objevila chybová zpráva s textem „Jejda, něco se pokazilo!“ Při pokusu o otevření PDF nebyl proveden požadavek na server, ale požadavek byl vzat při spuštění prohlížeče, což naznačuje pokračující možnost stahovat spustitelný kód v rozporu s pravidly Google Play.

Zdroj: www.habr.com

Přidat komentář