Enkonduko
Fine de marto ni
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
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,
Ĉ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
Do, kiam uzanto volas malfermi PDF rekte en la retumilo, la jenaj petoj videblas en la trafiko:
Unue estas POST-peto al
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
La biblioteko mem estas pakita en ZIP kaj ne estas ĉifrita.
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.
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.
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:
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:
La tablo klare komenciĝas ĉe 0xB144. Kio estas en sub_494C?
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:
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:
IDA denove ne rekonis ĉi tiun areon kiel kodo. Ni helpas ŝin kaj vidas alian dezajnon tie:
La instrukcioj post BLX ne ŝajnas havi multe da senco, ĝi pli similas al ia movo. Ni rigardu sub_4964:
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:
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:
Krom la jam menciitaj strukturoj, la kodo enhavas ankaŭ la jenon:
Kiel en la antaŭa kazo, post la BLX-instrukcio estas ofseto:
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:
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:
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:
Ni movas la unuan indekson al R11:
Por ricevi adreson de tabelo, uzu indekson:
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.
La rezulta kodo estas timiga. Sed, irante tra ĝia ĝangalo, vi povas rimarki alvokon al funkcio jam konata al ni sub_6115C:
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.
Ĉ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:
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.
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.
Poste ni havas tri strukturojn, kiuj enhavas la datumgrandecon kaj montrilon al la datenoj por la ŝlosilo, komenca vektoro kaj ĉifritaj datumoj.
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.
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:
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:
Ĝ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.
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:
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