Serĉante vundeblecojn en UC-Retumilo

Serĉante vundeblecojn en UC-Retumilo

Enkonduko

Fine de marto ni raportis, ke ili malkovris kaŝitan kapablon ŝargi kaj ruli nekontrolitan kodon en UC-Retumilo. Hodiaŭ ni rigardos detale kiel ĉi tiu elŝuto okazas kaj kiel hackers povas uzi ĝin por siaj propraj celoj.

Antaŭ iom da tempo, UC-Retumilo estis reklamita kaj distribuita tre agreseme: ĝi estis instalita sur aparatoj de uzantoj uzante malware, distribuita de diversaj retejoj sub la alivestiĝo de videodosieroj (t.e., uzantoj pensis, ke ili elŝutas, ekzemple, pornvideon, sed anstataŭe ricevis APK per ĉi tiu retumilo), uzis timigajn standardojn kun mesaĝoj, ke la retumilo estis malaktuala, vundebla, kaj tiaj aferoj. En la oficiala grupo de UC Browser sur VK ekzistas la temo, en kiu uzantoj povas plendi pri maljusta reklamado, ekzistas multaj ekzemploj tie. En 2016 estis eĉ video reklamado en la rusa (jes, reklamado por reklam-bloka retumilo).

En la momento de la skribado, UC-Retumilo havas pli ol 500-instalaĵojn en Google Play. Ĉi tio estas impona - nur Google Chrome havas pli. Inter la recenzoj vi povas vidi sufiĉe multajn plendojn pri reklamado kaj alidirektiloj al iuj aplikaĵoj en Google Play. Jen la kialo de nia esplorado: ni decidis vidi ĉu UC Browser faras ion malbonan. Kaj montriĝis, ke li faras!

En la aplika kodo, la kapablo elŝuti kaj ruli plenumeblan kodon estis malkovrita, kio estas kontraŭa al la reguloj por eldonado de aplikaĵoj en Google Play. Krom elŝuti plenumeblan kodon, UC Browser faras tion en nesekura maniero, kiu povas esti uzata por lanĉi MitM-atakon. Ni vidu, ĉu ni povas fari tian atakon.

Ĉio ĉi-sube skribita estas grava por la versio de UC-Retumilo, kiu estis disponebla ĉe Google Play dum la studo:

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

Vektoro de atako

En la manifesto de UC Browser vi povas trovi servon kun memklarigebla nomo com.uc.deployment.UpgradeDeployService.

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

Kiam ĉi tiu servo komenciĝas, la retumilo faras POST-peton al puds.ucweb.com/upgrade/index.xhtml, kiu povas esti vidita en trafiko iom da tempo post la komenco. Responde, li eble ricevos komandon por elŝuti iun ĝisdatigon aŭ novan modulon. Dum la analizo, la servilo ne donis tiajn komandojn, sed ni rimarkis, ke kiam ni provas malfermi PDF-on en la retumilo, ĝi faras duan peton al la supre specifita adreso, post kio ĝi elŝutas la denaskan bibliotekon. Por efektivigi la atakon, ni decidis uzi ĉi tiun funkcion de UC-Retumilo: la kapablo malfermi PDF per denaska biblioteko, kiu ne estas en la APK kaj kiun ĝi elŝutas de la Interreto se necese. Indas rimarki, ke teorie, UC-Retumilo povas esti devigita elŝuti ion sen uzantinterago - se vi donas bone formitan respondon al peto, kiu estas plenumita post lanĉo de la retumilo. Sed por fari tion, ni devas pli detale studi la protokolon de interago kun la servilo, do ni decidis, ke estos pli facile redakti la kaptitan respondon kaj anstataŭigi la bibliotekon por labori kun PDF.

Do, kiam uzanto volas malfermi PDF rekte en la retumilo, la jenaj petoj videblas en la trafiko:

Serĉante vundeblecojn en UC-Retumilo

Unue estas POST-peto al puds.ucweb.com/upgrade/index.xhtml, tiam
Arkivo kun biblioteko por vidi PDF kaj oficejaj formatoj estas elŝutita. Estas logike supozi, ke la unua peto transdonas informojn pri la sistemo (almenaŭ la arkitekturo por havigi la bezonatan bibliotekon), kaj responde al ĝi la retumilo ricevas kelkajn informojn pri la biblioteko, kiun oni devas elŝuti: la adreson kaj, eventuale. , io alia. La problemo estas, ke ĉi tiu peto estas ĉifrita.

