
Hyrje
Në fund të marsit ne , që zbuluam një mundësi të fshehtë për të ngarkuar dhe ekzekutuar kod të paushtruar në UC Browser. Sot do të shohim hollësisht se si ndodh ky ngarkim dhe si mund ta përdorin hackerët për qëllimet e tyre.
Disa kohë më parë, UC Browser reklamohej dhe shpërndahej shumë agresivisht: instalohej në pajisjet e përdoruesve përmes programeve të dëmshme, shpërndahej nga skedarë të ndryshëm nën maskën e skedarëve video (dmth. përdoruesit mendonin se po shkarkonin, për shembull, një seksion pornografik, dhe në vend të tij merrnin një APK me këtë shfletues), përdoren pankarta frikësuese me mesazhe se shfletuesi është i vjetruar, i dobët dhe kështu me radhë. Në grupin zyrtar të UC Browser në VK ka , ku përdoruesit mund të ankohen për reklamimin e papërshtatshëm, aty ka shumë shembuj. Në vitin 2016 ishte madje në gjuhën ruse (po, reklamë për një shfletues që bllokon reklamën).
Në momentin e shkruajtjes së këtij artikulli, UC Browser kishte më shumë se 500,000,000 instalime në Google Play. Kjo është e impresionueshme — vetëm Google Chrome ka më shumë. Mes komenteve mund të shohim mjaft ankesave për reklamat dhe redirektimet në disa aplikacione në Google Play. Kjo është bërë arsyeja për hulumtimin: ne vendosëm të shohim nëse UC Browser po bënte ndonjë gjë të keqe. Dhe rezultoi se po bënte!
Në kodin e aplikacionit u zbulua mundësia për të ngarkuar dhe ekzekutuar kod ekzekutiv, në Google Play. Përveç faktit se UC Browser ngarkon kod ekzekutiv, ai e bën këtë në mënyrë të pasigurt, gjë që mund të përdoret për të realizuar një sulm MitM. Le të shohim nëse mund ta kryejmë një sulm të tillë.
Të gjitha që janë shkruar më poshtë janë aktuale për versionin e UC Browser që ishte në Google Play në momentin e hulumtimit:
package: com.UCMobile.intl
versionName: 12.10.8.1172
versionCode: 10598
sha1 APK-fajllit: f5edb2243413c777172f6362876041eb0c3a928cVektori i sulmit
Në manifestin e UC Browser mund të zbuloni një shërbim me një emër të thjeshtë com.uc.deployment.UpgradeDeployService.
<service android_exported="false" android_name="com.uc.deployment.UpgradeDeployService" android_process=":deploy" />
Kur aktivizohet ky shërbim, shfletuesi realizon një kërkesë POST në , i cili mund të vërehet në trafik pas një kohe pas fillimit. Si përgjigje, ai mund të marrë një urdhër për të shkarkuar ndonjë përditësim ose modul të ri. Në procesin e analizes, serveri nuk dha të tilla komanda, por vëmendëm se, kur përpiqeshim të hapnim një PDF në shfletues, ai bënte një kërkesë të përsëritur në adresën e mësipërme, pas së cilës shkarkonte një bibliotekë natyrale. Për të kryer sulmin, vendosëm të përdorim këtë veçori të UC Browser: aftësia për të hapur PDF me ndihmën e një biblioteke natyrale, e cila nuk ndodhet në APK dhe që e shkarkon nga interneti kur është e nevojshme. Duhet të theksohet se, teorikisht, UC Browser mund të detyrohet të shkarkojë diçka pa ndërveprimin e përdoruesit - nëse jepet një përgjigje e formuar siç duhet për një kërkesë që ekzekutohet pas fillimit të shfletuesit. Por për këtë, duhet të studiojmë më në detaje protokollin e ndërveprimit me serverin, prandaj vendosëm se ishte më e lehtë të redaktojmë përgjigjen e kapur dhe të zëvendësojmë bibliotekën për punën me PDF.
Pra, kur përdoruesi dëshiron të hapë PDF-në direkt në shfletues, në trafik mund të shihen kërkesat e mëposhtme:

