
Ievads
Marta beigās mēs , ka atklājām slēptu iespēju lejupielādēt un palaist neiztestētu kodu UC Browser. Šodien detalizēti izskatīsim, kā norisinās šī lejupielāde un kā hakeri var to izmantot savās interesēs.
Kādu laiku atpakaļ UC Browser agresīvi reklamēja un izplatīja: to uzstādīja lietotāju ierīcēs, izmantojot ļaunprātīgas programmas, izplatīja no dažādām vietnēm kā video failus (t.i., lietotāji domāja, ka lejupielādē, piemēram, pornovideo, bet saņēma vietā APK ar šo pārlūku), izmantoja biedējošus banerus ar ziņojumiem par to, ka pārlūka versija ir novecojusi, neaizsargāta un tamlīdzīgi. UC Browser oficiālajā grupā VK ir , kur lietotāji var sūdzēties par negodīgu reklāmu, tur ir daudz piemēru. 2016. gadā pat bija krievu valodā (jā, pārlūka reklāma, kas bloķē reklāmas).
Raksta rakstīšanas brīdī UC Browser bija pārsniedzis 500 000 000 instalāciju Google Play. Tas ir iespaidīgi — tikai Google Chrome ir vairāk. Atsauksmēs var redzēt diezgan daudz sūdzību par reklāmām un novirzīšanu uz dažādām lietotnēm Google Play. Tas kļuva par iemeslu izpētei: mēs nolēmām apskatīt, vai UC Browser nedara ko slikti. Un izrādījās, ka tomēr dara!
Lietotnes kodā tika atklāta iespēja lejupielādēt un palaist izpildāmo kodu, Google Play. Tāpat kā UC Browser lejupielādē izpildāmo kodu, to dara nedroši, kas var tikt izmantots MitM uzbrukuma veikšanai. Apskatīsim, vai mums izdosies veikt tādu uzbrukumu.
Viss, kas šeit ir rakstīts, attiecas uz UC Browser versiju, kas bija pieejama Google Play izpētes veikšanas brīdī:
package: com.UCMobile.intl
versionName: 12.10.8.1172
versionCode: 10598
sha1 APK faila: f5edb2243413c777172f6362876041eb0c3a928cUzbrukuma vektors
UC Browser manifestā var atrast pakalpojumu ar runājošu nosaukumu com.uc.deployment.UpgradeDeployService.
<service android_exported="false" android_name="com.uc.deployment.UpgradeDeployService" android_process=":deploy" />
Palaižot šo pakalpojumu, pārlūks veic POST pieprasījumu uz , kuru var pamanīt satiksmē pēc kāda laika pēc uzsākšanas. Atbildē viņš var saņemt komandu lejupielādēt kādu atjauninājumu vai jaunu moduli. Analīzes procesā serveris šādas komandas nesniedza, tomēr mēs pamanījām, ka, mēģinot atvērt PDF pārlūkā, tiek veikts atkārtots pieprasījums iepriekš norādītajā adresē, pēc kura tiek lejupielādēta nativā bibliotēka. Uzbrukuma īstenošanai mēs nolēmām izmantot šo īpatnību UC Browser: spēja atvērt PDF, izmantojot nativu bibliotēku, kas nav APK un kuru tas nepieciešamības gadījumā lejupielādē no Interneta. Jāatzīmē, ka teorētiski UC Browser var piespiest kaut ko lejupielādēt arī bez lietotāja mijiedarbības – ja pareizi noformē atbildi uz pieprasījumu, kas tiek izpildīts pēc pārlūka palaišanas. Bet tam nepieciešams detalizētāk izpētīt protokolu par mijiedarbību ar serveri, tāpēc mēs nolēmām, ka ir vienkāršāk rediģēt nozagto atbildi un aizstāt bibliotēku darbam ar PDF.
Tātad, kad lietotājs vēlas atvērt PDF tieši pārlūkā, satiksmē var redzēt šādus pieprasījumus:

Vispirms notiek POST pieprasījums uz , pēc tam
lejupielādē arhīvu ar bibliotēku PDF un biroja formātu skatīšanai. Ir loģiski pieņemt, ka pirmajā pieprasījumā tiek nosūtīta informācija par sistēmu (vismaz arhitektūra, lai nodotu pareizo bibliotēku), bet atbildē pārlūks saņem kādu informāciju par bibliotēku, kuru nepieciešams lejupielādēt: adresi un, iespējams, vēl kaut ko. Problēma ir tāda, ka šis pieprasījums ir šifrēts.
Pieprasījuma fragments
Atbildes fragments


Paša bibliotēka ir sapakota ZIP un nav šifrēta.