Petu fragmenton

Respondfragmento

Serĉante vundeblecojn en UC-Retumilo

Serĉante vundeblecojn en UC-Retumilo

La biblioteko mem estas pakita en ZIP kaj ne estas ĉifrita.

Serĉante vundeblecojn en UC-Retumilo

Serĉu trafikan deĉifradan kodon

Ni provu deĉifri la servilan respondon. Ni rigardu la klaskodon com.uc.deployment.UpgradeDeployService: de metodo onStartCommand iru al com.uc.deployment.bx, kaj de ĝi ĝis 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);
}

Ni vidas la formadon de POST-peto ĉi tie. Ni atentas la kreadon de tabelo de 16 bajtoj kaj ĝia plenigo: 0x5F, 0, 0x1F, -50 (=0xCE). Koincidas kun tio, kion ni vidis en la ĉi-supra peto.

En la sama klaso vi povas vidi nestitan klason kiu havas alian interesan metodon:

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

La metodo prenas tabelon de bajtoj kiel enigo kaj kontrolas ke la nul bajto estas 0x60 aŭ la tria bajto estas 0xD0, kaj la dua bajto estas 1, 11 aŭ 0x1F. Ni rigardas la respondon de la servilo: la nula bajto estas 0x60, la dua estas 0x1F, la tria estas 0x60. Sonas kiel tio, kion ni bezonas. Juĝante laŭ la linioj ("up_decrypt", ekzemple), metodo devus esti nomita ĉi tie, kiu deĉifris la respondon de la servilo.
Ni transiru al la metodo gj. Notu, ke la unua argumento estas la bajto ĉe ofseto 2 (t.e. 0x1F en nia kazo), kaj la dua estas la servila respondo sen
unuaj 16 bajtoj.

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

Evidente, ĉi tie ni elektas malĉifridan algoritmon, kaj la saman bajton kiu estas en nia
kazo egala al 0x1F, indikas unu el tri eblaj opcioj.

Ni daŭre analizas la kodon. Post kelkaj saltoj ni trovas nin en metodo kun memklarigebla nomo decryptBytesByKey.

Ĉi tie du pliaj bajtoj estas apartigitaj de nia respondo, kaj ĉeno estas akirita de ili. Estas klare, ke tiamaniere la ŝlosilo por malĉifri la mesaĝon estas elektita.

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

Rigardante antaŭen, ni rimarkas, ke en ĉi tiu etapo ni ankoraŭ ne akiras ŝlosilon, sed nur ĝian "identigilon". Akiri la ŝlosilon estas iom pli komplika.

En la sekva metodo, du pliaj parametroj estas aldonitaj al la ekzistantaj, farante kvar el ili: la magia numero 16, la ŝlosila identigilo, la ĉifritaj datumoj kaj nekomprenebla ĉeno (en nia kazo, malplena).

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

Post serio da transiroj ni alvenas al la metodo staticBinarySafeDecryptNoB64 de la interfaco com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. Ne estas klasoj en la ĉefa aplika kodo, kiuj efektivigas ĉi tiun interfacon. Estas tia klaso en la dosiero lib/armeabi-v7a/libsgmain.so, kiu fakte ne estas .so, sed .jaro. La metodo pri kiu ni interesiĝas estas efektivigita jene:

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

Ĉi tie nia listo de parametroj estas kompletigita per du pliaj entjeroj: 2 kaj 0. Juĝante per
ĉio, 2 signifas deĉifradon, kiel en la metodo doFinal sistema klaso javax.crypto.Cipher. Kaj ĉio ĉi estas transdonita al certa Router kun la numero 10601 - ĉi tio ŝajne estas la komanda numero.

Post la sekva ĉeno de transiroj ni trovas klason kiu efektivigas la interfacon IRouterComponent kaj metodo 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);
}
}

Kaj ankaŭ klaso JNICBiblioteko, en kiu la indiĝena metodo estas deklarita doCommandNative:

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

Ĉi tio signifas, ke ni devas trovi metodon en la denaska kodo doCommandNative. Kaj ĉi tie komenciĝas la amuzo.

Malklariĝo de maŝinkodo