Fillimisht ka një kërkesë POST ndaj , pas së cilës
shkarkohet një arkiv me bibliotekën për shikimin e PDF-ve dhe formateve zyrtare. Është logjikë të supozojmë se në kërkesën e parë dërgohet informacion rreth sistemit (të paktën, arkitektura, që të jepet biblioteka e duhur), ndërsa si përgjigje, shfletuesi merr informacione rreth bibliotekës që duhet të shkarkohet: adresa dhe, ndoshta, diçka tjetër. Problemi është se kjo kërkesë është e enkriptuar.
Fragmenti i kërkesës
Fragmenti i përgjigjes


Biblioteka vetë është e paketuar në ZIP dhe nuk është e enkriptuar.

Kërkimi i kodit për dekryptimin e trafikut
Do të përpiqemi të dekryptonim përgjigjen e serverit. Shikojmë kodin e klasës com.uc.deployment.UpgradeDeployService: nga metoda onStartCommand kalojmë në com.uc.deployment.b.x, dhe nga ai në com.uc.browser.core.d.c.f.e:
publike 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);
}Këtu shohim formimin e një kërkese POST. Vëmendje i kushtohet krijimit të një vargu prej 16 bajtësh dhe mbushjes së tij: 0x5F, 0, 0x1F, -50 (=0xCE). Përputhet me atë që pamë në kërkesën më sipër.
Në këtë klasë, gjithashtu, mund të vëreni një klasë të përfshirë, e cila 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 si input një varg bajtësh dhe kontrollon që bajti i parë të jetë 0x60 ose bajti i tretë të jetë 0xD0, ndërsa bajti i dytë të jetë 1, 11 ose 0x1F. Shikojmë përgjigjen nga serveri: bajti i parë është 0x60, i dyti është 0x1F, i treti është 0x60. Duket se është ajo që na nevojitet. Gjithashtu, sipas rreshtave (“up_decrypt”, për shembull), këtu duhet të thërritet një metodë që dekrypton përgjigjen e serverit.
Kalohet te metoda g.j. Vërejmë se si argumenti i parë i jepet një bajt në pozitën 2 (domethënë 0x1F në rastin tonë) dhe si argumenti i dytë - përgjigja e serverit pa
16 bajtët e parë.
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;
} Këtu ndodhi qartazi zgjedhja e algoritmit të dekryptimit, dhe ai bajt që kemi në rastin tonë
në raste është 0x1F, që tregon një nga tri mundësitë.
Vazhdojmë analizën e kodit. Pas disa skakjeve arrijmë në një metodë me emrin e qartë decryptBytesByKey.
Këtu ndahen edhe dy bajta nga përgjigja jonë, dhe nga ata krijohet një varg. Është e qartë që në këtë mënyrë zgjidhet çelësi për dekriptimin 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 bajta
System.arraycopy(bytes, 0, prefix, 0, prefix.length);
String keyId = c.ayR().d(ByteBuffer.wrap(prefix).getShort()); // Zgjedhja e çelësit
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 e parë përpara, vërejmë se në këtë fazë nuk merret ende çelësi, por vetëm "identifikuesi" i tij. Marrja e çelësit është pak më e komplikuar.
Në metodën tjetër, dy parametra të rinj i shtohen atyre ekzistues, dhe bëhen katër: numri magjik 16, identifikuesi i çelësit, të dhënat e koduara dhe një varg i panjohur (në rastin tonë, bosh).
public final byte[] l(String keyId, byte[] encrypted) throws SecException {
return this.ayJ().staticBinarySafeDecryptNoB64(16, keyId, encrypted, "");
}Pas një serie kalimesh arrijmë në metodën staticBinarySafeDecryptNoB64 ndërfaqja com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. Në kodin kryesor të aplikacionit nuk ka klasa që zbatojnë këtë ndërfaqe. Një klasë e tillë është në skedarin lib/armeabi-v7a/libsgmain.so, e cila në të vërtetë nuk është .so, por .jar. Metoda që na intereson është implementuar si më poshtë:
paket com.alibaba.wireless.security.a.i;
// ...
publik klasa a zbaton 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);
}
//...
} Këtu lista jonë e parameterëve kompletohet me dy numra të tjerë të plotë: 2 dhe 0. Duke gjykuar nga
gjithçka, 2 përfaqëson dekriptimin, si në metodën doFinal klasës sistemore javax.crypto.Cipher. Dhe gjithçka kalon në një Router me numrin 10601 — kjo, duket, është numri i komandës.
Pas një vargu kalimesh gjejmë klasën që implementon ndërfaqen IRouterComponent dhe metodën doCommand:
paket com.alibaba.wireless.security.mainplugin;
import com.alibaba.wireless.security.framework.IRouterComponent;
import com.taobao.wireless.security.adapter.JNICLibrary;
publik klasa a implements IRouterComponent {
public a() {
super();
}
public Object doCommand(int arg2, Object[] arg3) {
return JNICLibrary.doCommandNative(arg2, arg3);
}
}Po ashtu klasa JNICLibrary, në të cilën është shpallur një metodë native doCommandNative:
paket com.taobao.wireless.security.adapter;
publik klasa JNICLibrary {
public static native Object doCommandNative(int arg0, Object[] arg1);
}Kështu, na nevojitet të gjejmë metodën në kodin native doCommandNative. Dhe këtu fillon argëtimi.
Obfuskimi i kodit të makinës
Në skedar libsgmain.so (i cili në të vërtetë është .jar dhe në të cilin përmendëm më lart implementimin e disa ndërfaqeve të lidhura me enkriptimin) ka një bibliotekë native: libsgmainso-6.4.36.so. E hapim atë në IDA dhe marrim një sërë dritareve dialogu me gabime. Problemi është se tabela e seksioneve (section header table) – është e pavlefshme. Kjo është bërë qëllimisht, për të vështirësuar analizën.

