Duke kërkuar për dobësi në Shfletuesin UC

Duke kërkuar për dobësi në Shfletuesin UC

Paraqitje

Në fund të marsit ne raportuar, se ata zbuluan një aftësi të fshehur për të ngarkuar dhe ekzekutuar kodin e paverifikuar në UC Browser. Sot do të shikojmë në detaje se si ndodh ky shkarkim dhe si mund ta përdorin hakerat për qëllimet e tyre.

Disa kohë më parë, UC Browser u reklamua dhe u shpërnda në mënyrë shumë agresive: ai u instalua në pajisjet e përdoruesve duke përdorur malware, i shpërndarë nga faqe të ndryshme nën maskën e skedarëve video (d.m.th., përdoruesit mendonin se po shkarkonin, për shembull, një video pornografike, por në vend të kësaj mori një APK me këtë shfletues), përdori banderola të frikshme me mesazhe se shfletuesi ishte i vjetëruar, i cenueshëm dhe gjëra të tilla. Në grupin zyrtar të Shfletuesit UC në VK ekziston тема, në të cilën përdoruesit mund të ankohen për reklama të padrejta, ka shumë shembuj atje. Në vitin 2016 ka pasur madje reklama me video në Rusisht (po, reklama për një shfletues që bllokon reklamat).

Në kohën e shkrimit, UC Browser ka mbi 500 instalime në Google Play. Kjo është mbresëlënëse - vetëm Google Chrome ka më shumë. Ndër rishikimet mund të shihni mjaft ankesa për reklamat dhe ridrejtimet në disa aplikacione në Google Play. Kjo ishte arsyeja e kërkimit tonë: vendosëm të shohim nëse UC Browser po bënte diçka të keqe. Dhe doli që ai po!

Në kodin e aplikacionit, u zbulua aftësia për të shkarkuar dhe ekzekutuar kodin e ekzekutueshëm, që është në kundërshtim me rregullat për publikimin e aplikacioneve në Google Play. Përveç shkarkimit të kodit të ekzekutueshëm, UC Browser e bën këtë në një mënyrë të pasigurt, e cila mund të përdoret për të nisur një sulm MitM. Le të shohim nëse mund të kryejmë një sulm të tillë.

Gjithçka e shkruar më poshtë është e rëndësishme për versionin e Shfletuesit UC që ishte i disponueshëm në Google Play në kohën e studimit:

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

Vektori i sulmit

Në manifestin e Shfletuesit UC mund të gjeni një shërbim me një emër të vetëshpjegueshëm com.uc.deployment.UpgradeDeployService.

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

Kur fillon ky shërbim, shfletuesi i bën një kërkesë POST puds.ucweb.com/upgrade/index.xhtml, e cila mund të shihet në trafik pak kohë pas fillimit. Si përgjigje, ai mund të marrë një komandë për të shkarkuar ndonjë përditësim ose modul të ri. Gjatë analizës, serveri nuk dha komanda të tilla, por ne vumë re se kur përpiqemi të hapim një PDF në shfletues, ai bën një kërkesë të dytë në adresën e specifikuar më lart, pas së cilës shkarkon bibliotekën vendase. Për të kryer sulmin, vendosëm të përdorim këtë veçori të Shfletuesit UC: aftësinë për të hapur PDF duke përdorur një bibliotekë amtare, e cila nuk është në APK dhe të cilën e shkarkon nga Interneti nëse është e nevojshme. Vlen të përmendet se, teorikisht, Shfletuesi UC mund të detyrohet të shkarkojë diçka pa ndërveprim të përdoruesit - nëse jepni një përgjigje të mirëformuar ndaj një kërkese që ekzekutohet pasi shfletuesi është nisur. Por për ta bërë këtë, ne duhet të studiojmë më në detaje protokollin e ndërveprimit me serverin, kështu që vendosëm që do të ishte më e lehtë të redaktonim përgjigjen e përgjuar dhe të zëvendësonim bibliotekën për të punuar me PDF.

Pra, kur një përdorues dëshiron të hapë një PDF direkt në shfletues, kërkesat e mëposhtme mund të shihen në trafik:

Duke kërkuar për dobësi në Shfletuesin UC