En dosiero libsgmain.so (kiu fakte estas .jar kaj en kiu ni trovis la efektivigon de iuj ĉifradaj interfacoj ĝuste supre) estas unu indiĝena biblioteko: libsgmainso-6.4.36.so. Ni malfermas ĝin en IDA kaj ricevas amason da dialogkestoj kun eraroj. La problemo estas, ke la sekcia kapliniotabelo estas nevalida. Ĉi tio estas farita intence por malfaciligi la analizon.

Serĉante vundeblecojn en UC-Retumilo

Sed ĝi ne estas bezonata: por ĝuste ŝargi ELF-dosieron kaj analizi ĝin, sufiĉas programa kaptabelo. Tial ni simple forigas la sekcian tabelon, nuligante la respondajn kampojn en la kaplinio.

Serĉante vundeblecojn en UC-Retumilo

Malfermu la dosieron en IDA denove.

Estas du manieroj diri al la Java virtuala maŝino kie ĝuste en la indiĝena biblioteko troviĝas la efektivigo de metodo deklarita en Java-kodo kiel indiĝena. La unua estas doni al ĝi specionomon Java_package_name_ClassName_MethodName.

La dua estas registri ĝin dum ŝarĝo de la biblioteko (en la funkcio JNI_OnLoad)
uzante funkciovokon Registru Indiĝenojn.

En nia kazo, se ni uzas la unuan metodon, la nomo devus esti tia: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

Ne ekzistas tia funkcio inter la eksportitaj funkcioj, kio signifas, ke vi devas serĉi vokon Registru Indiĝenojn.
Ni iru al la funkcio JNI_OnLoad kaj ni vidas ĉi tiun bildon:

Serĉante vundeblecojn en UC-Retumilo

Kio okazas ĉi tie? Unuavide, la komenco kaj fino de la funkcio estas tipaj por ARM-arkitekturo. La unua instrukcio sur la stako stokas la enhavon de la registroj, kiujn la funkcio uzos en sia funkciado (en ĉi tiu kazo, R0, R1 kaj R2), same kiel la enhavon de la LR-registro, kiu enhavas la revenadreson de la funkcio. . La lasta instrukcio restarigas la konservitajn registrojn, kaj la revenadreso tuj estas metita en la komputilan registron - tiel revenante de la funkcio. Sed se vi rigardas atente, vi rimarkos, ke la antaŭlasta instrukcio ŝanĝas la revenadreson konservitan sur la stako. Ni kalkulu kiel ĝi estos poste
koda ekzekuto. Certa adreso 1xB0 estas ŝarĝita en R130, 5 estas subtrahita de ĝi, tiam ĝi estas transdonita al R0 kaj 0x10 estas aldonita al ĝi. Rezultas 0xB13B. Tiel, IDA opinias ke la lasta instrukcio estas normala funkcio reveno, sed fakte ĝi iras al la kalkulita adreso 0xB13B.

Indas rememori ĉi tie, ke ARM-procesoroj havas du reĝimojn kaj du arojn da instrukcioj: ARM kaj Thumb. La malplej signifa peco de la adreso rakontas al la procesoro kiu instrukcio estas uzata. Tio estas, la adreso estas fakte 0xB13A, kaj unu en la malplej signifa bito indikas la Thumb-reĝimon.

Simila "adaptilo" estis aldonita al la komenco de ĉiu funkcio en ĉi tiu biblioteko kaj
rubkodo. Pri ili ni ne plu detale detale - ni nur memoras
ke la vera komenco de preskaŭ ĉiuj funkcioj estas iom pli for.

Ĉar la kodo ne eksplicite saltas al 0xB13A, IDA mem ne rekonis, ke la kodo troviĝas ĉe ĉi tiu loko. Pro la sama kialo, ĝi ne rekonas la plej grandan parton de la kodo en la biblioteko kiel kodon, kio faras analizon iom malfacila. Ni diras al IDA, ke ĉi tio estas la kodo, kaj jen kio okazas:

Serĉante vundeblecojn en UC-Retumilo

La tablo klare komenciĝas ĉe 0xB144. Kio estas en sub_494C?

Serĉante vundeblecojn en UC-Retumilo

Kiam oni vokas ĉi tiun funkcion en la LR-registro, ni ricevas la adreson de la antaŭe menciita tabelo (0xB144). En R0 - indekso en ĉi tiu tabelo. Tio estas, la valoro estas prenita de la tabelo, aldonita al LR kaj la rezulto estas
la adreso al kiu iri. Ni provu kalkuli ĝin: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Ni iras al la ricevita adreso kaj vidas laŭvorte kelkajn utilajn instrukciojn kaj denove iras al 0xB140:

