UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

تعارف

مارچ جي آخر ۾ اسان رپورٽ ڪيو، ته انهن يو سي برائوزر ۾ اڻ تصديق ٿيل ڪوڊ لوڊ ڪرڻ ۽ هلائڻ جي پوشیدہ صلاحيت دريافت ڪئي. اڄ اسان تفصيل سان ڏسنداسين ته هي ڊائون لوڊ ڪيئن ٿئي ٿو ۽ هيڪرز ان کي پنهنجي مقصدن لاءِ ڪيئن استعمال ڪري سگهن ٿا.

ڪجهه عرصو اڳ، يو سي برائوزر کي اشتهار ڏنو ويو ۽ تمام جارحتي طور تي ورهايو ويو: اهو مالويئر استعمال ڪندي صارفين جي ڊوائيسز تي نصب ڪيو ويو، مختلف سائيٽن تان وڊيو فائلن جي آڙ ۾ ورهايو ويو (يعني، صارفين سوچيو ته اهي ڊائون لوڊ ڪري رهيا آهن، مثال طور، هڪ فحش وڊيو، پر بجاءِ هن برائوزر سان هڪ APK وصول ڪيو)، پيغامن سان گڏ خوفناڪ بينر استعمال ڪيا ته برائوزر پراڻو، ڪمزور، ۽ اهڙيون شيون آهن. VK تي سرڪاري يو سي برائوزر گروپ ۾ موجود آھي موضوع، جنهن ۾ صارفين غير منصفانه اشتهارن جي باري ۾ شڪايت ڪري سگهن ٿا، اتي ڪيترائي مثال آهن. 2016 ۾ اتي به هو وڊيو اشتهارن روسي ۾ (ها، اشتهارن کي بلاڪ ڪرڻ واري برائوزر لاءِ اشتهار).

لکڻ جي وقت تي، يو سي برائوزر گوگل پلي تي 500 تنصيب کان مٿي آهن. هي شاندار آهي - صرف گوگل ڪروم وڌيڪ آهي. تبصرن مان توھان ڏسي سگھو ٿا ڪافي شڪايتون اشتهارن جي باري ۾ ۽ گوگل پلي تي ڪجھ ايپليڪيشنن ڏانھن ريڊائريڪٽس. اهو اسان جي تحقيق جو سبب هو: اسان اهو ڏسڻ جو فيصلو ڪيو ته ڇا يو سي برائوزر ڪجهه خراب ڪري رهيو هو. ۽ اهو ظاهر ٿيو ته هو ڪندو آهي!

ايپليڪيشن ڪوڊ ۾، قابل عمل ڪوڊ ڊائون لوڊ ۽ هلائڻ جي صلاحيت دريافت ڪئي وئي، جيڪو ايپليڪيشن شايع ڪرڻ جي ضابطن جي خلاف آهي Google Play تي. ايگزيڪيوٽو ڪوڊ ڊائون لوڊ ڪرڻ کان علاوه، يو سي برائوزر غير محفوظ انداز ۾ ڪندو آهي، جيڪو استعمال ڪري سگهجي ٿو MitM حملي کي شروع ڪرڻ لاءِ. اچو ته ڏسون ته ڇا اسان اهڙو حملو ڪري سگهون ٿا.

هيٺ ڏنل هر شيءِ لاڳاپيل آهي UC برائوزر جي ورزن لاءِ جيڪا مطالعي جي وقت Google Play تي موجود هئي:

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

ويڪٽر تي حملو

يو سي برائوزر مينيفيسٽ ۾ توھان ھڪ خدمت ڳولي سگھو ٿا ھڪڙي خود وضاحت ڪندڙ نالي سان com.uc.deployment.UpgradeDeployService.

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

جڏهن هي خدمت شروع ٿئي ٿي، برائوزر پوسٽ ڪرڻ جي درخواست ڪري ٿو puds.ucweb.com/upgrade/index.xhtml، جيڪو شروع ٿيڻ کان پوءِ ڪجهه وقت ٽريفڪ ۾ ڏسي سگھجي ٿو. جواب ۾، هن کي ڪجهه تازه ڪاري يا نئين ماڊل ڊائون لوڊ ڪرڻ لاء هڪ حڪم ملي سگهي ٿو. تجزيي دوران، سرور اهڙا حڪم نه ڏنا، پر اسان ڏٺو ته جڏهن اسان برائوزر ۾ PDF کولڻ جي ڪوشش ڪندا آهيون، اهو مٿي ڄاڻايل ايڊريس تي هڪ ٻي درخواست ڪندو آهي، جنهن کان پوء اهو مقامي لائبريري ڊائون لوڊ ڪري ٿو. حملي کي انجام ڏيڻ لاءِ، اسان يو سي برائوزر جي هن خصوصيت کي استعمال ڪرڻ جو فيصلو ڪيو: هڪ مقامي لائبريري استعمال ڪندي PDF کولڻ جي صلاحيت، جيڪا APK ۾ نه آهي ۽ جيڪا ضرورت هجي ته انٽرنيٽ تان ڊائون لوڊ ڪري. اها ڳالهه نوٽ ڪرڻ جي قابل آهي ته، نظرياتي طور تي، يو سي برائوزر کي مجبور ڪري سگهجي ٿو بغير ڪنهن شيءِ کي ڊائون لوڊ ڪرڻ لاءِ صارف جي رابطي کان سواءِ - جيڪڏهن توهان هڪ درخواست جو سٺو ٺهيل جواب ڏيو جيڪو برائوزر شروع ٿيڻ کان پوءِ عمل ۾ اچي ٿو. پر ائين ڪرڻ لاءِ، اسان کي سرور سان رابطي جي پروٽوڪول جو وڌيڪ تفصيل سان مطالعو ڪرڻو پوندو، ان ڪري اسان فيصلو ڪيو ته ان ۾ مداخلت ٿيل جواب کي ايڊٽ ڪرڻ ۽ PDF سان ڪم ڪرڻ لاءِ لائبريري کي تبديل ڪرڻ آسان ٿيندو.