Së pari ka një kërkesë POST për të puds.ucweb.com/upgrade/index.xhtml, atëherë
Një arkiv me një bibliotekë për shikimin e formateve PDF dhe zyrës është shkarkuar. Është logjike të supozohet se kërkesa e parë transmeton informacion në lidhje me sistemin (të paktën arkitekturën për të siguruar bibliotekën e kërkuar), dhe në përgjigje të saj shfletuesi merr disa informacione rreth bibliotekës që duhet të shkarkohet: adresën dhe, ndoshta , diçka tjetër. Problemi është se kjo kërkesë është e koduar.

Kërko fragment

Fragmenti i përgjigjes

Duke kërkuar për dobësi në Shfletuesin UC

Duke kërkuar për dobësi në Shfletuesin UC

Vetë biblioteka është e paketuar në ZIP dhe nuk është e koduar.

Duke kërkuar për dobësi në Shfletuesin UC

Kërkoni për kodin e deshifrimit të trafikut

Le të përpiqemi të deshifrojmë përgjigjen e serverit. Le të shohim kodin e klasës com.uc.deployment.UpgradeDeployService: nga metoda onStartCommand shkoni në com.uc.deployment.bx, dhe nga ajo në 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);
}

Ne shohim formimin e një kërkese POST këtu. Ne i kushtojmë vëmendje krijimit të një grupi prej 16 bajtësh dhe mbushjes së tij: 0x5F, 0, 0x1F, -50 (=0xCE). Përkon me atë që pamë në kërkesën e mësipërme.

Në të njëjtën klasë mund të shihni një klasë të ndërthurur që ka një metodë tjetër interesante:

        public final void a(l arg10, byte[] arg11) {
f v0 = this.iGQ;
StringBuilder v1 = new StringBuilder("[");
v1.append(arg10.iGX.ipR);
v1.append("]:UpgradeSuccess");
byte[] v1_1 = null;
if(arg11 == null) {
}
else if(arg11.length < 16) {
}
else {
if(arg11[0] != 0x60 && arg11[3] != 0xFFFFFFD0) {
goto label_57;
}
int v3 = 1;
int v5 = arg11[1] == 1 ? 1 : 0;
if(arg11[2] != 1 && arg11[2] != 11) {
if(arg11[2] == 0x1F) {
}
else {
v3 = 0;
}
}
byte[] v7 = new byte[arg11.length - 16];
System.arraycopy(arg11, 16, v7, 0, v7.length);
if(v3 != 0) {
v7 = g.j(arg11[2], v7);
}
if(v7 == null) {
goto label_57;
}
if(v5 != 0) {
v1_1 = g.P(v7);
goto label_57;
}
v1_1 = v7;
}
label_57:
if(v1_1 == null) {
v0.iGY.iGI.a(arg10, "up_decrypt", "yes", "fail");
return;
}
q v11 = g.b(arg10, v1_1);
if(v11 == null) {
v0.iGY.iGI.a(arg10, "up_decode", "yes", "fail");
return;
}
if(v0.iGY.iGt) {
v0.d(arg10);
}
if(v0.iGY.iGo != null) {
v0.iGY.iGo.a(0, ((o)v11));
}
if(v0.iGY.iGs) {
v0.iGY.a(((o)v11));
v0.iGY.iGI.a(v11, "up_silent", "yes", "success");
v0.iGY.iGI.a(v11);
return;
}
v0.iGY.iGI.a(v11, "up_silent", "no", "success");
}
}

Metoda merr një grup bajtësh si hyrje dhe kontrollon që bajt zero është 0x60 ose bajt i tretë është 0xD0, dhe bajt i dytë është 1, 11 ose 0x1F. Ne shikojmë përgjigjen nga serveri: bajt zero është 0x60, i dyti është 0x1F, i treti është 0x60. Tingëllon si ajo që na nevojitet. Duke gjykuar nga rreshtat ("up_decrypt", për shembull), këtu duhet të thirret një metodë që do të deshifrojë përgjigjen e serverit.
Le të kalojmë te metoda gj. Vini re se argumenti i parë është bajt në offset 2 (d.m.th. 0x1F në rastin tonë), dhe i dyti është përgjigja e serverit pa
16 bajtet e para.

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