Por ajo nuk është e nevojshme: për të ngarkuar korrektësisht një skedhë ELF dhe për ta analizuar atë, mjafton tabela e segmenteve (program header table). Prandaj, thjesht e heqim tabelën e seksioneve, duke zerojë përkatësisht fushat në titull.

Sërish hapim skedhën në IDA.
Ka dy mënyra për të treguar makinës virtuale Java, se ku ndodhet implementimi i metodës, e shpallur në kodin Java si native. E para — t'i japësh një emër si Java_emri_paketës_EmriKlases_emriMetodës.
E dyta — ta regjistrohet gjatë ngarkimit të bibliotekës (në funksionin JNI_OnLoad)
me thirrjen e funksionit RegisterNatives.
Në rastin tonë, nëse përdorim mënyrën e parë, emri duhet të jetë si më poshtë: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.
Përmes funksioneve të eksportuara nuk ka një të tillë, kështu që duhen kërkuar thirrje RegisterNatives.
Shkëmbejmë në funksionin JNI_OnLoad dhe shohim këtë pamje:

Çfarë po ndodh këtu? Në shikim të parë, fillimi dhe fundi i funksionit janë tipike për arkitekturën ARM. Instruksioni i parë ruan në stack përmbajtjen e regjistrave që funksioni do të përdorë në punën e tij (në këtë rast R0, R1 dhe R2), si dhe përmbajtjen e regjistrit LR, ku ndodhet adresa e rikthimit nga funksioni. Instruksioni i fundit rikthen regjistrat e ruajtur, dhe adresa e rikthimit vendoset menjëherë në regjistrin PC — kështu ndodh rikthimi nga funksioni. Por nëse shikoni më me kujdes, mund të vëreni se instruksioni përpara fundit ndryshon adresën e rikthimit të ruajtur në stack. Le të llogarisim se cila do të jetë ajo pas
ekzekutimit të kodit. Në R1 ngarkohet një adresë 0xB130, nga e cila përjashtohet 5, më pas ai kalon në R0 dhe i shtohet 0x10. Kështu, rezultati është 0xB13B. Kështu, IDA mendon se në instrukcionin e fundit ndodh një rikthim normal nga funksioni, ndërsa në të vërtetë ndodh një kalim në adresën e llogaritur 0xB13B.
Këtu është e rëndësishme të përmendim se procesorët ARM kanë dy mënyra dhe dy grupe instrukcionesh: ARM dhe Thumb. Biti më i vogël i adresës i thotë procesorit se cili grup instrukcionesh po përdoret. Pra, adresa në të vërtetë është 0xB13A, dhe njësi në bitin më të vogël tregon modalitetin Thumb.
Një kalim i tillë është shtuar në fillim të secilit funksion në këtë bibliotekë dhe
kod i mbetur. Më pas nuk do të ndalemi shumë në to — thjesht mbajmë mend
se fillimi i vërtetë i pothuajse të gjitha funksioneve ndodhet pak më tutje.
Sepse në kod nuk ka një kalim të qartë në 0xB13A, IDA vetë nuk e ka njohur se në këtë vend ndodhet kodi. Për këtë arsye, shumicën e kodit në bibliotekë, ajo nuk e njeh si kod, gjë që paksa vështirëson analizën. I themi IDA-s se këtu është kod, dhe ja çfarë ndodh:

Në 0xB144 qartë fillon tabela. Çfarë ndodhet në sub_494C?

Kur thirret ky funksion, në regjistrin LR do të kemi adresën e tabelës së përmendur më parë (0xB144). Në R0 — indeksi në këtë tabelë. Pra, merret vlera nga tabela, i shtohet LR dhe rezultati është
adresa në të cilën duhet të kalojmë. Le të përpiqemi ta llogarisim: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Kalojmë në adresën e marrë dhe shohim përveç disa udhëzimeve të dobishme përsëri kalim në 0xB140:

Tani do të kalojmë në offset me indeks 0x20 nga tabela.
Duke iu referuar madhësisë së tabelës, do të ketë shumë kalime të tilla në kod. Lind pyetja, a mund të luftojmë ndonjëherë me këtë më automatikisht, pa llogaritje manuale të adresave. Dhe në ndihmë na vijnë skriptet dhe mundësia për të patchuar kodin në 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 "indeksi =", 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 "Tipi i gabuar i operandit në", hex(ea1), "-", get_operand_type(ea1, 0), get_operand_type(ea1, 1)
table = None
if table is None:
print "Nuk arritëm të gjejmë tabelën"
else:
print "tabela =", hex(table)
offset = get_wide_dword(table + (index << 2))
put_unconditional_branch(ea, table + offset)
else:
print "Kodi i panjohur", get_operand_type(ea1, 0), get_operand_value(ea1, 0), get_operand_type(ea1, 1) == 2
else:
print "Nuk arritëm të zbulonim instruksionin e parë"Vë kurorin në rreshtin 0xB26A, aktivizoj skriptin dhe shoh kalimin në 0xB4B0:

IDA sërish nuk e identifikoi këtë pjesë si kod. E ndihmojmë dhe shohim atje një strukturë tjetër:

Instruksionet pas BLX duken paksa të paqartë, kjo duket më shumë si një offset. Po shohim në sub_4964:

Dhe në të vërtetë, këtu merret dword nga adresa që ndodhet në LR, shtohet këtë adresë, pas së cilës merret vlera nga adresa e marrë dhe vendoset në stek. Po ashtu, në LR shtohet 4, për të kaluar këtë offset pas rikthimit nga funksioni. Pas saj, komanda POP {R1} nxjerr vlerën e marrë nga stek. Nëse shohim se çfarë ndodhet në adresën 0xB4BA + 0xEA = 0xB5A4, mund të shohim diçka të ngjashme me një tabelë adresash:

Për të bërë patch këtë konstruksion, nevojiten dy parametra nga kodi: zhvendosja dhe numri i regjistrit, ku duhet të vendoset rezultati. Për çdo regjistër të mundshëm, do të jetë e nevojshme të përgatitet paraprakisht një pjesë kodi.
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 "Instruksioni POP nuk u gjet"
else:
print "Lloji i operandit është i gabuar në +4:", get_operand_type(ea + 4, 0)
else:
print "Nuk është e mundur të identifikohet instruksioni i parë"Vendosim kursorin në fillimin e konstruksionit që dëshirojmë të zëvendësojmë — 0xB4B2 — dhe fillojmë skriptin:

Përveç konstruksioneve tashmë të përmendura, në kod kemi edhe këto:

Ashtu si në rastin e mëparshëm, pas instruksionit BLX vjen zhvendosja:

Kemi zhvendosjen nga adresa nga LR, e shtojmë atë në LR dhe kalojmë atje. 0x72044 + 0xC = 0x72050. Skripti për këtë konstruksion është shumë i thjeshtë:
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 "Nuk është e mundur të identifikohet instrukcioni i parë"Rezultati i ekzekutimit të skriptit:

Pasi e gjitha funsioni është riparuar, mund t'i tregojmë IDA fillimin e saj të vërtetë. Ajo do të mbledhë të gjithë kodin e funksionit në pjesë dhe ai mund të dekompilohet me HexRays.
Dekriptimi i vargjeve
Mësuam të luftojmë me obfuscimin e kodit makinerik në bibliotekën libsgmainso-6.4.36.so e UC Browser dhe morëm 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ë shikojmë më me kujdes rreshtat e mëposhtëm:
sub_73E24(&unk_83EA6, &v6, 49);
clazz = (jclass)((int (__fastcall *)(JNIEnv *, int *))(*env)->FindClass)(env, &v6);Në funksionin sub_73E24 pa dyshim ndodh dekriptimi i emrit të klasës. Si parametra të këtij funksioni kalon një tregues në të dhëna, të ngjashme me të enkriptuara, një tampon dhe një numër. Është e qartë se pas thirrjes së funksionit, tamponi do të ketë rreshtin e dekriptuar, pasi ai i kalon funksionit FindClass, e cila merr si parametër të dytë emrin e klasës. Pra, numri është madhësia e tamponit ose gjatësia e rreshtit. Të përpiqemi të dekriptojmë emrin e klasës, ai duhet të na tregojë nëse po shkojmë në drejtimin e duhur. Le të shqyrtojmë me kujdes çfarë ndodh në 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;
}Funksioni sub_7AF78 krijon një instancë të një kontejneri për arrays të bajtave me përmasat e caktuara (nuk do të ndalemi shumë te këto konteinerë). Këtu krijohen dy të tillë: njëri mban një varg "DcO/lcK+h?m3c*q@" (nuk është e vështirë të kuptohet se ky është çelësi), tjetri mbajti të dhëna të enkriptuara. Më pas, të dy objektet vendosen në një strukturë të caktuar, që i kalon funksionit sub_6115C. Gjithashtu, vërejnë në këtë strukturë një fushë me vlerën 3. Le të shohim se çfarë ndodhi me këtë strukturë më pas.
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;
}Si parametri switch kalon një fushë strukture, të cilës më parë i është caktuar vlera 3. Shikojmë case 3: në funksion sub_6364C kalohen parametrat nga struktura, të cilat janë vendosur në funksionin e mëparshëm, dmth çelësi dhe të dhënat e enkriptuara. Nëse e shqyrtojmë me kujdes sub_6364C, mund të zbulojmë tek algoritmi RC4.
Kemi algoritmin dhe çelësin. Të provojmë të dekodojmë emrin e klasës. Ja çfarë morëm: com/taobao/wireless/security/adapter/JNICLibrary. Shkëlqyeshëm! Jemi në rrugën e duhur.
Pema e komandave
Tani duhet të gjejmë thirrjen RegisterNatives, e cila do të na tregojë për funksionin doCommandNative. Shikojmë funksionet që thirren nga JNI_OnLoad, dhe e gjejmë atë në 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;
}Dhe në të vërtetë, këtu regjistrohet një metodë natyrore me emrin doCommandNative. Tani e dimë adresën e saj. Shikojmë se çfarë bën ajo.
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;
}Nga emri mund të dedukohet se këtu është pika e hyrjes për të gjitha funksionet që zhvilluesit vendosën të transferonin në bibliotekën natyrore. Na intereson funksioni me numër 10601.
Nga kodi mund të shohim se nga numri i komandës gjenden tre numra: command / 10000, command % 10000 / 100 dhe command % 10, dmth, në rastin tonë, 1, 6 dhe 1. Këta tre numra, së bashku me treguesin në JNIEnv dhe argumentet e kaluara në funksion, bashkohen në një strukturë dhe kalohen më tej. Me ndihmën e këtyre tre numrave (të quajtur N1, N2 dhe N3) ndërtohet pemë komandash.
Për një pamje të tillë:

Pema plotësohet dinamikisht në JNI_OnLoad.
Tre numra kodifikojnë rrugën në pemë. Çdo gjethe e pemës përmban adresën e koduar të funksionit përkatës. Çelësi ndodhet në nyjën prind. Të gjejmë vendin në kod ku funksioni që na nevojitet shtohet në pemë, nuk është shumë e vështirë nëse kuptojmë të gjitha strukturat e përdorura (përshkrimi i tyre nuk jepet për të mos e tepruar me këtë artikull të konsiderueshëm).
Akoma obfuskimi
Kemi marrë adresën e funksionit që duhet të dekriptojë trafikun: 0x5F1AC. Por ende nuk duhet të gëzohemi: zhvilluesit e UC Browser na kanë përgatitur një surprizë tjetër.
Pas marrjes së parametrave nga массив, i cili u formua në kodin Java, arrijmë
në funksionin në adresën 0x4D070. Dhe këtu na pret një tjetër lloj obfuskimi të kodit.
Vendosim në R7 dhe R4 dy indekse:

Transferojmë indeksin e parë në R11:

Për të marrë adresën nga tabela, përdorim indeksin:

Pas kalimit në adresën e parë përdoret indeksi i dytë, që është në R4. Në tabelë ka 230 elementë.
Çfarë të bëjmë me këtë? Mund të themi IDA se është një switch: Edit -> Other -> Specify switch idiom.

Kodi i marrë është i frikshëm. Por, duke kaluar përmes gjëra e tij, mund të vërejmë thirrjen e njohur të funksionit tonë sub_6115C:

Aty kishte një switch, ku në case 3 ndodhej dekripti me përdorimin e algoritmit RC4. Dhe në këtë rast, struktura që i kalon në funksion plotësohet nga parametrat e kaluara në doCommandNative. Kujtojmë se kemi pasur magicInt me vlerën 16. Shikojmë case përkatës - dhe pas disa kalimeve gjejmë kodin, sipas të cilit mund të identifikohet algoritmi.