Serĉante vundeblecojn en UC-Retumilo

Nun estos transiro ĉe ofseto kun indekso 0x20 de la tabelo.

Juĝante laŭ la grandeco de la tablo, estos multaj tiaj transiroj en la kodo. Estiĝas la demando, ĉu eblas iel trakti tion pli aŭtomate, sen permane kalkuli adresojn. Kaj skriptoj kaj la kapablo fliki kodon en IDA venas al nia helpo:

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"

Metu la kursoron sur la linio 0xB26A, rulu la skripton kaj vidu la transiron al 0xB4B0:

Serĉante vundeblecojn en UC-Retumilo

IDA denove ne rekonis ĉi tiun areon kiel kodo. Ni helpas ŝin kaj vidas alian dezajnon tie:

Serĉante vundeblecojn en UC-Retumilo

La instrukcioj post BLX ne ŝajnas havi multe da senco, ĝi pli similas al ia movo. Ni rigardu sub_4964:

Serĉante vundeblecojn en UC-Retumilo

Kaj efektive, ĉi tie oni prenas dvorton ĉe la adreso kuŝanta en LR, aldonita al ĉi tiu adreso, post kio la valoro ĉe la rezulta adreso estas prenita kaj metita sur la stakon. Ankaŭ, 4 estas aldonita al LR tiel ke post reveno de la funkcio, ĉi tiu sama ofseto estas preterpasita. Post kio la komando POP {R1} prenas la rezultan valoron el la stako. Se vi rigardas tion, kio troviĝas ĉe adreso 0xB4BA + 0xEA = 0xB5A4, vi vidos ion similan al adrestabelo:

Serĉante vundeblecojn en UC-Retumilo

Por fliki ĉi tiun dezajnon, vi devos akiri du parametrojn de la kodo: la ofseto kaj la registro nombro en kiu vi volas meti la rezulton. Por ĉiu ebla registro, vi devos prepari kodon antaŭe.

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"

Ni metas la kursoron ĉe la komenco de la strukturo, kiun ni volas anstataŭigi - 0xB4B2 - kaj rulu la skripton:

Serĉante vundeblecojn en UC-Retumilo

Krom la jam menciitaj strukturoj, la kodo enhavas ankaŭ la jenon:

Serĉante vundeblecojn en UC-Retumilo

Kiel en la antaŭa kazo, post la BLX-instrukcio estas ofseto:

Serĉante vundeblecojn en UC-Retumilo

Ni prenas la ofseton al la adreso de LR, aldonu ĝin al LR kaj iras tien. 0x72044 + 0xC = 0x72050. La skripto por ĉi tiu dezajno estas sufiĉe simpla:

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"

Rezulto de skripto-ekzekuto:

Serĉante vundeblecojn en UC-Retumilo

Post kiam ĉio estas flikita en la funkcio, vi povas indiki IDA al ĝia reala komenco. Ĝi kunmetos la tutan funkciokodon, kaj ĝi povas esti malkompilita per HexRays.

Malkodado de ŝnuroj

Ni lernis trakti malklarigon de maŝinkodo en la biblioteko libsgmainso-6.4.36.so de UC Browser kaj ricevis la funkciokodon 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;
}

Ni rigardu pli detale la sekvajn liniojn:

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

En funkcio sub_73E24 la klasnomo estas klare deĉifrita. Kiel parametroj al ĉi tiu funkcio, montrilo al datumoj similaj al ĉifritaj datumoj, certa bufro kaj nombro estas pasitaj. Evidente, post vokado de la funkcio, estos deĉifrita linio en la bufro, ĉar ĝi estas pasita al la funkcio. Trovu Klason, kiu prenas la klasnomon kiel la duan parametron. Tial, la nombro estas la grandeco de la bufro aŭ la longo de la linio. Ni provu deĉifri la klasnomon, ĝi devus diri al ni ĉu ni iras en la ĝustan direkton. Ni rigardu pli detale kio okazas en 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;
}

funkcio sub_7AF78 kreas ekzemplon de ujo por bajtaj tabeloj de la specifita grandeco (ni ne detale pritraktos ĉi tiujn ujojn). Ĉi tie kreiĝas du tiaj ujoj: unu enhavas la linion "DcO/lcK+h?m3c*q@" (estas facile diveni, ke ĉi tio estas ŝlosilo), la alia enhavas ĉifritajn datumojn. Poste, ambaŭ objektoj estas metitaj en certan strukturon, kiu estas transdonita al la funkcio sub_6115C. Ni marku ankaŭ kampon kun la valoro 3 en ĉi tiu strukturo. Ni vidu kio okazas al ĉi tiu strukturo poste.

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