تنهن ڪري، جڏهن صارف هڪ PDF کي سڌو سنئون برائوزر ۾ کولڻ چاهي ٿو، هيٺ ڏنل درخواستون ٽرئفڪ ۾ ڏسي سگهجن ٿيون:

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

پهرين پوسٽ جي درخواست آهي puds.ucweb.com/upgrade/index.xhtml، پوءِ
PDF ۽ آفيس فارميٽ ڏسڻ لاءِ لائبريري سان گڏ هڪ آرڪائيو ڊائون لوڊ ڪيو ويو آهي. اهو سمجهڻ منطقي آهي ته پهرين درخواست سسٽم بابت معلومات منتقل ڪري ٿي (گهٽ ۾ گهٽ گهربل لائبريري مهيا ڪرڻ لاءِ آرڪيٽيڪچر) ۽ ان جي جواب ۾ برائوزر کي لائبريري بابت ڪجهه معلومات ملي ٿي جيڪا ڊائون لوڊ ٿيڻ جي ضرورت آهي: ايڊريس ۽ ممڪن طور تي ، ڪجھ ٻيو. مسئلو اهو آهي ته هي درخواست انڪوڊ ٿيل آهي.

درخواست جو حصو

جواب جو حصو

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

لائبريري پاڻ زپ ۾ پيڪيج ٿيل آهي ۽ انڪوڊ ٿيل نه آهي.

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

ٽريفڪ ڊيڪرپشن ڪوڊ جي ڳولا ڪريو

اچو ته سرور جي جواب کي سمجهڻ جي ڪوشش ڪريون. اچو ته ڪلاس ڪوڊ ڏسو com.uc.deployment.UpgradeDeployService: طريقي کان onStartCommand ڏانهن وڃو com.uc.deployment.bx، ۽ ان کان وٺي 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);
}

اسان هتي پوسٽ جي درخواست جي ٺهڻ کي ڏسو. اسان 16 بائيٽ جي هڪ صف جي تخليق ۽ ان جي ڀرڻ تي ڌيان ڏيون ٿا: 0x5F، 0، 0x1F، -50 (=0xCE). انهي سان ٺهڪي اچي ٿو جيڪو اسان مٿي ڏنل درخواست ۾ ڏٺو.

ساڳئي ڪلاس ۾ توهان هڪ نسٽڊ ڪلاس ڏسي سگهو ٿا جنهن ۾ هڪ ٻيو دلچسپ طريقو آهي:

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

طريقو ان پٽ جي طور تي بائيٽس جو هڪ صف وٺندو آهي ۽ چيڪ ڪري ٿو ته صفر بائيٽ 0x60 آهي يا ٽيون بائيٽ 0xD0 آهي، ۽ ٻيو بائيٽ 1، 11 يا 0x1F آهي. اسان سرور کان جواب تي نظر رکون ٿا: صفر بائيٽ 0x60 آهي، ٻيو آهي 0x1F، ٽيون آهي 0x60. اهو آواز آهي جيڪو اسان کي گهرجي. لائنن ذريعي فيصلو ڪندي ("up_decrypt"، مثال طور)، ھڪڙو طريقو ھتي سڏيو وڃي ٿو جيڪو سرور جي جواب کي رد ڪري ڇڏيندو.
اچو ته طريقي سان اڳتي وڌون جي جي. نوٽ ڪريو ته پھريون دليل آھي بائيٽ آف سيٽ 2 تي (يعني 0x1F اسان جي ڪيس ۾) ۽ ٻيو آھي سرور جو جواب بغير
پهريون 16 بائيٽ.

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

ظاهر آهي، هتي اسان هڪ ڊيڪرپشن الگورٿم چونڊيو ٿا، ۽ ساڳيو بائيٽ جيڪو اسان ۾ آهي
ڪيس 0x1F جي برابر، ٽن ممڪن اختيارن مان هڪ کي ظاهر ڪري ٿو.

اسان ڪوڊ جو تجزيو ڪرڻ جاري رکون ٿا. ڪجھ جمپ کان پوء اسان پاڻ کي ھڪڙي طريقي سان ڳوليندا آھيون ھڪڙي خود وضاحت ڪندڙ نالو سان decryptBytesByKey.

هتي ٻه وڌيڪ بائيٽ اسان جي جواب کان الڳ ڪيا ويا آهن، ۽ انهن مان هڪ تار حاصل ڪيو ويو آهي. اهو واضح آهي ته هن طريقي سان پيغام کي ختم ڪرڻ لاء اهم چونڊيو ويو آهي.

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

اڳتي ڏسي رهيا آهيون، اسان ياد رکون ٿا ته هن اسٽيج تي اسان اڃا تائين هڪ اهم حاصل نه ڪيو آهي، پر صرف ان جي "سڃاڻندڙ". چاٻي حاصل ڪرڻ ٿورو وڌيڪ پيچيده آهي.

ايندڙ طريقي ۾، ٻه وڌيڪ پيٽرولر شامل ڪيا ويا آهن موجوده ۾، انهن مان چار ٺاهيندا آهن: جادو نمبر 16، اهم سڃاڻپ ڪندڙ، انڪوڊ ٿيل ڊيٽا، ۽ هڪ ناقابل فهم تار (اسان جي صورت ۾، خالي).

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