Ky është AES!
Algoritmi është, mbetet të marrim parametrat e tij: mënyrën, çelësin dhe, ndoshta, vektorët e inicializimit (prania e tij varet nga mënyra e funksionimit të algoritmit AES). Struktura me ta duhet të formohet diku para thirrjes së funksionit sub_6115C, por kjo pjesë e kodit është obfuskatuar veçanërisht mirë, kështu që lind ideja për të patch-uar kodin, që të gjitha parametrat e funksionit të dekriptimeve të dump-ohen në skedarin.
Patch
Për të mos shkruar të gjithë kodin e patch-it në gjuhën assembler manualisht, mund të nisni Android Studio, të shkruani atje një funksion që merr si hyrje parametrat e njëjtë si funksioni ynë i dekriptimeve, dhe shkruan në skedar, pasi të kopjoni kodin që do të gjenerojë kompajleri.
Rreth lehtësisë së shtimit të kodit, shokët tanë nga ekipi UC Browser gjithashtu janë "kujdesur". Kujtojmë se në fillim të çdo funksioni kemi kod të pavlerë, që lehtë mund të zëvendësohet me ndonjë tjetër. Shumë e përshtatshme 🙂 Megjithatë, në fillim të funksionit të synuar, hapësira për kodin që ruan të gjithë parametrat në skedarin, është e vogël. Duhet të ndajmë atë në pjesë dhe të përdorim blloqet e pavlera të funksioneve fqinj. Në total, kemi katër pjesë.
Pjesa e parë:

Në arkitekturën ARM, katër parametrat e parë të funksionit kalohen përmes regjistrave R0-R3, të tjerët, nëse ka, kalohen përmes stek. Adresa e rikthimit kalon në regjistrin LR. Të gjitha këto duhet të ruhet që funksioni të mund të ekzekutohet pasi ne të nxjerrim parametrat e tij. Gjithashtu, duhet të ruajmë të gjithë regjistrat që do të përdorim gjatë procesit, prandaj bëjmë PUSH.W {R0-R10,LR}. Në R7, ne kemi adresën e listës së parametrave të kaluara në funksion përmes stek.
Me funksionin fopen do të hapim skedarin /data/local/tmp/aes në modalitetin "ab",
d.m.th. për shtimin. Në R0 ngarkohet adresa e emrit të skedarit, në R1 - adresa e vargut me specifikimin e modalitetit. Dhe këtu përfundon kodi i pavlerë, kështu që kalojmë në funksionin tjetër. Që ai të vazhdojë të punojë, vendosim në fillim kalimin në kodin e vërtetë të funksionit duke anashkaluar mbeturinat, dhe në vend të mbetjeve shtojmë vazhdimin e patch-it.

Thërrasim fopen.
Të parët tre parametrat e funksionit aes kanë tipin int. Pasi ne në fillim ruajmë regjistrat në stek, mund ta kalojmë thjesht adresat e fwrite tyre në stek.

Pastaj kemi tre struktura që përmbajnë Madhësinë e të dhënave dhe një pointer në të dhëna për çelësin, vektorin e inicializimit dhe të dhënat e enkriptuara.

Në fund, mbyllim skedarin, rikthejmë regjistrat dhe i japim kontrollin funksionit të vërtetë aes.
Kemi mbledhur APK me bibliotekën e modifikuar, e kemi nënshkruar, e kemi vendosur në pajisjen/emulatorin, e hapim. Shohim se dump-i ynë krijohet, dhe shumë të dhëna shkruhen atje. Shfletuesi përdor enkriptimin jo vetëm për trafikun, dhe e gjithë enkriptimi kalon përmes funksionit të shqyrtuar. Sidoqoftë, të dhënat e nevojshme mungojnë për një arsye, dhe në trafik nuk shihet kërkesa e nevojshme. Që të mos presim derisa UC Browser të bëjë kërkesën e nevojshme, do të marrim përgjigjen e enkriptuar nga serveri, e marrë më parë, dhe do ta modifikojmë aplikacionin përsëri: do të shtojmë dekodimin 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;)IGrupojmë, nënshkruajmë, instalojmë, hapim. Marrim NullPointerException, pasi metoda ktheu null.
Gjatë analizës më të thellë të kodit, u zbulua një funksion që dekripton disa rreshta interesante: «META-INF/» dhe «.RSA». Duke dukur, aplikacioni kontrollon certifikatën e tij. Ose madje gjeneron çelësa nga ajo. Nuk do të duhej të merremi me atë që ndodh me certifikatën, prandaj thjesht do t'i ofrojmë certifikatën e duhur. Do ta riparojmë rreshtin e enkriptuar në një mënyrë që në vend të «META-INF/» do të kemi «BLABLINF/», do të krijojmë një dosje me këtë emër në APK dhe do t’i hedhim atje certifikatën e belkobrupsit.
Grupojmë, nënshkruajmë, instalojmë, hapim. Bingo! Çelësi është te ne!
MitM
Mori çelësin dhe vektorin e inicializimit, të barabartë me çelësin. Do të provojmë të dekriptojmë përgjigjen e serverit në modalitetin CBC.