Koda meklēšana satiksmes atšifrēšanai
Mēģināsim atšifrēt servera atbildi. Apskatām klases kodu com.uc.deployment.UpgradeDeployService: no metodes onStartCommand pārejam uz com.uc.deployment.b.x, un no tā uz com.uc.browser.core.d.c.f.e:
publiska beigu metode void e(l arg9) {
int v4_5;
String v3_1;
byte[] v3;
byte[] v1 = null;
ja arg9 == null) {
v3 = v1;
}
citādi {
v3_1 = arg9.iGX.ipR;
StringBuilder v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]produkts:");
v4.append(arg9.iGX.ipR);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]versija:");
v4.append(arg9.iGX.iEn);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]jaunināšanas_tips:");
v4.append(arg9.iGX.mMode);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]spēkā_būt:");
v4.append(arg9.iGX.iEo);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]klusa_režīma:");
v4.append(arg9.iGX.iDQ);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]klusa_tipa:");
v4.append(arg9.iGX.iEr);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]klusa_stāvokļa:");
v4.append(arg9.iGX.iEp);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]klusa_faila:");
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("]lejamā_tipa:");
v4.append(arg9.mDownloadType);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]lejamā_grupa:");
v4.append(arg9.mDownloadGroup);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]lejamā_ceļa:");
v4.append(arg9.iGH);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_bērna_versija:");
v4.append(arg9.iGX.iEx);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_sērija:");
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;
ja(v3_2 != null && v3_2.size() != 0) {
Iterator v3_3 = v3_2.iterator();
kam v3_3.hasNext() {
Object v4_1 = v3_3.next();
StringBuilder v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]komponenta_nosaukums:");
v5.append(((au)v4_1).getName());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]komponenta_versijas_nosaukums:");
v5.append(((au)v4_1).aDA());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]komponenta_versijas_kods:");
v5.append(((au)v4_1).gBl);
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]komponenta_prasību_tips:");
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_bērna_versija", arg9.iGX.iEx));
v3_2.add(g.fs("versija_sērija", arg9.iGX.iEw));
v3_2.add(g.fs("bērna_versija", 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("jaunināšanas_dienasgrāmata", i.bjt()));
v3_2.add(g.fs("klusa_instalācija", String.valueOf(arg9.iGX.iDQ)));
v3_2.add(g.fs("klusa_stāvoklis", String.valueOf(arg9.iGX.iEp)));
v3_2.add(g.fs("klusa_faila", arg9.iGX.iEq));
v3_2.add(g.fs("klusa_tipa", 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("kopējā_ram", 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();
kam 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();
}
ja v3 == null) {
this.iGY.iGI.a(arg9, "up_encode", "jā", "neizdodas");
returns;
}
v4_5 = this.iGY.iGw ? 0x1F : 0;
ja v3 == null) {
}
citādi {
v3 = g.i(v4_5, v3);
ja v3 == null) {
}
citādi {
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);
}
}
ja v1 == null) {
this.iGY.iGI.a(arg9, "up_encrypt", "jā", "neizdodas");
returns;
}
ja(TextUtils.isEmpty(this.iGY.mUpgradeUrl)) {
this.iGY.iGI.a(arg9, "up_url", "jā", "neizdodas");
returns;
}
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", "jā", "veiksmīgi");
this.iGY.iGI.b(arg9);
}Šeit redzam POST pieprasījuma veidošanu. Pievēršam uzmanību 16 baitus lielā masīva izveidei un tā aizpildīšanai: 0x5F, 0, 0x1F, -50 (=0xCE). Tas sakrīt ar to, ko mēs redzējām iepriekšējā pieprasījumā.
Šajā pašā klasē var pamanīt iekļauto klasi, kurā ir vēl viens interesants metods:
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");
}
} Metode iegūst byte masīvu un pārbauda, vai nulles byte ir vienāds ar 0x60 vai trešais byte ir vienāds ar 0xD0, bet otrais byte — 1, 11 vai 0x1F. Apskatām atbildi no servera: nulles byte — 0x60, otrais — 0x1F, trešais — 0x60. Izskatās, ka tas, kas mums vajadzīgs. Spriežot pēc rindām ("up_decrypt", piemēram), šeit jābūt izsaukumam metodei, kas atšifrēs servera atbildi.
Pārejam uz metodi g.j. Pamanām, ka kā pirmais arguments tiek nodots byte ar pārvietojumu 2 (t.i., 0x1F mūsu gadījumā), bet kā otrais — servera atbilde bez
pirmajiem 16 baitiem.
public static byte[] j(int arg1, byte[] arg2) {
ja (arg1 == 1) {
arg2 = c.c(arg2, c.adu);
}
citādi, ja (arg1 == 11) {
arg2 = m.aF(arg2);
}
citādi, ja (arg1 != 0x1F) {
}
citādi {
arg2 = EncryptHelper.decrypt(arg2);
}
atgriezt arg2;
} Acīmredzot notiek šifrēšanas algoritma izvēle, un tas pats byte, kas mūsu
gadījumā ir 0x1F, kas apzīmē vienu no trim iespējamiem variantiem.
Turpinām koda analīzi. Pēc pāris lēcieniem nonākam metodē ar runājošu nosaukumu decryptBytesByKey.
Šeit no mūsu atbildes atdala vēl divus baitus, un no tiem iznāk virknes. Acīmredzot šādā veidā tiek izvēlēts atslēgas parametrs ziņojuma atšifrēšanai.
private static byte[] decryptBytesByKey(byte[] bytes) {
byte[] v0 = null;
ja (bytes != null) {
mēģināt {
ja (bytes.length < EncryptHelper.PREFIX_BYTES_SIZE) {
}
citādi ja (bytes.length == EncryptHelper.PREFIX_BYTES_SIZE) {
atgriezt v0;
}
citādi {
byte[] prefix = new byte[EncryptHelper.PREFIX_BYTES_SIZE]; \/\/ 2 baiti
System.arraycopy(bytes, 0, prefix, 0, prefix.length);
String keyId = c.ayR().d(ByteBuffer.wrap(prefix).getShort()); \/\/ Atslēgas izvēle
ja (keyId == null) {
atgriezt v0;
}
citādi {
a v2 = EncryptHelper.ayL();
ja (v2 == null) {
atgriezt v0;
}
citādi {
byte[] enrypted = new byte[bytes.length - EncryptHelper.PREFIX_BYTES_SIZE];
System.arraycopy(bytes, EncryptHelper.PREFIX_BYTES_SIZE, enrypted, 0, enrypted.length);
atgriezt v2.l(keyId, enrypted);
}
}
}
}
catch (SecException v7_1) {
EncryptHelper.handleDecryptException(((Throwable)v7_1), v7_1.getErrorCode());
atgriezt v0;
}
catch (Throwable v7) {
EncryptHelper.handleDecryptException(v7, 2);
atgriezt v0;
}
}
atgriezt v0;
}Iepriekš minot, atzīmēsim, ka šajā posmā vēl nav atslēgas, bet gan tikai tās „identifikators”. Ar atslēgas iegūšanu viss ir nedaudz sarežģītāk.
Nākamajā metodē esošajiem parametriem tiek pievienoti vēl divi, un to kļūst četri: burvju skaitlis 16, atslēgas identifikators, šifrētie dati un neskaidra virknes (mūsu gadījumā tukša).
public final byte[] l(String keyId, byte[] encrypted) throws SecException {
return this.ayJ().staticBinarySafeDecryptNoB64(16, keyId, encrypted, "");
}Pēc virknes pāreju nonākam pie metodes staticBinarySafeDecryptNoB64 interfeisa com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. Galvenajā lietotnes kodā nav nekādu klašu, kas īsteno šo interfeisu. Šāda klase ir failā lib\/armeabi-v7a\/libsgmain.so, kas patiesībā nav .so, bet gan .jar. Mūs interesējošā metode tiek īstenota šādi:
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 0) {
return this.b(magicInt, keyId, encrypted, magicString);
}
throw new SecException("", 301);
}
//...
} Šeit mūsu parametru saraksts tiek papildināts vēl ar diviem veselajiem skaitļiem: 2 un 0. Acīmredzot,
2 nozīmē atšifrēšanu, kā metodē doFinal sistēmas klasē javax.crypto.Cipher. Un viss tas tiek nodots kādam Router ar numuru 10601 — šķiet, tas ir komandas numurs.
Pēc kārtējās pāreju ķēdes atrodam klasi, kas implementē interfeisu IRouterComponent un metode 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);
}
}Un arī klase JNICLibrary, kurā ir deklarēta natīvā metode doCommandNative:
package com.taobao.wireless.security.adapter;
public class JNICLibrary {
public static native Object doCommandNative(int arg0, Object[] arg1);
}Tātad, mums jāatrod natīvā kodā metode doCommandNative. Un te sākas lielā jautrība.
Obfuskācija mašīnkodā
Failā libsgmain.so (kas patiesībā ir .jar un kur mēs nedaudz augstāk esam atraduši dažu interfeisu, kas saistīti ar šifrēšanu, implementāciju) ir viena natīvā bibliotēka: libsgmainso-6.4.36.so. Atveram to IDA un saņemam kaudzi dialoga logu ar kļūdām. Problēma ir tāda, ka sadaļu tabula (section header table) – ir nederīga. Tas izdarīts speciāli, lai sarežģītu analīzi.