منتقلي جي هڪ سيريز کان پوء، اسان طريقي تي پهچي ويا آهيون staticBinarySafeDecryptNoB64 انٽرويو com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. مکيه ايپليڪيشن ڪوڊ ۾ ڪي به ڪلاس نه آهن جيڪي هن انٽرفيس کي لاڳو ڪن. فائل ۾ هڪ اهڙي ڪلاس آهي lib/armeabi-v7a/libsgmain.so، جيڪو اصل ۾ هڪ .so نه آهي، پر هڪ .jar. اهو طريقو جنهن ۾ اسان دلچسپي وٺندا آهيون هيٺ ڏنل طريقي سان عمل ڪيو ويندو آهي:

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

هتي اسان جي پيرا ميٽرن جي فهرست ٻن وڌيڪ عددن سان گڏ آهي: 2 ۽ 0.
سڀڪنھن شيء کي، 2 مطلب decryption، جيئن طريقو ۾ do فائنل سسٽم ڪلاس javax.crypto.Cipher. ۽ اهو سڀ ڪجهه 10601 نمبر سان هڪ مخصوص روٽر ڏانهن منتقل ڪيو ويو آهي - اهو ظاهر آهي ته حڪم نمبر.

ٽرانزيڪشن جي ايندڙ زنجير کان پوءِ اسان هڪ ڪلاس ڳوليندا آهيون جيڪو انٽرفيس کي لاڳو ڪري ٿو IRouter Component ۽ طريقو 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);
}
}

۽ پڻ ڪلاس JNICL لائبريري، جنهن ۾ ملڪي طريقو قرار ڏنو ويو آهي doCommandNative:

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

هن جو مطلب آهي ته اسان کي مقامي ڪوڊ ۾ هڪ طريقو ڳولڻ جي ضرورت آهي doCommandNative. ۽ هي آهي جتي مزو شروع ٿئي ٿو.

مشيني ڪوڊ جي ابتڙ

فائل ۾ libsgmain.so (جيڪو اصل ۾ هڪ .jar آهي ۽ جنهن ۾ اسان ڪجهه انڪرپشن سان لاڳاپيل انٽرفيس جي نفاذ کي صرف مٿي ڏٺو آهي) اتي هڪ مقامي لائبريري آهي: libsgmainso-6.4.36.so. اسان ان کي IDA ۾ کوليو ۽ غلطين سان ڊائلاگ باڪس جو هڪ گروپ حاصل ڪيو. مسئلو اهو آهي ته سيڪشن هيڊر ٽيبل غلط آهي. اهو مقصد تي ڪيو ويو آهي تجزيو کي پيچيده ڪرڻ لاء.

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

پر اهو ضروري ناهي ته: ELF فائل کي صحيح طريقي سان لوڊ ڪرڻ ۽ ان جو تجزيو ڪرڻ لاءِ، پروگرام هيڊر ٽيبل ڪافي آهي. تنهن ڪري، اسان صرف سيڪشن ٽيبل کي حذف ڪريون ٿا، هيڊر ۾ لاڳاپيل شعبن کي صفر ڪندي.

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

IDA ۾ فائل ٻيهر کوليو.

جاوا ورچوئل مشين کي ٻڌائڻ جا ٻه طريقا آهن جتي اصل لائبريريءَ ۾ جاوا ڪوڊ ۾ بيان ڪيل طريقي جي نفاذ کي اصل ۾ واقع آهي. پهريون اهو آهي ته ان کي هڪ قسم جو نالو ڏيو Java_package_name_ClassName_MethodName.

ٻيو ان کي رجسٽر ڪرڻ آهي جڏهن لائبريري لوڊ ڪندي (فنڪشن ۾ JNI_Onload)
فنڪشن ڪال استعمال ڪندي رجسٽر نيٽيوز.

اسان جي حالت ۾، جيڪڏهن اسان پهريون طريقو استعمال ڪندا آهيون، نالو هن طرح هجڻ گهرجي: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

برآمد ٿيل ڪمن ۾ ڪو به اهڙو فنڪشن ناهي، جنهن جو مطلب آهي ته توهان کي ڪال ڳولڻ جي ضرورت آهي رجسٽر نيٽيوز.
اچو ته فنڪشن ڏانهن وڃو JNI_Onload ۽ اسان هي تصوير ڏسون ٿا:

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

هتي ڇا ٿي رهيو آهي؟ پهرين نظر ۾، فنڪشن جي شروعات ۽ پڇاڙي ARM فن تعمير لاء عام آهن. اسٽيڪ تي پهرين هدايتون رجسٽر جي مواد کي محفوظ ڪري ٿو جيڪو فنڪشن پنهنجي آپريشن ۾ استعمال ڪندو (هن صورت ۾، R0، R1 ۽ R2)، انهي سان گڏ LR رجسٽر جو مواد، جنهن ۾ فنڪشن مان واپسي ايڊريس شامل آهي. . آخري هدايتون محفوظ ٿيل رجسٽرن کي بحال ڪري ٿو، ۽ واپسي جو پتو فوري طور تي پي سي رجسٽر ۾ رکيل آهي - اهڙيء طرح فنڪشن مان واپسي. پر جيڪڏهن توهان ويجهڙائي سان ڏسندا، توهان کي خبر پوندي ته حتمي هدايتون اسٽيڪ تي محفوظ ڪيل واپسي پتي کي تبديل ڪري ٿي. اچو ته حساب ڪريون ته پوءِ ڇا ٿيندو
ڪوڊ لڳائڻ. ھڪڙو خاص پتو 1xB0 R130 ۾ لوڊ ڪيو ويو آھي، 5 ان مان گھٽايو ويندو آھي، پوء اھو R0 ڏانھن منتقل ڪيو ويندو آھي ۽ 0x10 ان ۾ شامل ڪيو ويندو آھي. اهو نڪتو 0xB13B. اهڙيء طرح، IDA سوچيو ته آخري هدايتون هڪ عام فنڪشنل موٽڻ آهي، پر حقيقت ۾ اهو ڳڻپيوڪر ايڊريس 0xB13B ڏانهن وڃي رهيو آهي.