La ŝaltilo-parametro estas strukturkampo al kiu antaŭe estis asignita la valoro 3. Rigardu kazon 3: al la funkcio sub_6364C parametroj estas transdonitaj de la strukturo kiu estis aldonita tie en la antaŭa funkcio, t.e. la ŝlosilo kaj ĉifrita datumo. Se vi atente rigardas sub_6364C, vi povas rekoni la RC4-algoritmon en ĝi.

Ni havas algoritmon kaj ŝlosilon. Ni provu deĉifri la klasnomon. Jen kio okazis: com/taobao/wireless/security/adapter/JNICLibrary. Bonege! Ni estas sur la ĝusta vojo.

Arbo de komando

Nun ni devas trovi defion Registru Indiĝenojn, kiu montros al ni la funkcion doCommandNative. Ni rigardu la funkciojn nomitajn de JNI_OnLoad, kaj ni trovas ĝin en 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;
}

Kaj efektive, denaska metodo kun la nomo estas registrita ĉi tie doCommandNative. Nun ni scias lian adreson. Ni vidu kion li faras.

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

Laŭ la nomo vi povas diveni, ke ĉi tie estas la eniro de ĉiuj funkcioj, kiujn la programistoj decidis transdoni al la denaska biblioteko. Ni interesiĝas pri funkcio numero 10601.

Vi povas vidi el la kodo, ke la komandnumero produktas tri nombrojn: komando/10000, komando % 10000 / 100 и komando % 10, t.e., en nia kazo, 1, 6 kaj 1. Ĉi tiuj tri nombroj, same kiel montrilo al JNIEnv kaj la argumentoj pasitaj al la funkcio estas aldonitaj al strukturo kaj pludonitaj. Uzante la tri nombrojn akiritajn (ni indiku ilin N1, N2 kaj N3), komando-arbo estas konstruita.

Io kiel ĉi tio:

Serĉante vundeblecojn en UC-Retumilo

La arbo estas dinamike plenigita JNI_OnLoad.
Tri nombroj kodas la vojon en la arbo. Ĉiu folio de la arbo enhavas la pokita adreso de la responda funkcio. La ŝlosilo estas en la gepatra nodo. Trovi la lokon en la kodo, kie la funkcio, kiun ni bezonas, estas aldonita al la arbo, ne estas malfacila, se vi komprenas ĉiujn uzatajn strukturojn (ni ne priskribas ilin por ne ŝveligi jam sufiĉe grandan artikolon).

Pli malklara

Ni ricevis la adreson de la funkcio, kiu devus malĉifri trafikon: 0x5F1AC. Sed estas tro frue por ĝoji: la programistoj de UC Browser preparis alian surprizon por ni.

Post ricevi la parametrojn de la tabelo, kiu estis formita en la Java-kodo, ni ricevas
al la funkcio ĉe adreso 0x4D070. Kaj ĉi tie atendas nin alia speco de malklarigado de kodo.

Ni metas du indeksojn en R7 kaj R4:

Serĉante vundeblecojn en UC-Retumilo

Ni movas la unuan indekson al R11:

Serĉante vundeblecojn en UC-Retumilo

Por ricevi adreson de tabelo, uzu indekson:

Serĉante vundeblecojn en UC-Retumilo

Post iri al la unua adreso, la dua indekso estas uzata, kiu estas en R4. Estas 230 elementoj en la tabelo.

Kion fari pri ĝi? Vi povas diri al IDA, ke ĉi tio estas ŝaltilo: Redakti -> Aliaj -> Specifu ŝanĝidiomon.

Serĉante vundeblecojn en UC-Retumilo

La rezulta kodo estas timiga. Sed, irante tra ĝia ĝangalo, vi povas rimarki alvokon al funkcio jam konata al ni sub_6115C:

Serĉante vundeblecojn en UC-Retumilo

Estis ŝaltilo en kiu en la kazo 3 estis deĉifrado uzante la RC4-algoritmon. Kaj en ĉi tiu kazo, la strukturo pasita al la funkcio estas plenigita de la parametroj pasitaj al doCommandNative. Ni memoru, kion ni havis tie magicInt kun la valoro 16. Ni rigardas la respondan kazon - kaj post pluraj transiroj ni trovas la kodon per kiu la algoritmo povas esti identigita.