Natyrisht, këtu ne zgjedhim një algoritëm deshifrimi, dhe të njëjtin bajt që është në tonë
rasti i barabartë me 0x1F, tregon një nga tre opsionet e mundshme.

Ne vazhdojmë të analizojmë kodin. Pas disa kërcimesh e gjejmë veten në një metodë me një emër të vetëshpjegueshëm dekriptojBytesByKey.

Këtu ndahen edhe dy bajt të tjerë nga përgjigja jonë dhe prej tyre merret një varg. Është e qartë se në këtë mënyrë zgjidhet çelësi për deshifrimin e mesazhit.

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

Duke parë përpara, vërejmë se në këtë fazë nuk marrim ende një çelës, por vetëm "identifikuesin" e tij. Marrja e çelësit është pak më e komplikuar.

Në metodën tjetër, dy parametra të tjerë u shtohen atyre ekzistues, duke bërë katër prej tyre: numrin magjik 16, identifikuesin e çelësit, të dhënat e koduara dhe një varg të pakuptueshëm (në rastin tonë, bosh).

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

Pas një sërë tranzicionesh arrijmë te metoda staticBinarySafeDecryptNoB64 ndërfaqja com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. Nuk ka klasa në kodin kryesor të aplikacionit që zbaton këtë ndërfaqe. Ekziston një klasë e tillë në skedar lib/armeabi-v7a/libsgmain.so, e cila në fakt nuk është një .so, por një .kavanoz. Metoda për të cilën ne jemi të interesuar zbatohet si më poshtë:

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

Këtu lista jonë e parametrave plotësohet me dy numra të tjerë të plotë: 2 dhe 0. Duke gjykuar nga
gjithçka, 2 do të thotë deshifrim, si në metodë doFinale klasa e sistemit javax.crypto.Cipher. Dhe e gjithë kjo transferohet në një ruter të caktuar me numrin 10601 - ky është me sa duket numri i komandës.

Pas zinxhirit tjetër të tranzicionit gjejmë një klasë që zbaton ndërfaqen Komponenti IRouter dhe metodë 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);
}
}

Dhe gjithashtu klasë JNICLbiblioteka, në të cilën deklarohet metoda amtare doCommandNative:

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

Kjo do të thotë që ne duhet të gjejmë një metodë në kodin vendas doCommandNative. Dhe këtu fillon argëtimi.

Mbulimi i kodit të makinës

Në dosje libsgmain.kështu (i cili në fakt është një .jar dhe në të cilin kemi gjetur zbatimin e disa ndërfaqeve të lidhura me enkriptimin pak më lart) ekziston një bibliotekë vendase: libsgmainso-6.4.36.so. Ne e hapim atë në IDA dhe marrim një sërë kuti dialogu me gabime. Problemi është se tabela e kokës së seksionit është e pavlefshme. Kjo bëhet me qëllim për të komplikuar analizën.

Duke kërkuar për dobësi në Shfletuesin UC

Por nuk është e nevojshme: për të ngarkuar saktë një skedar ELF dhe për ta analizuar atë, mjafton një tabelë e titullit të programit. Prandaj, ne thjesht fshijmë tabelën e seksioneve, duke zeruar fushat përkatëse në kokë.

Duke kërkuar për dobësi në Shfletuesin UC

Hapni përsëri skedarin në IDA.

Ka dy mënyra për t'i treguar makinës virtuale Java se ku ndodhet saktësisht në bibliotekën vendase zbatimi i një metode të deklaruar në kodin Java si vendase. E para është t'i jepet një emër specie Java_name_paketë_Klasa_Emri_MethodName.

E dyta është ta regjistroni atë kur ngarkoni bibliotekën (në funksion JNI_OnLoad)
duke përdorur një thirrje funksioni Regjistrohu vendasit.

Në rastin tonë, nëse përdorim metodën e parë, emri duhet të jetë si ky: Java_com_taobao_wireless_security_adapter_JNICLlibrary_doCommandNative.

Nuk ka një funksion të tillë midis funksioneve të eksportuara, që do të thotë se duhet të kërkoni një telefonatë Regjistrohu vendasit.
Le të shkojmë te funksioni JNI_OnLoad dhe ne shohim këtë foto:

Duke kërkuar për dobësi në Shfletuesin UC