Bet tā nav nepieciešama: lai pareizi ielādētu ELF failu un analizētu to, ir pietiekami ar segmentu tabulu (program header table). Tāpēc mēs vienkārši izdzēšam sadaļu tabulu, nullējot atbilstošos laukus virsrakstā.

Atkal atveram failu IDA.
Ir divi veidi, kā paziņot virtuālajai Java mašīnai, kur tieši natīvajā bibliotēkā atrodas metode, ko Java kodā deklarē kā natīvu. Pirmais — dot tam vārdu formā Java_paketes_nosaukums_Klases_nosaukums_metodes_nosaukums.
Otrā — reģistrēt to, kad tiek ielādēta bibliotēka (funkcijā JNI_OnLoad)
izmantojot funkcijas izsaukumu RegisterNatives.
Mūsu gadījumā, ja izmantojam pirmo veidu, nosaukumam jābūt šādam: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.
Starppārbaudīto funkciju nav, tas nozīmē, ka ir jāmeklē izsaukums RegisterNatives.
Iedvesmojamies uz funkciju JNI_OnLoad un redzam šādu ainu:

Kas te notiek? Pirmajā mirklī funkcijas sākums un beigas ir tipiskas ARM arhitektūrai. Pirmajā instrukcijā stekā tiek saglabāta reģistru saturs, kurus funkcija izmantos (šajā gadījumā R0, R1 un R2), kā arī reģistra LR saturs, kurā atrodas adrese, uz kuru tiek atgriezts no funkcijas. Pēdējā instrukcijā saglabātie reģistri tiek atjaunoti, un adrese, uz kuru tiks atgriezts, tiek tūlīt ievietota reģistrā PC — tādējādi notiek atgriezeniskā darbība no funkcijas. Bet, ja pievēršam uzmanību, varam pamanīt, ka pirms pēdējās instrukcijas tiek mainīta adreses atgriešana, kas uzglabāta stekā. Aprēķināsim, kāda tā būs pēc
koda izpildes. R1 tiek ielādēta kāda adrese 0xB130, no tās tiek atņemti 5, tad tā tiek pārcelta uz R0 un pie tās tiek pieskaitīta 0x10. Iegūstam 0xB13B. Tādējādi IDA domā, ka pēdējā instrukcijā notiek parasta atgriešanās no funkcijas, bet patiesībā notiek pāreja uz aprēķināto adresi 0xB13B.
Šeit ir vērts atgādināt, ka ARM procesoriem ir divi režīmi un divi instrukciju komplekti: ARM un Thumb. Zemākais bits adresē norāda procesoram, kurš instrukciju komplekts tiek izmantots. T. i., adrese patiesībā ir 0xB13A, un viena zemākajā bitā apzīmē režīmu Thumb.
Katras funkcijas sākumā šajā bibliotēkā ir pievienots līdzīgs «pārveidotājs» un
atkritumu kods. Turpmāk neapstāsies pie tiem — vienkārši atceramies,
ka faktiskais sākums gandrīz visām funkcijām atrodas nedaudz tālāk.
Tā kā kodā nav skaidru pāreju uz 0xB13A, IDA pati nav atpazīstams, ka šajā vietā atrodas kods. Tieši tāpēc lielāko daļu koda bibliotēkā tā neatzīst kā kodu, kas nedaudz apgrūtina analīzi. Sakām IDA, ka šeit ir kods, un lūk, kas sanāk:

0xB144 skaidri sākas tabula. Un kas ir sub_494C?

Izsaucot šo funkciju, reģistrā LR saņemsim iepriekš minētās tabulas adresi (0xB144). R0 — indekss šajā tabulā. T. i., tiek ņemta vērtība no tabulas, pievienota LR, un iegūstam
adrese, uz kuru jāpāriet. Mēģināsim to aprēķināt: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Pārejam uz iegūto adresi un redzam burtiski dažus noderīgus norādījumus un atkal pāreju uz 0xB140:

Tagad būs pāreja pa nobīdi ar indeksu 0x20 no tabulas.
Spriežot pēc tabulas lieluma, šādu pāreju kodā būs daudz. Rodas jautājums, vai ir iespējams kaut kā to automatizēt, nevis manuāli aprēķināt adreses. Un uz palīgu nāk skripti un iespēja pataisīt kodu IDA:
def put_unconditional_branch(source, destination):
offset = (destination - source - 4) >> 1
if offset > 2097151 or offset 1023 or offset > 11) & 0x7ff)
instruction2 = 0xb800 | (offset & 0x7ff)
patch_word(source, instruction1)
patch_word(source + 2, instruction2)
else:
instruction = 0xe000 | (offset & 0x7ff)
patch_word(source, instruction)
ea = here()
if get_wide_word(ea) == 0xb503: #PUSH {R0,R1,LR}
ea1 = ea + 2
if get_wide_word(ea1) == 0xbf00: #NOP
ea1 += 2
if get_operand_type(ea1, 0) == 1 and get_operand_value(ea1, 0) == 0 and get_operand_type(ea1, 1) == 2:
index = get_wide_dword(get_operand_value(ea1, 1))
print "index =", hex(index)
ea1 += 2
if get_operand_type(ea1, 0) == 7:
table = get_operand_value(ea1, 0) + 4
elif get_operand_type(ea1, 1) == 2:
table = get_operand_value(ea1, 1) + 4
else:
print "Nepareiza operanda tips uz", hex(ea1), "-", get_operand_type(ea1, 0), get_operand_type(ea1, 1)
table = None
if table is None:
print "Nevar atrast tabulu"
else:
print "table =", hex(table)
offset = get_wide_dword(table + (index << 2))
put_unconditional_branch(ea, table + offset)
else:
print "Nezināms kods", get_operand_type(ea1, 0), get_operand_value(ea1, 0), get_operand_type(ea1, 1) == 2
else:
print "Nevar noteikt pirmo instrukciju"Novietojam kursoru uz rindiņas 0xB26A, palaižam skriptu un redzam pāreju uz 0xB4B0:

IDA atkal nav atpazinusi šo sadaļu kā kodu. Palīdzam tai un redzam tur citu konstrukciju:

Instrukcijas pēc BLX izskatās ne pārāk jēgpilnas, tās vairāk atgādina kādu nobīdi. Apskatīsim sub_4964:

Un tiešām, šeit tiek ņemts dword no adreses, kas atrodas LR, pievienojot šai adresei, pēc tam tiek ņemts vērtība no iegūtās adreses un liek uz steka. Tāpat LR tiek pievienoti 4, lai pēc atgriešanās no funkcijas pārlēktu šo nobīdi. Pēc tam komanda POP {R1} izvelk iegūto vērtību no steka. Ja paskatāmies, kas atrodas adresē 0xB4BA + 0xEA = 0xB5A4, tad varam redzēt kaut ko līdzīgu adresu tabulai:

Lai pielāgotu šo konstrukciju, būs nepieciešams iegūt divus parametrus no koda: pārvietojumu un reģistra numuru, kurā jāievieto rezultāts. Katram iespējamam reģistram iepriekš būs jāsagatavo neliels koda fragments.
patches = {}
patches[0] = (0x00, 0xbf, 0x01, 0x48, 0x00, 0x68, 0x02, 0xe0)
patches[1] = (0x00, 0xbf, 0x01, 0x49, 0x09, 0x68, 0x02, 0xe0)
patches[2] = (0x00, 0xbf, 0x01, 0x4a, 0x12, 0x68, 0x02, 0xe0)
patches[3] = (0x00, 0xbf, 0x01, 0x4b, 0x1b, 0x68, 0x02, 0xe0)
patches[4] = (0x00, 0xbf, 0x01, 0x4c, 0x24, 0x68, 0x02, 0xe0)
patches[5] = (0x00, 0xbf, 0x01, 0x4d, 0x2d, 0x68, 0x02, 0xe0)
patches[8] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0x80, 0xd8, 0xf8, 0x00, 0x80, 0x01, 0xe0)
patches[9] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0x90, 0xd9, 0xf8, 0x00, 0x90, 0x01, 0xe0)
patches[10] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0xa0, 0xda, 0xf8, 0x00, 0xa0, 0x01, 0xe0)
patches[11] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0xb0, 0xdb, 0xf8, 0x00, 0xb0, 0x01, 0xe0)
ea = here()
if (get_wide_word(ea) == 0xb082 #SUB SP, SP, #8
and get_wide_word(ea + 2) == 0xb503): #PUSH {R0,R1,LR}
if get_operand_type(ea + 4, 0) == 7:
pop = get_bytes(ea + 12, 4, 0)
if pop[1] == 'xbc':
register = -1
r = get_wide_byte(ea + 12)
for i in range(8):
if r == (1 <> 4
if register in patches:
address = get_wide_dword(ea + 8) + ea + 8
for b in patches[register]:
patch_byte(ea, b)
ea += 1
patch_dword(ea, address)
else:
print "POP instrukcija netika atrasta"
else:
print "Nepareizs operandu veids uz +4:", get_operand_type(ea + 4, 0)
else:
print "Neizdevās noteikt pirmās instrukcijas"Novietojam kursoru konstrukcijas sākumā, kuru vēlamies nomainīt — 0xB4B2 — un palaižam skriptu:

Papildus jau minētajām konstrukcijām kodā arī sastopami šādi:

Kā iepriekšējā gadījumā, pēc instrukcijas BLX seko pārvietojums:

Paņemam pārvietojumu no adreses LR, pievienojam to LR un nokļūstam tur. 0x72044 + 0xC = 0x72050. Skripts šai konstrukcijai ir pavisam vienkāršs:
def put_unconditional_branch(source, destination):
offset = (destination - source - 4) >> 1
if offset > 2097151 or offset 1023 or offset > 11) & 0x7ff)
instruction2 = 0xb800 | (offset & 0x7ff)
patch_word(source, instruction1)
patch_word(source + 2, instruction2)
else:
instruction = 0xe000 | (offset & 0x7ff)
patch_word(source, instruction)
ea = here()
if get_wide_word(ea) == 0xb503: #PUSH {R0,R1,LR}
ea1 = ea + 6
if get_wide_word(ea + 2) == 0xbf00: #NOP
ea1 += 2
offset = get_wide_dword(ea1)
put_unconditional_branch(ea, (ea1 + offset) & 0xffffffff)
else:
print "Neizdevās noteikt pirmo instrukciju"Skripta izpildes rezultāts:

Pēc tam, kad funkcija ir pilnībā pielāgota, var norādīt IDA uz tās īsto sākumu. Tā apkopos visu funkcijas kodu pa daļām, un to varēs dekodēt ar HexRays palīdzību.
Virknu atšifrēšana
Mēs esam iemācījušies cīnīties ar mašīnkoda obfuskāciju bibliotēkā libsgmainso-6.4.36.so no UC Browser un saņēmuši funkcijas kodu JNI_OnLoad.
int __fastcall real_JNI_OnLoad(JavaVM *vm)
{
int rezultāts; // 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;
ja ( !vm )
goto LABEL_39;
sub_7C4F4();
env = (JNIEnv *)sub_7C5B0(0);
ja ( !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);
ja ( 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) )
{
rezultāts = (sub_B7B0(env, clazz) >> 31) | 0x10004;
}
ja
{
LABEL_39:
rezultāts = -1;
}
atgriezt rezultāts;
}Papildus apskatīsim nākamās rindas:
sub_73E24(&unk_83EA6, &v6, 49);
clazz = (jclass)((int (__fastcall *)(JNIEnv *, int *))(*env)->FindClass)(env, &v6);Funkcijā sub_73E24 acīmredzot notiek klases nosaukuma atšifrēšana. Šīs funkcijas parametri ietver norādi uz datiem, kas izskatās pēc šifrētiem, simbolu buferi un skaitli. Acīmredzami, ka pēc funkcijas izsaukšanas buferī būs atšifrēta virkne, jo tā tiek nodota funkcijai FindClass, kas kā otro parametru pieņem klases nosaukumu. Tātad skaitlis ir bufera izmērs vai virknes garums. Mēģināsim atšifrēt klases nosaukumu, tam vajadzētu norādīt, vai ejam pareizajā virzienā. Apskatīsim sīkāk, kas notiek 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;
}Funkcija sub_7AF78 izveido kontūru priekš byte masīviem norādītā izmēra (neplānojam detalizēti apspriest šo konteineru). Šeit tiek izveidoti divi tādi konteineri: vienā ievieto virkni «DcO/lcK+h?m3c*q@» (nav grūti uzminēt, ka šis ir atslēga), otrā — šifrētie dati. Turklāt abi objekti tiek ievietoti kādā struktūrā, kas tiek nodota funkcijai sub_6115C. Tāpat atzīmēsim šajā struktūrā lauku ar vērtību 3. Paskatīsimies, kas ar šo struktūru notiek tālāk.
int __fastcall sub_611B4(struc_1 *a1, _DWORD *a2)
{
int v3; // lr
unsigned int v4; // r1
int v5; // r0
int v6; // r1
int result; // r0
int v8; // r0
*a2 = 820000;
if ( a1 )
{
v3 = a1->field_14;
if ( v3 )
{
v4 = a1->field_4;
if ( v4 field_0, a1->field_10, v3);
goto LABEL_17;
case 3u:
v8 = sub_6364C(a1->field_0, a1->field_10, v3);
goto LABEL_17;
case 0x10u:
case 0x11u:
case 0x12u:
v8 = sub_612F4(
a1->field_0,
v4,
*(_QWORD *)&a1->field_8,
*(_QWORD *)&a1->field_8 >> 32,
a1->field_10,
v3,
a2);
goto LABEL_17;
case 0x14u:
v8 = sub_63A28(a1->field_0, v3);
goto LABEL_17;
case 0x15u:
sub_61A60(a1->field_0, v3, a2);
return result;
case 0x16u:
v8 = sub_62440(a1->field_14);
goto LABEL_17;
case 0x17u:
v8 = sub_6226C(a1->field_10, v3);
goto LABEL_17;
case 0x18u:
v8 = sub_63530(a1->field_14);
LABEL_17:
v6 = 0;
if ( v8 )
{
*a2 = 0;
v6 = v8;
}
return v6;
default:
LOWORD(v5) = 28032;
goto LABEL_5;
}
}
}
}
LOWORD(v5) = -27504;
LABEL_5:
HIWORD(v5) = 13;
v6 = 0;
*a2 = v5;
return v6;
}Kā mainīgais switch tiek nodots struktūrai, kurai iepriekš ir piešķirta vērtība 3. Apskatām case 3: funkcijā sub_6364C tiek nodoti parametri no struktūras, kas iepriekšējā funkcijā tika iekļauti, t. i., atslēga un šifrētie dati. Ja uzmanīgi paskatāmies uz sub_6364C, varam uzzināt tajā algoritmu RC4.
Mums ir algoritms un atslēga. Mēģināsim atšifrēt klases nosaukumu. Lūk, ko esam ieguvuši: com/taobao/wireless/security/adapter/JNICLibrary. Lieliski! Mēs esam pareizajā virzienā.
Komandu koks
Tagad mums jāatrod izsaukums RegisterNatives, kas norādīs uz funkciju doCommandNative. Pārskatām funkcijas, kas tiek izsauktas no JNI_OnLoad, un atrodam to 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;
}Un tiešām, šeit reģistrējas natīvā metode ar nosaukumu doCommandNative. Tagad mēs zinām tās adresi. Apskatīsim, ko tā dara.
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;
}Pēc nosaukuma varam uzminēt, ka šeit atrodas visu funkciju ieejas punkts, kuras izstrādātāji nolēma pārvietot uz natīvo bibliotēku. Mūs interesē funkcija ar numuru 10601.
Pēc koda var redzēt, ka no komandas numura tiek iegūti trīs skaitļi: command / 10000, command % 10000 / 100 un command % 10, t. i., mūsu gadījumā, 1, 6 un 1. Šie trīs skaitļi, kā arī norāde uz JNIEnv un funkcijai nodotajiem argumentiem tiek apvienoti struktūrā un nodoti tālāk. Izmantojot iegūtos trīs skaitļus (apzīmēsim tos ar N1, N2 un N3), tiek veidots komandu koks.
Aptuveni tāds:

Koks tiek aizpildīts dinamiski JNI_OnLoad.
Trīs skaitļi kodē ceļu kokā. Katrs koka lapu satur atbilstošās funkcijas adresi. Atslēga ir vecākajā mezglā. Atrast vietu kodā, kur kokā pievieno nepieciešamo funkciju, nav grūti, ja saprot visus izmantotos strukturālos elementus (to aprakstu neiekļaujam, lai nepārslogotu jau tā apjomīgo rakstu).
Vēl obfiskācija
Mēs saņēmām funkcijas adresi, kas jādecifrē trafiku: 0x5F1AC. Bet vēl ir pāragri priecāties: UC Browser izstrādātāji ir sagatavojuši vēl vienu pārsteigumu.
Pēc parametru saņemšanas no masīva, kas veidots Java kodā, nonākam
funkcijā ar adresi 0x4D070. Un šeit mūs gaida vēl viena koda obfiskācijas forma.
Ieliekam R7 un R4 divus indeksus:

Pārceļam pirmo indeksu uz R11:

Lai iegūtu adresi no tabulas, izmantojam indeksu:

Pēc pārejas uz pirmo adresi tiek izmantots otrais indekss, kas ir R4. Tabulā ir 230 elementi.
Ko ar to darīt? Var pateikt IDA, ka tas ir tāds switch: Edit -> Other -> Specify switch idiom.

Iegūtais kods ir biedējošs. Bet, izkārtojoties caur tā mežiem, var pamanīt jau pazīstamās funkcijas izsaukumu sub_6115C:

Tur bija switch, kurā case 3 atradās dekodēšana, izmantojot RC4 algoritmu. Un šajā gadījumā funkcijai nodotā struktūra tiek aizpildīta no parametriem, kas nodoti doCommandNative. Atceramies, ka mums tur bija magicInt ar vērtību 16. Skatāmies atbilstošo case – un pēc dažām pārejām atrodam kodu, pēc kura var identificēt algoritmu.

Tas ir AES!
Algoritms ir iegūts, palicis tikai saņemt tā parametrus: režīms, atslēga un, iespējams, inicializācijas vektors (t tā esamība ir atkarīga no AES darba režīma). Struktūrai ar tiem jāveidojas kaut kur pirms funkcijas sub_6115C, bet šī koda daļa ir obfiskēta īpaši labi, tāpēc rodas ideja pārrakstīt kodu, lai visi dekodēšanas funkcijas parametri tiek izrakstīti failā.
Pārlikums
Lai visu pārlikuma kodu neierakstītu manuāli assemblera valodā, var iedarbināt Android Studio, uzrakstīt tur funkciju, kas saņem tādus pašus parametrus kā mūsu dekodēšanas funkcija, un raksta failā, pēc tam nokopēt kodu, ko ģenerēs kompilators.
Par ērtību pievienot kodu mūsu draugi no UC Browser komandas arī ir «parūpējušies». Atceramies, ka katras funkcijas sākumā mums ir atkritumu kods, kuru viegli var nomainīt ar jebkuru citu. Ļoti ērti 🙂 Diemžēl mērķa funkcijas sākumā ir par maz vietas kodam, kas saglabā visus parametrus failā. Nācas to dalīt daļās un izmantot blakus funkciju atkritumu blokus. Kopā sanāca četras daļas.
Pirma daļa:

ARM arhitektūrā pirmie četri funkcijas parametri tiek nodoti caur reģistiem R0-R3, pārējie, ja tādi ir, — caur steku. Reģistrā LR tiek nodota atgriešanās adrese. Viss šis jāizsaglabā, lai funkcija varētu strādāt pēc tam, kad mēs iesaldēsim tās parametrus. Tāpat jāizsaglabā visi reģistri, kurus lietosim procesā, tāpēc veicam PUSH.W {R0-R10,LR}. R7 mums iegūst parametru saraksta adresi, kas tika nodota funkcijai caur steku.
Izmantojot funkciju fopen atvērsim failu /data/local/tmp/aes režīmā «ab»,
t. i., pievienošanai. R0 ielādējam faila nosaukuma adresi, R1 — adresi ar režīma norādi. Un šeit atkritumu kods beidzas, tāpēc pārejam uz nākamo funkciju. Lai tā turpinātu strādāt, ievietojam sākumā pāreju uz īsto funkcijas kodu, apejot atkritumus, bet atkritumos pievienojam turpinājumu.

Izsaucam fopen.
Pirmie trīs funkcijas parametri aes ir tipa int. Tā kā mēs sākumā saglabājām reģistrus stekā, varam vienkārši nodot funkcijai fwrite to adreses stekā.

Tālāk mums ir trīs struktūras, kas satur datu lielumu un datu rādītāju atslēgai, inicializācijas vektoram un šifrētajiem datiem.