Serĉante vundeblecojn en UC-Retumilo

Ĉi tio estas AES!

La algoritmo ekzistas, restas nur akiri ĝiajn parametrojn: reĝimo, ŝlosilo kaj, eventuale, la inicialiga vektoro (ĝia ĉeesto dependas de la operacia reĝimo de la AES-algoritmo). La strukturo kun ili devas esti formita ie antaŭ la funkciovoko sub_6115C, sed ĉi tiu parto de la kodo estas speciale bone malklarigita, do ekestas la ideo fliki la kodon por ke ĉiuj parametroj de la malĉifra funkcio estu forĵetitaj en dosieron.

Diakilo

Por ne skribi la tutan flikkodon en asembla lingvo permane, vi povas lanĉi Android Studion, skribi tie funkcion, kiu ricevas la samajn enigajn parametrojn kiel nia malĉifra funkcio kaj skribas al dosiero, tiam kopii-alglui la kodon, kiun la kompililo faros. generi.

Niaj amikoj de la teamo de UC Browser ankaŭ zorgis pri la oportuno aldoni kodon. Ni memoru, ke komence de ĉiu funkcio ni havas ruban kodon, kiu povas facile esti anstataŭigita per iu ajn alia. Tre oportune 🙂 Tamen, komence de la cela funkcio ne estas sufiĉe da spaco por la kodo, kiu konservas ĉiujn parametrojn al dosiero. Mi devis dividi ĝin en partojn kaj uzi rubblokojn de najbaraj funkcioj. Estis kvar partoj entute.

Unua parto:

Serĉante vundeblecojn en UC-Retumilo

En la ARM-arkitekturo, la unuaj kvar funkcioparametroj estas pasitaj tra registroj R0-R3, la resto, se entute, estas pasitaj tra la stako. La LR-registro portas la revenadreson. Ĉio ĉi devas esti konservita por ke la funkcio povu funkcii post kiam ni forĵetas ĝiajn parametrojn. Ni ankaŭ devas konservi ĉiujn registrojn, kiujn ni uzos en la procezo, do ni faras PUSH.W {R0-R10,LR}. En R7 ni ricevas la adreson de la listo de parametroj pasitaj al la funkcio per la stako.

Uzante la funkcion fopen ni malfermu la dosieron /data/local/tmp/aes en "ab" reĝimo
t.e. por aldono. En R0 ni ŝargas la adreson de la dosiernomo, en R1 - la adreson de la linio indikanta la reĝimon. Kaj ĉi tie la rubkodo finiĝas, do ni transiru al la sekva funkcio. Por ke ĝi daŭre funkciu, ni metas komence la transiron al la reala kodo de la funkcio, preterirante la rubaĵon, kaj anstataŭ la rubo ni aldonas daŭrigon de la flikaĵo.

Serĉante vundeblecojn en UC-Retumilo

Vokado fopen.

La unuaj tri parametroj de la funkcio aes havas tipon int. Ĉar ni konservis la registrojn al la stako komence, ni povas simple pasi la funkcion fskribi iliaj adresoj sur la stako.

Serĉante vundeblecojn en UC-Retumilo

Poste ni havas tri strukturojn, kiuj enhavas la datumgrandecon kaj montrilon al la datenoj por la ŝlosilo, komenca vektoro kaj ĉifritaj datumoj.

Serĉante vundeblecojn en UC-Retumilo

Fine, fermu la dosieron, restarigu la registrojn kaj transigu kontrolon al la reala funkcio aes.

Ni kolektas APK kun flikita biblioteko, subskribas ĝin, alŝutas ĝin al la aparato/emulilo kaj lanĉas ĝin. Ni vidas, ke nia rubejo estas kreita, kaj multaj datumoj estas skribitaj tie. La retumilo uzas ĉifradon ne nur por trafiko, kaj ĉiu ĉifrado trairas la koncernan funkcion. Sed ial la necesaj datumoj ne estas tie, kaj la postulata peto ne estas videbla en la trafiko. Por ne atendi ĝis UC-Retumilo degnas fari la necesan peton, ni prenu la ĉifritan respondon de la servilo ricevita pli frue kaj fliku la aplikaĵon denove: aldonu la deĉifradon al onCreate de la ĉefa agado.

    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