اهو هتي ياد ڪرڻ جي قابل آهي ته ARM پروسيسرز وٽ ٻه طريقا آهن ۽ هدايتن جا ٻه سيٽ آهن: ARM ۽ انگوٺ. پتي جو گھٽ ۾ گھٽ اھم بٽ پروسيسر کي ٻڌائي ٿو ته ھدايت وارو سيٽ استعمال ڪيو پيو وڃي. اھو آھي، پتو اصل ۾ 0xB13A آھي، ۽ ھڪڙو گھٽ ۾ گھٽ اھم بٽ انگوزي موڊ کي اشارو ڪري ٿو.

هن لائبريري ۾ هر فنڪشن جي شروعات ۾ هڪ جهڙو ”اڊاپٽر“ شامل ڪيو ويو آهي ۽
گندگي جو ڪوڊ. اسان انهن تي وڌيڪ تفصيل سان نه رهنداسين - اسان کي صرف ياد آهي
ته لڳ ڀڳ سڀني ڪمن جي حقيقي شروعات ٿورڙي پري آهي.

جيئن ته ڪوڊ واضح طور تي 0xB13A ڏانهن نه ٿو اچي، IDA پاڻ کي سڃاڻي نه سگهيو ته ڪوڊ هن جڳهه تي واقع هو. ساڳئي سبب لاء، اهو لائبريري ۾ اڪثر ڪوڊ ڪوڊ جي طور تي نه سڃاڻندو آهي، جيڪو تجزيو ڪجهه ڏکيو بڻائي ٿو. اسان IDA کي ٻڌايو ته هي ڪوڊ آهي، ۽ اهو ئي ٿئي ٿو:

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

ٽيبل واضح طور تي شروع ٿئي ٿو 0xB144. sub_494C ۾ ڇا آهي؟

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

جڏهن هن فنڪشن کي LR رجسٽر ۾ سڏين ٿا، اسان کي اڳئين ذڪر ڪيل ٽيبل جو پتو (0xB144) ملي ٿو. R0 ۾ - انڊيڪس هن جدول ۾. اھو آھي، قدر ميز تان ورتو ويو آھي، LR ۾ شامل ڪيو ويو آھي ۽ نتيجو آھي
وڃڻ جو پتو. اچو ته ان کي ڳڻڻ جي ڪوشش ڪريون: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. اسان حاصل ڪيل ايڊريس تي وڃون ٿا ۽ لفظي طور تي ڪجھ مفيد هدايتون ڏسو ۽ ٻيهر وڃو 0xB140:

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

ھاڻي ٽيبل مان انڊيڪس 0x20 سان آفسيٽ تي ھڪڙي منتقلي ٿيندي.

جدول جي ماپ جي لحاظ کان، ڪوڊ ۾ ڪيتريون ئي اهڙيون تبديليون هونديون. سوال اهو پيدا ٿئي ٿو ته ڇا اهو ممڪن آهي ته ڪنهن به طريقي سان هن سان وڌيڪ خودڪار طريقي سان ڊيل ڪرڻ، دستي طور تي پتي جي حساب سان. ۽ اسڪرپٽ ۽ IDA ۾ پيچ ڪوڊ ڪرڻ جي صلاحيت اسان جي مدد لاءِ اچي ٿي:

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"

ڪرسر کي 0xB26A لائن تي رکو، اسڪرپٽ کي هلائڻ ۽ 0xB4B0 ڏانهن منتقلي ڏسو:

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

IDA ٻيهر هن علائقي کي ڪوڊ طور تسليم نه ڪيو. اسان هن جي مدد ڪريو ۽ اتي هڪ ٻيو ڊزائن ڏسو:

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

BLX کان پوءِ هدايتون گهڻو احساس نه ٿيون لڳي، اهو وڌيڪ آهي ڪجهه قسم جي بي گھرڻ وانگر. اچو ته ڏسو sub_4964:

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

۽ درحقيقت، هتي LR ۾ موجود ايڊريس تي هڪ لفظ ورتو ويو آهي، هن ايڊريس تي شامل ڪيو ويو آهي، جنهن کان پوء نتيجو پتي تي قيمت ورتي ويندي آهي ۽ اسٽيڪ تي رکيل آهي. انهي سان گڏ، 4 کي LR ۾ شامل ڪيو ويو آهي ته جيئن فنڪشن مان موٽڻ کان پوء، اهو ساڳيو آفسٽ ڇڏيو ويو آهي. جنهن کان پوءِ POP {R1} ڪمانڊ اسٽيڪ مان نڪرندڙ قدر وٺندو آهي. جيڪڏهن توهان ڏسندا ته ايڊريس 0xB4BA + 0xEA = 0xB5A4 تي ڇا واقع آهي، توهان ڏسندا ته ايڊريس ٽيبل وانگر ڪجهه ملندو:

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

ھن ڊيزائن کي پيچ ڪرڻ لاءِ، توھان کي ڪوڊ مان ٻه پيرا ميٽر حاصل ڪرڻا پوندا: آفسيٽ ۽ رجسٽر نمبر جنھن ۾ توھان نتيجو ڪڍڻ چاھيو ٿا. هر ممڪن رجسٽر لاءِ، توهان کي اڳ ۾ ئي ڪوڊ جو هڪ ٽڪرو تيار ڪرڻو پوندو.

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"

اسان ڪرسر کي ڍانچي جي شروعات ۾ رکون ٿا جيڪو اسان تبديل ڪرڻ چاهيون ٿا - 0xB4B2 - ۽ اسڪرپٽ کي هلائيندا آهيون:

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

اڳ ۾ ئي ذڪر ڪيل اڏاوتن کان علاوه، ڪوڊ پڻ شامل آهن:

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