Beigās aizveram failu, atjaunojam reģistrus un nododam kontroli īstajai funkcijai. aes.
Sagatavojam APK ar pielāgotu bibliotēku, parakstām, nosūtām uz ierīci/emuļatoru, un palaidām. Redzam, ka mūsu dumps tiek radīts, un tur tiek rakstīti daudzi dati. Pārlūks izmanto šifrēšanu ne tikai datu pārraidē, un visa šifrēšana notiek caur apskatāmo funkciju. Bet vajadzīgie dati kaut kādā veidā nav pieejami, un trafika pieprasījums nav redzams. Lai nenāktos gaidīt, kamēr UC Browser nolems izpildīt vajadzīgo pieprasījumu, paņemsim iepriekš iegūto šifrēto atbildi no servera un vēlreiz pielāgosim lietotni: pievienosim atšifrēšanu onCreate galvenajā aktivitātē.
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;)ISagatavojam, parakstām, instalējam, palaidām. Iegūstam NullPointerException, jo metode atgrieza null.
Tālāk analizējot kodu, tika atklāta funkcija, kurā tiek atšifrētas interesantas rindas: «META-INF/» un «.RSA». Izskatās, ka lietotne pārbauda savu sertifikātu. Vai pat ģenerē atslēgas no tā. Nevēlamies pamatīgi analizēt, kas notiek ar sertifikātu, tāpēc vienkārši iedosim tam pareizo sertifikātu. Propatčos šifrēto rindu tā, lai vietā «META-INF/» būtu «BLABLINF/», izveidosim mapīti ar šādu nosaukumu APK un iestiprināsim tur balto pārlūka sertifikātu.
Sagatavojam, parakstām, instalējam, palaidām. Bingo! Atslēga ir mūsu rīcībā!
MitM
Mēs saņēmām atslēgu un inicializācijas vektoru, kas ir līdzīgs atslēgai. Mēģināsim atšifrēt servera atbildi CBC režīmā.

Redzam arhīva URL, kaut kas līdzīgs MD5, «extract_unzipsize» un skaitlis. Pārbaudām: arhīva MD5 sakrīt, atšifrētās bibliotekas izmērs sakrīt. Mēģinām propatčot šo bibliotēku un nodot to pārlūkam. Lai parādītu, ka mūsu propatčotā bibliotēka ir ielādēta, palaidīsim Intent uz SMS izveidi ar tekstu «PWNED!». Nomainīsim divas servera atbildes: un uz arhīva lejupielādi. Pirmajā nomainām MD5 (izmērs pēc atšifrēšanas nemainās), otrajā nododam arhīvu ar propatčoto bibliotēku.
Pārlūks pāris reizes mēģina lejupielādēt arhīvu, pēc tam izmet kļūdu. Izskatās, ka kaut kas
tam nepatīk. Analizējot šo neskaidro formātu, noskaidrojām, ka serveris pārsūta arī arhīva izmēru:

Tas ir kodēts LEB128. Pēc patčojuma arhīva izmērs ar bibliotēku nedaudz mainījās, tāpēc pārlūks uzskatīja, ka arhīvs tika nepareizi lejupielādēts, un pēc dažiem mēģinājumiem izmeta kļūdu.
Labojam arhīva izmēru… Un – uzvara! 🙂 Rezultāts video.
Sekas un izstrādātāja reakcija
Tāpat hakeri varētu izmantot UC Browser nevēlamo funkciju, lai izplatītu un palaistu ļaunprātīgas bibliotēkas. Šīs bibliotēkas darbosies pārlūkprogrammas kontekstā, tādējādi iegūstot visas tās sistēmas atļaujas. Rezultātā radīsies iespēja rādīt pikšķerēšanas logus, kā arī piekļūt oranžās ķīniešu vāveres darba failiem, tostarp datu bāzē glabātajiem lietotājvārdiem, parolēm un sīkdatnēm.
Mēs sazinājāmies ar UC Browser izstrādātājiem un informējām viņus par atrasto problēmu, mēģinājām norādīt uz ievainojamību un tās bīstamību, taču viņi nevēlējās ar mums par to diskutēt. Savukārt pārlūkprogramma turpināja izrādīt bīstamo funkciju visiem redzot. Taču, tiklīdz mēs atklājām ievainojamības detaļas, vairs nebija iespējams ignorēt šo situāciju kā iepriekš. 27. martā tika
izlaista jauna UC Browser versija 12.10.9.1193, kas sazinājās ar serveri, izmantojot HTTPS: .
Turklāt, pēc "labojuma" un līdz raksta rakstīšanas brīdim, mēģinājums atvērt PDF pārlūkā izraisīja kļūdas ziņojuma parādīšanos ar tekstu "Oups, kaut kas nogāja greizi!". Pieprasījums serverim mēģinājuma atvērt PDF laikā netika izpildīts, taču pieprasījums tika veikts, kad pārlūkprogramma tika iedarbināta, kas liecina par palikušu iespēju lejupielādēt izpildāmu kodu, pārkāpjot Google Play noteikumus.
Avots: habr.com