Shikojmë URL-në e arkivës, diçka që duket si MD5, «extract_unzipsize» dhe një numër. Kontrollojmë: MD5 e arkivës është e njëjtë, madhësia e bibliotekës pas dekompresimit është e njëjtë. Do të provojmë ta riparojmë këtë bibliotekë dhe t'ia japim shfletuesit. Për të treguar se biblioteka jonë e riparuar u ngarkua, do të fillojmë një qëllim për të krijuar një SMS me tekstin «PWNED!». Do të zëvendësojmë dy përgjigje nga serveri: dhe për shkarkimin e arkivës. Në të parën zëvendësojmë MD5 (madhësia pas dekompresimit nuk ndryshon), në të dytën japim arkivën me bibliotekën e riparuar.
Shfletuesi përpiqet disa herë të shkarkojë arkivën, pas së cilës jep një gabim. Me sa duket, diçka
nuk i pëlqen. Si rezultat i analizës së këtij formati të paqartë, u zbulua se serveri gjithashtu dërgon madhësinë e arkivës:

Ajo është e koduar në LEB128. Pas riparimit, madhësia e arkivës me bibliotekën ka ndryshuar pak, prandaj shfletuesi mendoi se arkiva u shkarkua keq, dhe pas disa përpjekjeve dha një gabim.
Korrigjojmë madhësinë e arkivës... Dhe – fitore! 🙂 Rezultati në video.
Pasojat dhe reagimi i zhvilluesit
Në të njëjtën mënyrë hakerët mund të shfrytëzojnë funksionin e pasigurt UC Browser për të shpërndarë dhe ekzekutuar biblioteka të dëmshme. Këto biblioteka do të funksionojnë në kontekstin e shfletuesit, prandaj do të marrin të gjitha lejet e tij sistemike. Si pasojë, do të kenë mundësinë të shfaqin dritare phishing, si dhe qasje në skedarët e punës së një shqiponje të portokalltë kineze, duke përfshirë emrat e përdoruesve, fjalëkalimet dhe cookies të ruajtura në databaza.
Ne kemi kontaktuar zhvilluesit e UC Browser dhe u kemi raportuar për problemin e gjetur, përpiqumë t'u tregojmë për dobësinë dhe rrezikun e saj, por ata nuk pranuan të diskutojnë asgjë me ne. Ndërkohë, shfletuesi vazhdoi të shfaqë funksionin e rrezikshëm për të gjithë. Por sapo ne zbuluam detajet e dobësisë, nuk ishte më e mundur ta injoronim atë si më parë. Më 27 mars u
lëshua një version i ri UC Browser 12.10.9.1193, i cili i drejtohej serverit përmes HTTPS: .
Përveç kësaj, pas "korrigjimit" dhe deri në momentin e hartimit të artikullit, përpjekja për të hapur një PDF në shfletues çonte në shfaqjen e një mesazhi gabimi me tekstin "Ups, diçka nuk shkoi siç duhet!". Kërkesa për serverin gjatë përpjekjes për të hapur PDF-në nuk u ekzekutua, por kërkesa u ekzekutua kur nisi shfletuesin, çka sugjeron se mbeti mundësia për të ngarkuar kod ekzekutiv në shkelje të rregullave të Google Play.
Burimi: habr.com