جيئن ته پوئين صورت ۾، BLX هدايتن کان پوء اتي هڪ آفسيٽ آهي:

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

اسان ايل آر کان ايڊريس تي آفسيٽ وٺي، ان کي LR ۾ شامل ڪريو ۽ اتي وڃو. 0x72044 + 0xC = 0x72050. هن ڊيزائن جي اسڪرپٽ بلڪل سادي آهي:

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"

اسڪرپٽ جي عمل جو نتيجو:

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

هڪ دفعو هر شي کي فنڪشن ۾ پيچ ڪيو ويو آهي، توهان IDA کي ان جي حقيقي شروعات ڏانهن اشارو ڪري سگهو ٿا. اهو سڀ فنڪشن ڪوڊ گڏ ڪري ڇڏيندو، ۽ اهو HexRays استعمال ڪندي ختم ڪري سگهجي ٿو.

ڊيڪوڊنگ تار

اسان لئبرريءَ ۾ مشيني ڪوڊ جي گمراھيءَ سان ڊيل ڪرڻ سکيو آھي libsgmainso-6.4.36.so UC برائوزر کان ۽ فنڪشن ڪوڊ حاصل ڪيو 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;
}

اچو ته هيٺ ڏنل لائنن تي هڪ ويجهي نظر رکون:

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

فنڪشن ۾ ذيلي_73E24 ڪلاس جو نالو واضح طور تي ڊسڪ ڪيو پيو وڃي. هن فنڪشن جي پيٽرولر جي طور تي، انڪرپٽ ٿيل ڊيٽا وانگر ڊيٽا ڏانهن اشارو، هڪ خاص بفر ۽ هڪ نمبر گذري ويا آهن. ظاهر آهي، فنڪشن کي ڪال ڪرڻ کان پوء، بفر ۾ هڪ ڊريپ ٿيل لائن هوندي، ڇاڪاڻ ته اها فنڪشن ڏانهن گذري ويندي آهي. ڳوليو ڪلاس، جيڪو ڪلاس جو نالو ٻئي پيٽرولر طور وٺندو آهي. تنهن ڪري، نمبر بفر جي ماپ يا لڪير جي ڊيگهه آهي. اچو ته ڪلاس جي نالي کي سمجهڻ جي ڪوشش ڪريون، اهو اسان کي ٻڌائڻ گهرجي ته ڇا اسان صحيح طرف وڃي رهيا آهيون. اچو ته هڪ ويجهي نظر رکون ته ڇا ٿئي ٿو ذيلي_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;
}

فعل ذيلي_7AF78 بيان ڪيل سائيز جي بائيٽ صفن لاء هڪ ڪنٽينر جو هڪ مثال ٺاهي ٿو (اسان انهن ڪنٽينرز تي تفصيل سان نه رهنداسين). هتي ٻه اهڙا ڪنٽينر ٺاهيا ويا آهن: هڪ تي مشتمل آهي لڪير "DcO/lcK+h?m3c*q@" (اهو اندازو لڳائڻ آسان آهي ته هي هڪ ڪنجي آهي)، ٻيو انڪريپٽ ٿيل ڊيٽا تي مشتمل آهي. اڳيون، ٻئي شيون هڪ خاص ڍانچي ۾ رکيل آهن، جيڪو فنڪشن ڏانهن گذري ٿو sub_6115C. اچو ته ھن ڍانچي ۾ قدر 3 سان ھڪڙي فيلڊ کي پڻ نشان ھڻو، اچو ته ڏسون ته ھن ڍانچي جو اڳتي ڇا ٿيندو.

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

سوئچ پيراميٽر هڪ ڍانچي جو ميدان آهي جنهن کي اڳي ئي مقرر ڪيو ويو هو قدر 3. ڏسو ڪيس 3: فنڪشن ڏانهن sub_6364C پيرا ميٽرز ان ڍانچي مان گذري ويا آهن جيڪي اڳئين فنڪشن ۾ شامل ڪيا ويا هئا، يعني ڪيئي ۽ انڪرپٽ ٿيل ڊيٽا. جيڪڏهن توهان غور سان ڏسندا sub_6364C، توھان ان ۾ RC4 الگورتھم کي سڃاڻي سگھو ٿا.

اسان وٽ هڪ الگورتھم ۽ هڪ ڪنجي آهي. اچو ته ڪلاس جي نالي کي سمجهڻ جي ڪوشش ڪريون. هتي ڇا ٿيو آهي: com/taobao/wireless/security/adapter/JNICLlibrary. زبردست! اسان صحيح رستي تي آهيون.

حڪم جو وڻ

هاڻي اسان کي هڪ چئلينج ڳولڻ جي ضرورت آهي رجسٽر نيٽيوز، جيڪو اسان کي فنڪشن ڏانهن اشارو ڪندو doCommandNative. اچو ته ان کان سڏ ڪيل افعال تي نظر رکون JNI_Onload, ۽ اسان ان ۾ ڳوليون ٿا ذيلي_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;
}

۽ درحقيقت، نالي سان هڪ اصلي طريقو هتي رجسٽر ٿيل آهي doCommandNative. هاڻي اسان کي سندس پتو معلوم ٿئي ٿو. اچو ته ڏسون ڇا ٿو ڪري.

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

نالي مان توهان اندازو لڳائي سگهو ٿا ته هتي سڀني ڪمن جو داخلا پوائنٽ آهي جنهن کي ڊولپرز مقامي لائبريري ڏانهن منتقل ڪرڻ جو فيصلو ڪيو آهي. اسان فنڪشن نمبر 10601 ۾ دلچسپي رکون ٿا.