Cfare po ndodh ketu? Në pamje të parë, fillimi dhe fundi i funksionit janë tipike për arkitekturën ARM. Udhëzimi i parë në pirg ruan përmbajtjen e regjistrave që funksioni do të përdorë në funksionimin e tij (në këtë rast, R0, R1 dhe R2), si dhe përmbajtjen e regjistrit LR, i cili përmban adresën e kthimit nga funksioni. . Instruksioni i fundit rikthen regjistrat e ruajtur, dhe adresa e kthimit vendoset menjëherë në regjistrin e PC - duke u kthyer kështu nga funksioni. Por nëse shikoni nga afër, do të vini re se instruksioni i parafundit ndryshon adresën e kthimit të ruajtur në rafte. Le të llogarisim se si do të jetë më pas
ekzekutimi i kodit. Një adresë e caktuar 1xB0 ngarkohet në R130, 5 zbritet prej saj, pastaj transferohet në R0 dhe i shtohet 0x10. Rezulton 0xB13B. Kështu, IDA mendon se instruksioni i fundit është një kthim normal i funksionit, por në fakt ai shkon në adresën e llogaritur 0xB13B.

Vlen të kujtojmë këtu se procesorët ARM kanë dy mënyra dhe dy grupe udhëzimesh: ARM dhe Thumb. Pjesa më pak e rëndësishme e adresës i tregon procesorit se cili grup instruksionesh po përdoret. Kjo do të thotë, adresa është në të vërtetë 0xB13A, dhe një në bitin më pak të rëndësishëm tregon modalitetin Thumb.

Një "përshtatës" i ngjashëm është shtuar në fillim të çdo funksioni në këtë bibliotekë dhe
kodi i plehrave. Ne nuk do të ndalemi në to në detaje - ne vetëm kujtojmë
se fillimi i vërtetë i pothuajse të gjitha funksioneve është pak më larg.

Meqenëse kodi nuk kalon në mënyrë eksplicite në 0xB13A, vetë IDA nuk e kuptoi që kodi ndodhej në këtë vendndodhje. Për të njëjtën arsye, ai nuk e njeh shumicën e kodit në bibliotekë si kod, gjë që e bën analizën disi të vështirë. Ne i themi IDA-s se ky është kodi dhe kjo është ajo që ndodh:

Duke kërkuar për dobësi në Shfletuesin UC

Tabela fillon qartë me 0xB144. Çfarë është në sub_494C?

Duke kërkuar për dobësi në Shfletuesin UC

Kur thërrasim këtë funksion në regjistrin LR, marrim adresën e tabelës së përmendur më parë (0xB144). Në R0 - indeksi në këtë tabelë. Kjo do të thotë, vlera merret nga tabela, i shtohet LR dhe rezultati është
adresën për të shkuar. Le të përpiqemi ta llogarisim atë: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Shkojmë në adresën e marrë dhe shohim fjalë për fjalë disa udhëzime të dobishme dhe përsëri shkojmë te 0xB140:

Duke kërkuar për dobësi në Shfletuesin UC

Tani do të ketë një tranzicion në kompensim me indeksin 0x20 nga tabela.

Duke gjykuar nga madhësia e tabelës, do të ketë shumë kalime të tilla në kod. Shtrohet pyetja nëse është e mundur që disi të merret me këtë më automatikisht, pa llogaritur manualisht adresat. Dhe skriptet dhe aftësia për të rregulluar kodin në IDA na vijnë në ndihmë:

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"

Vendosni kursorin në rreshtin 0xB26A, ekzekutoni skriptin dhe shikoni kalimin në 0xB4B0:

Duke kërkuar për dobësi në Shfletuesin UC

IDA përsëri nuk e njohu këtë zonë si kod. Ne e ndihmojmë atë dhe shohim një dizajn tjetër atje:

Duke kërkuar për dobësi në Shfletuesin UC

Udhëzimet pas BLX nuk duket se kanë shumë kuptim, janë më shumë si një lloj zhvendosjeje. Le të shohim sub_4964:

Duke kërkuar për dobësi në Shfletuesin UC