Ni arigas, subskribas, instalas, lanĉas. Ni ricevas NullPointerException ĉar la metodo revenis nula.

Dum plua analizo de la kodo oni malkovris funkcion, kiu deĉifras interesajn liniojn: “META-INF/” kaj “.RSA”. Ŝajnas, ke la aplikaĵo kontrolas sian atestilon. Aŭ eĉ generas ŝlosilojn de ĝi. Mi ne vere volas trakti tion, kio okazas kun la atestilo, do ni simple glitos al ĝi la ĝustan atestilon. Ni fliku la ĉifritan linion por ke anstataŭ "META-INF/" ni ricevu "BLABLINF/", kreu dosierujon kun tiu nomo en la APK kaj aldonu tie la atestilon pri sciuro-retumilo.

Ni arigas, subskribas, instalas, lanĉas. Bingo! Ni havas la ŝlosilon!

MitM

Ni ricevis ŝlosilon kaj komencan vektoron egala al la ŝlosilo. Ni provu malĉifri la servilan respondon en CBC-reĝimo.

Serĉante vundeblecojn en UC-Retumilo

Ni vidas la arkivan URL, ion similan al MD5, "extract_unzipsize" kaj nombron. Ni kontrolas: la MD5 de la arkivo estas la sama, la grandeco de la malpakita biblioteko estas la sama. Ni provas fliki ĉi tiun bibliotekon kaj doni ĝin al la retumilo. Por montri, ke nia flikita biblioteko ŝarĝis, ni lanĉos Intencon krei SMS kun la teksto "PWNED!" Ni anstataŭigos du respondojn de la servilo: puds.ucweb.com/upgrade/index.xhtml kaj elŝuti la arkivon. En la unua ni anstataŭigas MD5 (la grandeco ne ŝanĝas post malpakado), en la dua ni donas la arkivon kun la flikita biblioteko.

La retumilo provas elŝuti la arkivon plurfoje, post kio ĝi donas eraron. Ŝajne io
li ne ŝatas. Kiel rezulto de analizo de ĉi tiu malklara formato, montriĝis, ke la servilo ankaŭ transdonas la grandecon de la arkivo:

Serĉante vundeblecojn en UC-Retumilo

Ĝi estas ĉifrita en LEB128. Post la flikaĵo, la grandeco de la arkivo kun la biblioteko iom ŝanĝiĝis, do la retumilo konsideris, ke la arkivo estis malrekte elŝutita, kaj post pluraj provoj ĝi ĵetis eraron.

Ni ĝustigas la grandecon de la arkivo... Kaj – venko! 🙂 La rezulto estas en la video.

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

Konsekvencoj kaj reago de la programisto

En la sama maniero, retpiratoj povus uzi la nesekuran funkcion de UC-Retumilo por distribui kaj funkciigi malicajn bibliotekojn. Ĉi tiuj bibliotekoj funkcios en la kunteksto de la retumilo, do ili ricevos ĉiujn ĝiajn sistemajn permesojn. Kiel rezulto, la kapablo montri phishing-fenestrojn, same kiel aliron al la labordosieroj de la oranĝa ĉina sciuro, inkluzive de ensalutoj, pasvortoj kaj kuketoj stokitaj en la datumbazo.

Ni kontaktis la programistojn de UC Browser kaj informis ilin pri la problemo, kiun ni trovis, provis montri la vundeblecon kaj ĝian danĝeron, sed ili nenion diskutis kun ni. Dume, la retumilo daŭre elmontris sian danĝeran funkcion en plena vido. Sed post kiam ni malkaŝis la detalojn de la vundebleco, ne plu eblis ignori ĝin kiel antaŭe. La 27-a de marto estis
nova versio de UC Browser 12.10.9.1193 estis publikigita, kiu aliris la servilon per HTTPS: puds.ucweb.com/upgrade/index.xhtml.

Krome, post la "korekto" kaj ĝis la tempo de verkado de ĉi tiu artikolo, provi malfermi PDF en retumilo rezultigis erarmesaĝon kun la teksto "Ho, io misfunkciis!" Peto al la servilo ne estis farita kiam oni provis malfermi PDF, sed peto estis farita kiam la retumilo estis lanĉita, kio aludas la daŭran kapablon elŝuti plenumeblan kodon malobee al la reguloj de Google Play.

fonto: www.habr.com

Aldoni komenton