توهان ڪوڊ مان ڏسي سگهو ٿا ته حڪم نمبر ٽي نمبر پيدا ڪري ٿو: حڪم / 10000, حڪم٪ 10000 / 100 и حڪم % 10، يعني، اسان جي صورت ۾، 1، 6 ۽ 1. اهي ٽي نمبر، انهي سان گڏ هڪ اشارو JNIEnv ۽ فنڪشن ۾ منظور ٿيل دليلن کي هڪ ڍانچي ۾ شامل ڪيو ويو آهي ۽ منظور ڪيو ويو آهي. حاصل ڪيل ٽن نمبرن کي استعمال ڪندي (اچو ته انھن کي بيان ڪريو N1، N2 ۽ N3)، ھڪڙو ڪمانڊ وڻ ٺاھيو ويو آھي.

ڪجهه هن طرح:

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

وڻ کي متحرڪ طور تي ڀريو ويندو آهي JNI_Onload.
ٽي نمبر وڻ ۾ رستو انڪوڊ ڪن ٿا. وڻ جي هر پني ۾ لاڳاپيل فنڪشن جو پڪو پتو هوندو آهي. ڪنجي والدين نوڊ ۾ آهي. ڪوڊ ۾ جڳھ ڳولهڻ جتي اسان کي وڻ ۾ شامل ڪيل فنڪشن کي شامل ڪرڻ ڏکيو نه آهي جيڪڏهن توهان استعمال ڪيل سڀني ساختن کي سمجھندا آهيو (اسين انهن کي بيان نه ڪندا آهيون ته جيئن اڳ ۾ ئي وڏي مضمون کي ڦٽو نه ڪيو وڃي).

وڌيڪ ڇڪتاڻ

اسان کي فنڪشن جو پتو ملي ٿو جيڪو ٽرئفڪ کي ختم ڪرڻ گهرجي: 0x5F1AC. پر خوش ٿيڻ تمام جلدي آهي: يو سي برائوزر جي ڊولپرز اسان لاءِ هڪ ٻيو تعجب تيار ڪيو آهي.

جاوا ڪوڊ ۾ ٺهيل صف مان پيرا ميٽرز حاصل ڪرڻ کان پوء، اسان حاصل ڪندا آهيون
ايڊريس 0x4D070 تي فنڪشن ڏانهن. ۽ هتي هڪ ٻيو قسم جو ڪوڊ اوچتو اسان جو انتظار ڪري رهيو آهي.

اسان ٻه انڊيڪس R7 ۽ R4 ۾ رکون ٿا:

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

اسان پهرين انڊيڪس کي R11 ڏانهن منتقل ڪيو:

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

ٽيبل مان پتو حاصل ڪرڻ لاءِ، انڊيڪس استعمال ڪريو:

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

پهرين ايڊريس تي وڃڻ کان پوء، ٻيو انڊيڪس استعمال ڪيو ويندو آهي، جيڪو R4 ۾ آهي. ٽيبل ۾ 230 عناصر آھن.

ان بابت ڇا ڪجي؟ توهان IDA کي ٻڌائي سگهو ٿا ته هي هڪ سوئچ آهي: ايڊٽ ڪريو -> ٻيا -> سوئچ جو محاورو بيان ڪريو.

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

نتيجو ڪوڊ خوفناڪ آهي. پر، ان جي جنگل ذريعي پنهنجو رستو ٺاهيندي، توهان محسوس ڪري سگهو ٿا هڪ ڪال ڪال هڪ فنڪشن لاءِ جيڪو اسان کان واقف آهي sub_6115C:

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

اتي ھڪڙو سوئچ ھو جنھن ۾ 3 صورت ۾ RC4 الورورٿم استعمال ڪندي ھڪڙي ڊسڪشن ھو. ۽ انهي صورت ۾، فنڪشن ڏانهن منظور ٿيل جوڙجڪ پيرا ميٽرن مان ڀريو ويو آهي doCommandNative. اچو ته ياد رکون ته اسان وٽ اتي ڇا هو magicInt قدر سان 16. اسان لاڳاپيل ڪيس کي ڏسون ٿا - ۽ ڪيترن ئي منتقلي کان پوء اسان کي ڪوڊ ملي ٿو جنهن سان الگورتھم کي سڃاڻي سگهجي ٿو.

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

هي AES آهي!

الورورٿم موجود آهي، باقي اهو سڀ ڪجهه حاصل ڪرڻ آهي ان جا پيرا ميٽرز: موڊ، ڪي ۽، ممڪن طور تي، شروعاتي ویکٹر (ان جي موجودگي جو دارومدار AES الورورٿم جي آپريٽنگ موڊ تي آهي). انهن سان گڏ ڍانچي کي فنڪشن سڏڻ کان اڳ ڪٿي ٺهيل هجڻ گهرجي sub_6115C، پر ڪوڊ جو هي حصو خاص طور تي چڱيءَ طرح مبهم آهي، تنهن ڪري اهو خيال پيدا ٿئي ٿو ته ڪوڊ کي پيچ ڪيو وڃي ته جيئن ڊيڪرپشن فنڪشن جا سڀئي پيرا ميٽر فائل ۾ ڊمپ ڪيا وڃن.

پيچ

اسمبلي ٻولي ۾ سمورو پيچ ڪوڊ دستي طور تي نه لکڻ لاءِ، توهان Android اسٽوڊيو لانچ ڪري سگهو ٿا، اتي هڪ فنڪشن لکي سگهو ٿا جيڪو ساڳيو ان پٽ پيراميٽر وصول ڪري جيئن اسان جي ڊيڪرپشن فنڪشن ۽ فائل تي لکندو، پوءِ ان ڪوڊ کي ڪاپي پيسٽ ڪري جيڪو ڪمپلر ڪندو. پيدا ڪرڻ.