Dhe me të vërtetë, këtu merret një dword në adresën e shtrirë në LR, i shtuar në këtë adresë, pas së cilës vlera në adresën që rezulton merret dhe vendoset në pirg. Gjithashtu, 4 i shtohet LR në mënyrë që pas kthimit nga funksioni, i njëjti kompensim të anashkalohet. Pas së cilës komanda POP {R1} merr vlerën që rezulton nga steka. Nëse shikoni se çfarë ndodhet në adresën 0xB4BA + 0xEA = 0xB5A4, do të shihni diçka të ngjashme me një tabelë adresash:

Duke kërkuar për dobësi në Shfletuesin UC

Për të rregulluar këtë dizajn, do t'ju duhet të merrni dy parametra nga kodi: kompensimin dhe numrin e regjistrit në të cilin dëshironi të vendosni rezultatin. Për çdo regjistër të mundshëm, do t'ju duhet të përgatisni paraprakisht një pjesë të kodit.

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"

Ne vendosim kursorin në fillim të strukturës që duam të zëvendësojmë - 0xB4B2 - dhe ekzekutojmë skriptin:

Duke kërkuar për dobësi në Shfletuesin UC

Përveç strukturave të përmendura tashmë, kodi përmban edhe sa vijon:

Duke kërkuar për dobësi në Shfletuesin UC

Ashtu si në rastin e mëparshëm, pas udhëzimit BLX ka një kompensim:

Duke kërkuar për dobësi në Shfletuesin UC

Ne e marrim kompensimin në adresën nga LR, e shtojmë atë në LR dhe shkojmë atje. 0x72044 + 0xC = 0x72050. Skenari për këtë dizajn është mjaft i thjeshtë:

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"

Rezultati i ekzekutimit të skenarit:

Duke kërkuar për dobësi në Shfletuesin UC

Pasi gjithçka është rregulluar në funksion, ju mund ta drejtoni IDA në fillimin e tij real. Ai do të bashkojë të gjithë kodin e funksionit dhe mund të dekompilohet duke përdorur HexRays.

Dekodimi i vargjeve

Ne kemi mësuar të merremi me turbullimin e kodit të makinës në bibliotekë libsgmainso-6.4.36.so nga Shfletuesi UC dhe mori kodin e funksionit 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;
}

Le të hedhim një vështrim më të afërt në rreshtat e mëposhtëm:

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

Në funksion nën_73E24 emri i klasës po deshifrohet qartë. Si parametra të këtij funksioni, kalohet një tregues për të dhënat e ngjashme me të dhënat e koduara, një buffer i caktuar dhe një numër. Natyrisht, pas thirrjes së funksionit, do të ketë një linjë të deshifruar në tampon, pasi i kalohet funksionit FindClass, e cila merr emrin e klasës si parametër të dytë. Prandaj, numri është madhësia e tamponit ose gjatësia e rreshtit. Le të përpiqemi të deshifrojmë emrin e klasës, ai duhet të na tregojë nëse po shkojmë në drejtimin e duhur. Le të hedhim një vështrim më të afërt se çfarë ndodh në nën_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;
}

Funksion nën_7AF78 krijon një shembull të një kontejneri për vargje bajte të madhësisë së specifikuar (ne nuk do të ndalemi në këto kontejnerë në detaje). Këtu krijohen dy kontejnerë të tillë: njëri përmban linjën "DcO/lcK+h?m3c*q@" (është e lehtë të merret me mend se ky është një çelës), tjetri përmban të dhëna të koduara. Më pas, të dy objektet vendosen në një strukturë të caktuar, e cila i kalohet funksionit nën_6115C. Le të shënojmë edhe një fushë me vlerën 3 në këtë strukturë. Le të shohim se çfarë do të ndodhë më pas me këtë strukturë.

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

Parametri switch është një fushë strukture që më parë i është caktuar vlera 3. Shikoni rastin 3: funksionit nën_6364C Parametrat kalohen nga struktura që janë shtuar aty në funksionin e mëparshëm, d.m.th. çelësi dhe të dhënat e koduara. Nëse shikoni nga afër nën_6364C, ju mund të njihni algoritmin RC4 në të.

Ne kemi një algoritëm dhe një çelës. Le të përpiqemi të deshifrojmë emrin e klasës. Ja çfarë ndodhi: com/taobao/wireless/security/adapter/JNICLlibrary. E madhe! Jemi në rrugën e duhur.

Pema e komandës