UC برائوزر ٽيم مان اسان جي دوستن به ڪوڊ شامل ڪرڻ جي سهولت جو خيال رکيو. اچو ته ياد رکون ته هر فنڪشن جي شروعات ۾ اسان وٽ گندگي جو ڪوڊ آهي جيڪو آساني سان ڪنهن ٻئي سان تبديل ڪري سگهجي ٿو. تمام آسان 🙂 جڏهن ته، ٽارگيٽ فنڪشن جي شروعات ۾ ڪوڊ لاءِ ڪافي جاءِ نه آهي جيڪا فائل ۾ سڀني پيرا ميٽرز کي محفوظ ڪري. مون کي ان کي حصن ۾ ورهائڻو هو ۽ پاڙيسري ڪمن مان ڪچري وارا بلاڪ استعمال ڪرڻا هئا. مجموعي ۾ چار حصا هئا.

پهرين حصو

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

ARM آرڪيٽيڪچر ۾، پهرين چار فنڪشن پيرا ميٽرز رجسٽرڊ R0-R3 ذريعي گذري ويا آهن، باقي، جيڪڏهن ڪو به، اسٽيڪ ذريعي گذري ويا آهن. LR رجسٽر واپسي ايڊريس رکي ٿو. اهو سڀ ڪجهه محفوظ ڪرڻ جي ضرورت آهي انهي ڪري ته فنڪشن ڪم ڪري سگهي ٿو اسان کي ان جي پيٽرولن کي ڊمپ ڪرڻ کان پوء. اسان کي انهن سڀني رجسٽرن کي پڻ محفوظ ڪرڻ جي ضرورت آهي جيڪي اسان پروسيس ۾ استعمال ڪنداسين، تنهنڪري اسان PUSH.W {R0-R10,LR} ڪندا آهيون. R7 ۾ اسان کي اسٽيڪ ذريعي فنڪشن کي منظور ڪيل پيرا ميٽرن جي لسٽ جو پتو ملي ٿو.

فنڪشن استعمال ڪندي فوپن اچو ته فائل کوليون /data/local/tmp/aes "ab" موڊ ۾
يعني اضافي لاءِ. R0 ۾ اسان فائل جي نالي جو پتو لوڊ ڪريون ٿا، R1 ۾ - لڪير جو پتو موڊ کي اشارو ڪري ٿو. ۽ ھتي گندگي جو ڪوڊ ختم ٿئي ٿو، تنھنڪري اسان اڳتي وڌون ٿا ايندڙ فنڪشن ڏانھن. ان لاءِ ڪم جاري رکڻ لاءِ، اسان شروعات ۾ ڪم جي حقيقي ڪوڊ ڏانھن منتقلي کي، ڪچري کي پاسو ڪندي، ۽ گندگي جي بدران اسان پيچ جو تسلسل شامل ڪيو.

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

سڏڻ فوپن.

فنڪشن جا پهريان ٽي پيرا ميٽر aes قسم آهي int. جيئن ته اسان شروع ۾ رجسٽرن کي اسٽيڪ ۾ محفوظ ڪيو، اسان صرف فنڪشن پاس ڪري سگهون ٿا لکڻ انهن جا ايڊريس اسٽيڪ تي آهن.

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

اڳيون اسان وٽ ٽي ڍانچيون آهن جن ۾ ڊيٽا جي سائيز ۽ هڪ پوائنٽر شامل آهي ڊيٽا ڏانهن ڪنجي، شروعاتي ویکٹر ۽ انڪرپٽ ٿيل ڊيٽا.

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

آخر ۾، فائل کي بند ڪريو، رجسٽر بحال ڪريو ۽ ڪنٽرول کي حقيقي فنڪشن ڏانھن منتقل ڪريو aes.

اسان هڪ APK گڏ ڪريون ٿا هڪ پيچ ٿيل لائبريري سان، ان کي سائن ان ڪريو، ان کي ڊوائيس/ايموليٽر تي اپلوڊ ڪريو، ۽ ان کي لانچ ڪريو. اسان ڏسون ٿا ته اسان جو ڊمپ ٺاهيو پيو وڃي، ۽ تمام گهڻو ڊيٽا اتي لکيو پيو وڃي. برائوزر انڪرپشن استعمال ڪري ٿو نه رڳو ٽرئفڪ لاءِ، ۽ سڀ انڪرپشن سوال ۾ موجود فنڪشن جي ذريعي وڃي ٿي. پر ڪجهه سببن لاء ضروري ڊيٽا موجود ناهي، ۽ گهربل درخواست ٽرئفڪ ۾ نظر نه ايندي آهي. انتظار نه ڪرڻ جي لاءِ جيستائين يو سي برائوزر ضروري درخواست ڪرڻ لاءِ تيار نه ٿئي، اچو ته اڳ ۾ مليل سرور کان انڪرپٽ ٿيل جواب وٺون ۽ ايپليڪيشن کي ٻيهر پيچ ڪريو: ڊريپشن کي شامل ڪريو onCreate of the main activity.

    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

اسان گڏ، سائن، انسٽال، لانچ. اسان حاصل ڪريون ٿا NullPointerException ڇاڪاڻ ته طريقو null موٽي آيو.

ڪوڊ جي وڌيڪ تجزيي دوران، هڪ فنڪشن دريافت ڪيو ويو جيڪو دلچسپ لائينن کي بيان ڪري ٿو: "META-INF/" ۽ ".RSA". لڳي ٿو ايپليڪيشن ان جي سرٽيفڪيٽ جي تصديق ڪري رهي آهي. يا ان مان ڪنجيون به ٺاهي ٿو. مان واقعي نه ٿو چاهيان ته سرٽيفڪيٽ سان ڇا ٿي رهيو آهي، تنهنڪري اسان صرف ان کي درست سند ڏينداسين. اچو ته انڪريپٽ ٿيل لائن کي پيچ ڪريون ته جيئن “META-INF/” جي بدران اسان کي “BLABLINF/” حاصل ٿئي، APK ۾ ان نالي سان فولڊر ٺاهيو ۽ اتي اسڪوائرل برائوزر سرٽيفڪيٽ شامل ڪريو.