Tani duhet të gjejmë një sfidë Regjistrohu vendasit, e cila do të na drejtojë funksionin doCommandNative. Le të shohim funksionet e thirrura nga JNI_OnLoad, dhe ne e gjejmë atë në nën_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;
}

Dhe me të vërtetë, një metodë amtare me emrin është regjistruar këtu doCommandNative. Tani e dimë adresën e tij. Le të shohim se çfarë bën ai.

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

Me emrin mund të merrni me mend se këtu është pika hyrëse e të gjitha funksioneve që zhvilluesit vendosën të transferojnë në bibliotekën vendase. Ne jemi të interesuar për numrin e funksionit 10601.

Nga kodi mund të shihni se numri i komandës prodhon tre numra: komandë/10000, komanda % 10000 / 100 и komanda % 10, d.m.th., në rastin tonë, 1, 6 dhe 1. Këta tre numra, si dhe një tregues për JNIEnv dhe argumentet e kaluara në funksion shtohen në një strukturë dhe kalohen. Duke përdorur tre numrat e fituar (le t'i shënojmë N1, N2 dhe N3), ndërtohet një pemë komande.

Diçka si kjo:

Duke kërkuar për dobësi në Shfletuesin UC

Pema është e mbushur në mënyrë dinamike JNI_OnLoad.
Tre numra kodojnë shtegun në pemë. Çdo fletë e pemës përmban adresën e grumbulluar të funksionit përkatës. Çelësi është në nyjen prind. Gjetja e vendit në kodin ku funksioni që na nevojitet shtohet në pemë nuk është e vështirë nëse i kuptoni të gjitha strukturat e përdorura (ne nuk i përshkruajmë ato në mënyrë që të mos fryjmë një artikull tashmë mjaft të madh).

Më shumë turbullim

Morëm adresën e funksionit që duhet të deshifrojë trafikun: 0x5F1AC. Por është shumë herët për t'u gëzuar: zhvilluesit e UC Browser kanë përgatitur një tjetër surprizë për ne.

Pas marrjes së parametrave nga grupi që u formua në kodin Java, marrim
te funksioni në adresën 0x4D070. Dhe këtu na pret një lloj tjetër errësimi i kodit.

Ne vendosim dy indekse në R7 dhe R4:

Duke kërkuar për dobësi në Shfletuesin UC

Ne e zhvendosim indeksin e parë në R11:

Duke kërkuar për dobësi në Shfletuesin UC

Për të marrë një adresë nga një tabelë, përdorni një indeks:

Duke kërkuar për dobësi në Shfletuesin UC

Pas kalimit në adresën e parë, përdoret indeksi i dytë, i cili është në R4. Janë 230 elementë në tabelë.

Çfarë duhet bërë për këtë? Mund t'i thoni IDA-s se ky është një ndërprerës: Edit -> Tjetër -> Specifikoni idiomën e ndërprerës.

Duke kërkuar për dobësi në Shfletuesin UC

Kodi që rezulton është i tmerrshëm. Por, duke kaluar nëpër xhunglën e saj, mund të vini re një thirrje në një funksion tashmë të njohur për ne nën_6115C:

Duke kërkuar për dobësi në Shfletuesin UC

Kishte një ndërprerës në të cilin në rastin 3 kishte një deshifrim duke përdorur algoritmin RC4. Dhe në këtë rast, struktura e kaluar në funksion plotësohet nga parametrat e kaluar tek doCommandNative. Le të kujtojmë se çfarë kishim atje magjiInt me vlerën 16. Shikojmë rastin përkatës - dhe pas disa kalimeve gjejmë kodin me të cilin mund të identifikohet algoritmi.

Duke kërkuar për dobësi në Shfletuesin UC

Ky është AES!

Algoritmi ekziston, gjithçka që mbetet është të merren parametrat e tij: modaliteti, çelësi dhe, ndoshta, vektori i inicializimit (prania e tij varet nga mënyra e funksionimit të algoritmit AES). Struktura me to duhet të formohet diku përpara thirrjes së funksionit nën_6115C, por kjo pjesë e kodit është veçanërisht e turbullt, kështu që lind ideja për të rregulluar kodin në mënyrë që të gjithë parametrat e funksionit të deshifrimit të hidhen në një skedar.

Patch

Për të mos shkruar manualisht të gjithë kodin patch në gjuhën e asamblesë, mund të hapni Android Studio, të shkruani një funksion atje që merr të njëjtat parametra hyrës si funksioni ynë i deshifrimit dhe shkruan në një skedar, më pas kopjoni-ngjisni kodin që do të përpiluesi gjenerojnë.

Miqtë tanë nga ekipi i Shfletuesit UC u kujdesën gjithashtu për lehtësinë e shtimit të kodit. Le të kujtojmë se në fillim të çdo funksioni kemi kodin e mbeturinave që lehtë mund të zëvendësohet me ndonjë tjetër. Shumë i përshtatshëm 🙂 Megjithatë, në fillim të funksionit të synuar nuk ka hapësirë ​​të mjaftueshme për kodin që ruan të gjithë parametrat në një skedar. Më duhej ta ndaja në pjesë dhe të përdorja blloqe mbeturinash nga funksionet fqinje. Gjithsej ishin katër pjesë.

Pjesa e parë:

Duke kërkuar për dobësi në Shfletuesin UC

Në arkitekturën ARM, katër parametrat e parë të funksionit kalohen përmes regjistrave R0-R3, pjesa tjetër, nëse ka, kalohet nëpër stek. Regjistri LR përmban adresën e kthimit. E gjithë kjo duhet të ruhet në mënyrë që funksioni të funksionojë pasi të hedhim parametrat e tij. Ne gjithashtu duhet të ruajmë të gjithë regjistrat që do të përdorim në proces, kështu që bëjmë PUSH.W {R0-R10,LR}. Në R7 marrim adresën e listës së parametrave që i kalohen funksionit nëpërmjet stackit.

Duke përdorur funksionin hap le të hapim skedarin /data/local/tmp/aes në modalitetin "ab".
dmth për shtim. Në R0 ngarkojmë adresën e emrit të skedarit, në R1 - adresën e rreshtit që tregon modalitetin. Dhe këtu përfundon kodi i mbeturinave, kështu që kalojmë te funksioni tjetër. Në mënyrë që ai të vazhdojë të funksionojë, ne vendosim në fillim kalimin në kodin real të funksionit, duke anashkaluar mbeturinat dhe në vend të mbeturinave shtojmë një vazhdim të patch-it.

Duke kërkuar për dobësi në Shfletuesin UC

duke thirrur hap.

Tre parametrat e parë të funksionit aes kanë lloj int. Meqenëse i ruajtëm regjistrat në stack në fillim, ne thjesht mund ta kalojmë funksionin fwrite adresat e tyre në rafte.

Duke kërkuar për dobësi në Shfletuesin UC

Më pas kemi tre struktura që përmbajnë madhësinë e të dhënave dhe një tregues drejt të dhënave për çelësin, vektorin e inicializimit dhe të dhënat e koduara.

Duke kërkuar për dobësi në Shfletuesin UC

Në fund, mbyllni skedarin, rivendosni regjistrat dhe transferoni kontrollin në funksionin real aes.

Ne mbledhim një APK me një bibliotekë të korrigjuar, e nënshkruajmë atë, e ngarkojmë në pajisje/emulator dhe e nisim. Shohim që po krijohet hale jonë dhe aty po shkruhen shumë të dhëna. Shfletuesi përdor kriptim jo vetëm për trafikun, dhe i gjithë kriptimi kalon përmes funksionit në fjalë. Por për disa arsye të dhënat e nevojshme nuk janë aty dhe kërkesa e kërkuar nuk është e dukshme në trafik. Për të mos pritur derisa UC Browser të vendosë për të bërë kërkesën e nevojshme, le të marrim përgjigjen e koduar nga serveri i marrë më herët dhe të rregullojmë përsëri aplikacionin: shtojmë deshifrimin në onCreate të aktivitetit kryesor.

    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

Ne mbledhim, nënshkruajmë, instalojmë, lëshojmë. Ne marrim një NullPointerException sepse metoda u kthye null.

Gjatë analizës së mëtejshme të kodit, u zbulua një funksion që deshifron linja interesante: "META-INF/" dhe ".RSA". Duket sikur aplikacioni po verifikon certifikatën e tij. Ose edhe gjeneron çelësa prej tij. Nuk dua të merrem me atë që po ndodh me certifikatën, kështu që ne thjesht do t'i japim certifikatën e duhur. Le të rregullojmë linjën e koduar në mënyrë që në vend të "META-INF/" të marrim "BLABLINF/", të krijojmë një dosje me atë emër në APK dhe të shtojmë certifikatën e shfletuesit të ketrit.

Ne mbledhim, nënshkruajmë, instalojmë, lëshojmë. Bingo! Ne kemi çelësin!

MitM

Ne morëm një çelës dhe një vektor inicializimi të barabartë me çelësin. Le të përpiqemi të deshifrojmë përgjigjen e serverit në modalitetin CBC.

Duke kërkuar për dobësi në Shfletuesin UC

Ne shohim URL-në e arkivit, diçka të ngjashme me MD5, "extract_unzipsize" dhe një numër. Ne kontrollojmë: MD5 e arkivit është e njëjtë, madhësia e bibliotekës së papaketuar është e njëjtë. Ne po përpiqemi ta korrigjojmë këtë bibliotekë dhe t'ia japim shfletuesit. Për të treguar se biblioteka jonë e korrigjuar është ngarkuar, ne do të hapim një qëllim për të krijuar një SMS me tekstin "PWNED!" Ne do të zëvendësojmë dy përgjigje nga serveri: puds.ucweb.com/upgrade/index.xhtml dhe për të shkarkuar arkivin. Në të parën zëvendësojmë MD5 (madhësia nuk ndryshon pas shpaketimit), në të dytën japim arkivin me bibliotekën e patched.

Shfletuesi përpiqet të shkarkojë arkivin disa herë, pas së cilës jep një gabim. Me sa duket diçka
ai nuk e pelqen. Si rezultat i analizimit të këtij formati të turbullt, doli që serveri transmeton gjithashtu madhësinë e arkivit:

Duke kërkuar për dobësi në Shfletuesin UC

Është i koduar në LEB128. Pas patch-it, madhësia e arkivit me bibliotekën ndryshoi pak, kështu që shfletuesi konsideroi se arkivi ishte shkarkuar në mënyrë të shtrembër dhe pas disa përpjekjeve hodhi një gabim.

Rregullojmë madhësinë e arkivit... Dhe – fitore! 🙂 Rezultati është në video.

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

Pasojat dhe reagimi i zhvilluesit

Në të njëjtën mënyrë, hakerët mund të përdorin veçorinë e pasigurt të Shfletuesit UC për të shpërndarë dhe drejtuar biblioteka me qëllim të keq. Këto biblioteka do të funksionojnë në kontekstin e shfletuesit, kështu që ata do të marrin të gjitha lejet e sistemit të tij. Si rezultat, aftësia për të shfaqur dritaret e phishing, si dhe aksesi në skedarët e punës të ketrit portokalli kinez, duke përfshirë hyrjet, fjalëkalimet dhe cookies të ruajtura në bazën e të dhënave.

Ne kontaktuam zhvilluesit e UC Browser dhe i informuam për problemin që gjetëm, u përpoqëm të tregonim cenueshmërinë dhe rrezikun e tij, por ata nuk diskutuan asgjë me ne. Ndërkohë, shfletuesi vazhdoi të shfaqte në pamje të qartë veçorinë e tij të rrezikshme. Por sapo zbuluam detajet e cenueshmërisë, nuk ishte më e mundur ta injoronim atë si më parë. 27 marsi ishte
u lëshua një version i ri i Shfletuesit UC 12.10.9.1193, i cili hyri në server përmes HTTPS: puds.ucweb.com/upgrade/index.xhtml.

Për më tepër, pas "rregullimit" dhe deri në kohën e shkrimit të këtij artikulli, përpjekja për të hapur një PDF në një shfletues rezultoi në një mesazh gabimi me tekstin "Oops, diçka shkoi keq!" Një kërkesë për serverin nuk u bë kur përpiqej të hapte një PDF, por një kërkesë u bë kur shfletuesi u hap, gjë që lë të kuptohet për aftësinë e vazhdueshme për të shkarkuar kodin e ekzekutueshëm në kundërshtim me rregullat e Google Play.

Burimi: www.habr.com

Shto një koment