اسان گڏ، سائن، انسٽال، لانچ. بنگو! اسان وٽ چاٻي آهي!

ايم ايم

اسان کي ڪيئي جي برابر هڪ ڪيئي ۽ شروعاتي ویکٹر ملي ٿو. اچو ته سي بي سي موڊ ۾ سرور جي جواب کي رد ڪرڻ جي ڪوشش ڪريو.

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

اسان آرڪائيو URL ڏسون ٿا، جيڪو ڪجهه MD5 سان ملندڙ جلندڙ آهي، "extract_unzipsize" ۽ هڪ نمبر. اسان چيڪ ڪريون ٿا: آرڪائيو جو MD5 ساڳيو آهي، بي ترتيب ٿيل لائبريري جو سائز ساڳيو آهي. اسان ڪوشش ڪري رهيا آهيون ته هن لائبريري کي پيچ ۽ برائوزر کي ڏيو. اهو ڏيکارڻ لاءِ ته اسان جي پيچ ٿيل لائبريري لوڊ ٿي وئي آهي، اسان "PWNED!" متن سان هڪ ايس ايم ايس ٺاهڻ جو ارادو شروع ڪنداسين. اسان سرور مان ٻه جواب تبديل ڪنداسين: puds.ucweb.com/upgrade/index.xhtml ۽ آرڪائيو ڊائون لوڊ ڪرڻ لاء. پهرين ۾ اسان MD5 کي تبديل ڪريون ٿا (پيڪ ڪرڻ کان پوءِ سائيز تبديل نه ٿيندي آهي)، ٻئي ۾ اسان آرڪائيو کي پيچ ٿيل لائبريري سان ڏيون ٿا.

برائوزر ڪيترائي ڀيرا آرڪائيو ڊائون لوڊ ڪرڻ جي ڪوشش ڪري ٿو، جنهن کان پوء اهو هڪ غلطي ڏئي ٿو. بظاهر ڪجهه
هو پسند نٿو ڪري. هن گندي شڪل جي تجزيو ڪرڻ جي نتيجي ۾، اهو ظاهر ٿيو ته سرور پڻ آرڪائيو جي سائيز کي منتقل ڪري ٿو:

UC برائوزر ۾ ڪمزورين کي ڳولي رھيا آھن

اهو LEB128 ۾ انڪوڊ ٿيل آهي. پيچ کان پوء، لائبريري سان گڏ آرڪائيو جي سائيز ٿوري تبديل ٿي وئي، تنهنڪري برائوزر سمجهي ٿو ته آرڪائيو ڊاهي ڊائون لوڊ ڪيو ويو آهي، ۽ ڪيترن ئي ڪوششن کان پوء ان ۾ غلطي ٿي وئي.

اسان آرڪائيو جي سائيز کي ترتيب ڏيو... ۽ - فتح! 🙂 نتيجو وڊيو ۾ آهي.

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

نتيجا ۽ ڊولپر ردعمل

ساڳيءَ طرح، هيڪرز استعمال ڪري سگھن ٿا غير محفوظ خصوصيت يو سي برائوزر کي ورهائڻ ۽ هلائڻ لاءِ نقصانڪار لائبريريون. اهي لائبريريون برائوزر جي حوالي سان ڪم ڪنديون، ان ڪري انهن کي ان جي سسٽم جون سڀ اجازتون ملنديون. نتيجي طور، فشنگ ونڊوز کي ڊسپلي ڪرڻ جي صلاحيت، انهي سان گڏ نارنگي چيني اسڪوائر جي ڪم ڪندڙ فائلن تائين رسائي، بشمول ڊيٽابيس ۾ محفوظ ڪيل لاگ ان، پاسورڊ ۽ ڪوڪيز.

اسان UC Browser جي ڊولپرز سان رابطو ڪيو ۽ اسان کي مليل مسئلي جي باري ۾ کين آگاهي ڏني، ان خطري ۽ خطري جي نشاندهي ڪرڻ جي ڪوشش ڪئي، پر هنن اسان سان ڪا به ڳالهه ٻولهه نه ڪئي. ان کان علاوه، برائوزر پنهنجي خطرناڪ خصوصيت کي صاف نظر ۾ جاري رکيو. پر هڪ دفعو اسان نقصان جي تفصيل کي ظاهر ڪيو، اهو هاڻي ممڪن نه هو ته ان کي اڳي وانگر نظر انداز ڪيو وڃي. 27 مارچ هئي
يو سي برائوزر 12.10.9.1193 جو نئون ورزن جاري ڪيو ويو، جيڪو سرور تائين HTTPS ذريعي پهچندو: puds.ucweb.com/upgrade/index.xhtml.

ان کان علاوه، "فيڪس" کان پوء ۽ هن آرٽيڪل لکڻ جي وقت تائين، هڪ برائوزر ۾ PDF کولڻ جي ڪوشش جي نتيجي ۾ متن سان هڪ غلطي پيغام "اڙي، ڪجهه غلط ٿي ويو!" PDF کي کولڻ جي ڪوشش ڪندي سرور کي درخواست نه ڏني وئي، پر ھڪڙي درخواست ڪئي وئي جڏھن برائوزر شروع ڪيو ويو، جيڪو Google Play جي ضابطن جي خلاف ورزي ۾ ايگزيڪيوٽو ڪوڊ ڊائون لوڊ ڪرڻ جي جاري صلاحيت تي اشارو ڪري ٿو.

جو ذريعو: www.habr.com

تبصرو شامل ڪريو