ค้นหาช่องโหว่ใน UC Browser

ค้นหาช่องโหว่ใน UC Browser

การแนะนำ

เมื่อปลายเดือนมีนาคมเรา รายงานพวกเขาค้นพบความสามารถที่ซ่อนอยู่ในการโหลดและเรียกใช้โค้ดที่ไม่ได้รับการยืนยันใน UC Browser วันนี้เราจะดูรายละเอียดว่าการดาวน์โหลดนี้เกิดขึ้นได้อย่างไรและแฮกเกอร์สามารถใช้เพื่อวัตถุประสงค์ของตนเองได้อย่างไร

เมื่อไม่นานมานี้ UC Browser ได้รับการโฆษณาและเผยแพร่อย่างดุเดือด โดยได้รับการติดตั้งบนอุปกรณ์ของผู้ใช้โดยใช้มัลแวร์ เผยแพร่จากไซต์ต่างๆ ภายใต้หน้ากากของไฟล์วิดีโอ (เช่น ผู้ใช้คิดว่าพวกเขากำลังดาวน์โหลด เช่น วิดีโอโป๊ แต่ ได้รับ APK ด้วยเบราว์เซอร์นี้แทน) ใช้แบนเนอร์ที่น่ากลัวพร้อมข้อความว่าเบราว์เซอร์ล้าสมัย มีช่องโหว่ และอะไรทำนองนั้น ในกลุ่ม UC Browser อย่างเป็นทางการบน VK มี หัวข้อซึ่งผู้ใช้สามารถร้องเรียนเกี่ยวกับการโฆษณาที่ไม่เป็นธรรมได้ มีตัวอย่างมากมาย ในปี 2016 มีแม้กระทั่ง โฆษณาวิดีโอ ในภาษารัสเซีย (ใช่ โฆษณาสำหรับเบราว์เซอร์บล็อกโฆษณา)

ในขณะที่เขียนบทความนี้ UC Browser มีการติดตั้งมากกว่า 500 ครั้งบน Google Play สิ่งนี้น่าประทับใจมาก มีเพียง Google Chrome เท่านั้นที่มีมากกว่านี้ ในบรรดาบทวิจารณ์ คุณสามารถเห็นข้อร้องเรียนจำนวนมากเกี่ยวกับการโฆษณาและการเปลี่ยนเส้นทางไปยังแอปพลิเคชันบางตัวบน Google Play นี่คือเหตุผลในการวิจัยของเรา: เราตัดสินใจดูว่า UC Browser กำลังทำอะไรไม่ดีอยู่หรือไม่ และปรากฎว่าเขาทำ!

ในโค้ดของแอปพลิเคชัน ความสามารถในการดาวน์โหลดและเรียกใช้โค้ดที่ปฏิบัติการได้ถูกค้นพบ ซึ่งขัดต่อหลักเกณฑ์ในการเผยแพร่ใบสมัคร บน Google Play นอกเหนือจากการดาวน์โหลดโค้ดปฏิบัติการแล้ว UC Browser ยังทำในลักษณะที่ไม่ปลอดภัย ซึ่งสามารถใช้เพื่อเริ่มการโจมตี MitM ได้ มาดูกันว่าเราจะสามารถทำการโจมตีดังกล่าวได้หรือไม่

ทุกสิ่งที่เขียนด้านล่างนี้เกี่ยวข้องกับเวอร์ชันของเบราว์เซอร์ UC ที่มีอยู่ใน Google Play ณ เวลาที่ศึกษา:

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

เวกเตอร์โจมตี

ในรายการ UC Browser คุณจะพบบริการที่มีชื่อที่อธิบายได้ในตัว com.uc.deployment.UpgradeDeployService.

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

เมื่อบริการนี้เริ่มต้น เบราว์เซอร์จะส่งคำขอ POST ไปที่ puds.ucweb.com/upgrade/index.xhtmlซึ่ง​สามารถ​เห็น​ได้​ใน​การจราจร​ช่วง​หนึ่ง​หลัง​ออก​ทาง​ไป เพื่อเป็นการตอบสนอง เขาอาจได้รับคำสั่งให้ดาวน์โหลดการอัพเดตหรือโมดูลใหม่ ในระหว่างการวิเคราะห์ เซิร์ฟเวอร์ไม่ได้ให้คำสั่งดังกล่าว แต่เราสังเกตเห็นว่าเมื่อเราพยายามเปิด PDF ในเบราว์เซอร์ เซิร์ฟเวอร์จะส่งคำขอครั้งที่สองไปยังที่อยู่ที่ระบุไว้ข้างต้น หลังจากนั้นจะดาวน์โหลดไลบรารีดั้งเดิม เพื่อดำเนินการโจมตี เราตัดสินใจใช้ฟีเจอร์นี้ของเบราว์เซอร์ UC: ความสามารถในการเปิด PDF โดยใช้ไลบรารีดั้งเดิม ซึ่งไม่ได้อยู่ใน APK และจะดาวน์โหลดจากอินเทอร์เน็ตหากจำเป็น เป็นที่น่าสังเกตว่าในทางทฤษฎี UC Browser สามารถถูกบังคับให้ดาวน์โหลดบางสิ่งโดยไม่ต้องมีการโต้ตอบจากผู้ใช้ - หากคุณให้การตอบกลับที่มีรูปแบบที่ดีต่อคำขอที่ดำเนินการหลังจากเปิดตัวเบราว์เซอร์ แต่ในการทำเช่นนี้ เราต้องศึกษาโปรโตคอลของการโต้ตอบกับเซิร์ฟเวอร์โดยละเอียดมากขึ้น ดังนั้นเราจึงตัดสินใจว่าการแก้ไขการตอบกลับที่ถูกดักจับจะง่ายกว่าและแทนที่ไลบรารีสำหรับการทำงานกับ PDF

ดังนั้น เมื่อผู้ใช้ต้องการเปิด PDF โดยตรงในเบราว์เซอร์ คำขอต่อไปนี้จะปรากฏในการรับส่งข้อมูล:

ค้นหาช่องโหว่ใน UC Browser

ก่อนอื่นจะมีการร้องขอ POST ไปที่ puds.ucweb.com/upgrade/index.xhtmlหลังจากนั้น
ดาวน์โหลดที่เก็บถาวรพร้อมไลบรารีสำหรับการดู PDF และรูปแบบ office มีเหตุผลที่จะสมมติว่าคำขอแรกส่งข้อมูลเกี่ยวกับระบบ (อย่างน้อยสถาปัตยกรรมเพื่อจัดเตรียมไลบรารีที่จำเป็น) และในการตอบสนองต่อคำขอนั้น เบราว์เซอร์จะได้รับข้อมูลบางอย่างเกี่ยวกับไลบรารีที่จำเป็นต้องดาวน์โหลด: ที่อยู่และอาจเป็นไปได้ , อื่น ๆ อีก. ปัญหาคือคำขอนี้ถูกเข้ารหัส

ขอส่วน

ตอบส่วน

ค้นหาช่องโหว่ใน UC Browser

ค้นหาช่องโหว่ใน UC Browser

ตัวไลบรารีนั้นบรรจุอยู่ในไฟล์ ZIP และไม่ได้เข้ารหัส

ค้นหาช่องโหว่ใน UC Browser

ค้นหารหัสถอดรหัสการรับส่งข้อมูล

ลองถอดรหัสการตอบสนองของเซิร์ฟเวอร์ มาดูรหัสชั้นเรียนกัน com.uc.deployment.UpgradeDeployService: จากวิธีการ บนStartCommand ไปที่ 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);
}

เราเห็นการก่อตัวของคำขอ POST ที่นี่ เราใส่ใจกับการสร้างอาร์เรย์ขนาด 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 หมายถึงหนึ่งในสามตัวเลือกที่เป็นไปได้

เราวิเคราะห์โค้ดต่อไป หลังจากการกระโดดสองครั้ง เราก็พบว่าตัวเองอยู่ในวิธีการที่มีชื่อที่อธิบายตนเองได้ ถอดรหัส BytesByKey.

ที่นี่อีกสองไบต์จะถูกแยกออกจากการตอบสนองของเรา และได้รับสตริงจากไบต์เหล่านั้น เป็นที่ชัดเจนว่าด้วยวิธีนี้เลือกกุญแจสำหรับการถอดรหัสข้อความ

    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 หมายถึงการถอดรหัสเช่นเดียวกับวิธีการ ทำรอบชิงชนะเลิศ คลาสระบบ javax.crypto.Cipher. และทั้งหมดนี้ถูกถ่ายโอนไปยังเราเตอร์บางตัวที่มีหมายเลข 10601 ซึ่งเห็นได้ชัดว่าเป็นหมายเลขคำสั่ง

หลังจากห่วงโซ่การเปลี่ยนแปลงถัดไป เราจะพบคลาสที่ใช้อินเทอร์เฟซ IRouterComponent และวิธีการ ทำคำสั่ง:

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

แล้วก็คลาสด้วย JNICLibraryซึ่งมีการประกาศวิธีการดั้งเดิม ทำCommandNative:

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

ซึ่งหมายความว่าเราจำเป็นต้องค้นหาวิธีการในโค้ดเนทีฟ ทำCommandNative. และนี่คือจุดเริ่มต้นของความสนุก

ความสับสนของรหัสเครื่อง

ในไฟล์ libsgmain.so (ซึ่งจริงๆ แล้วคือ .jar และเราพบว่ามีการใช้อินเทอร์เฟซที่เกี่ยวข้องกับการเข้ารหัสบางส่วนด้านบน) มีไลบรารีเนทีฟหนึ่งไลบรารี: libsgmainso-6.4.36.so. เราเปิดมันใน IDA และได้รับกล่องโต้ตอบจำนวนมากพร้อมข้อผิดพลาด ปัญหาคือตารางส่วนหัวไม่ถูกต้อง การทำเช่นนี้มีวัตถุประสงค์เพื่อทำให้การวิเคราะห์ซับซ้อนขึ้น

ค้นหาช่องโหว่ใน UC Browser

แต่ไม่จำเป็น: หากต้องการโหลดไฟล์ ELF และวิเคราะห์อย่างถูกต้อง ตารางส่วนหัวของโปรแกรมก็เพียงพอแล้ว ดังนั้นเราจึงลบตารางส่วนโดยลบฟิลด์ที่เกี่ยวข้องในส่วนหัวเป็นศูนย์

ค้นหาช่องโหว่ใน UC Browser

เปิดไฟล์ใน IDA อีกครั้ง

มีสองวิธีในการบอก Java virtual machine ว่าตำแหน่งใดในไลบรารีเนทิฟว่ามีการนำเมธอดที่ประกาศในโค้ด Java ว่าเป็นเนทิฟอยู่ตรงไหน ประการแรกคือการตั้งชื่อสายพันธุ์ให้กับมัน Java_package_name_ClassName_MethodName.

อย่างที่สองคือการลงทะเบียนเมื่อโหลดไลบรารี่ (ในฟังก์ชั่น JNI_OnLoad)
โดยใช้การเรียกใช้ฟังก์ชัน ลงทะเบียนNatives.

ในกรณีของเรา หากเราใช้วิธีแรก ชื่อควรเป็นดังนี้: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

ไม่มีฟังก์ชันดังกล่าวในฟังก์ชันที่ส่งออก ซึ่งหมายความว่าคุณต้องค้นหาการโทร ลงทะเบียนNatives.
ไปที่ฟังก์ชั่นกันเถอะ JNI_OnLoad และเราเห็นภาพนี้:

ค้นหาช่องโหว่ใน UC Browser

เกิดอะไรขึ้นที่นี่? เมื่อมองแวบแรก จุดเริ่มต้นและจุดสิ้นสุดของฟังก์ชันเป็นเรื่องปกติสำหรับสถาปัตยกรรม ARM คำสั่งแรกบนสแต็กจะจัดเก็บเนื้อหาของรีจิสเตอร์ที่ฟังก์ชันจะใช้ในการดำเนินการ (ในกรณีนี้คือ R0, R1 และ R2) เช่นเดียวกับเนื้อหาของรีจิสเตอร์ LR ซึ่งมีที่อยู่ผู้ส่งจากฟังก์ชัน . คำสั่งสุดท้ายจะคืนค่ารีจิสเตอร์ที่บันทึกไว้ และที่อยู่ผู้ส่งจะถูกวางไว้ในรีจิสเตอร์ PC ทันที - จึงเป็นการส่งคืนจากฟังก์ชัน แต่ถ้าคุณมองอย่างใกล้ชิด คุณจะสังเกตเห็นว่าคำสั่งสุดท้ายเปลี่ยนที่อยู่ผู้ส่งที่จัดเก็บไว้ในสแต็ก มาคำนวณกันว่าหลังจากนั้นจะเป็นอย่างไร
การดำเนินการโค้ด โหลดที่อยู่ 1xB0 บางตัวลงใน R130 จากนั้นลบ 5 จากนั้นจึงโอนไปที่ R0 และเพิ่ม 0x10 เข้าไป ปรากฎว่า 0xB13B ดังนั้น IDA คิดว่าคำสั่งสุดท้ายเป็นการส่งคืนฟังก์ชันปกติ แต่จริงๆ แล้วคำสั่งดังกล่าวจะส่งไปยังที่อยู่ที่คำนวณ 0xB13B

โปรดจำไว้ว่าโปรเซสเซอร์ ARM มีสองโหมดและชุดคำสั่งสองชุด: ARM และ Thumb บิตที่มีนัยสำคัญน้อยที่สุดของที่อยู่จะบอกโปรเซสเซอร์ว่ามีการใช้ชุดคำสั่งใด นั่นคือที่อยู่จริง ๆ แล้วคือ 0xB13A และหนึ่งในบิตที่มีนัยสำคัญน้อยที่สุดบ่งบอกถึงโหมด Thumb

มีการเพิ่ม "อะแดปเตอร์" ที่คล้ายกันไว้ที่จุดเริ่มต้นของแต่ละฟังก์ชันในไลบรารีนี้และ
รหัสขยะ เราจะไม่พูดถึงรายละเอียดเหล่านี้เพิ่มเติม - เราแค่จำได้
ว่าจุดเริ่มต้นที่แท้จริงของฟังก์ชั่นเกือบทั้งหมดนั้นอยู่ไกลออกไปอีกหน่อย

เนื่องจากโค้ดไม่ได้ข้ามไปที่ 0xB13A อย่างชัดเจน IDA เองก็ไม่ทราบว่าโค้ดอยู่ที่ตำแหน่งนี้ ด้วยเหตุผลเดียวกัน จึงไม่รู้จักโค้ดส่วนใหญ่ในไลบรารีว่าเป็นโค้ด ซึ่งทำให้การวิเคราะห์ค่อนข้างยาก เราบอก IDA ว่านี่คือรหัส และนี่คือสิ่งที่เกิดขึ้น:

ค้นหาช่องโหว่ใน UC Browser

ตารางเริ่มต้นอย่างชัดเจนที่ 0xB144 มีอะไรอยู่ใน sub_494C?

ค้นหาช่องโหว่ใน UC Browser

เมื่อเรียกใช้ฟังก์ชันนี้ในรีจิสเตอร์ LR เราจะได้ที่อยู่ของตารางที่กล่าวถึงก่อนหน้านี้ (0xB144) ใน R0 - ดัชนีในตารางนี้ นั่นคือค่าจะถูกนำมาจากตาราง เพิ่มใน LR และผลลัพธ์ก็คือ
ที่อยู่ที่จะไป ลองคำนวณดู: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264 ไปที่ที่อยู่ที่ได้รับและดูคำแนะนำที่เป็นประโยชน์สองสามข้อแล้วไปที่ 0xB140 อีกครั้ง:

ค้นหาช่องโหว่ใน UC Browser

ตอนนี้จะมีการเปลี่ยนแปลงที่ชดเชยด้วยดัชนี 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 Browser

IDA ไม่รู้จักพื้นที่นี้เป็นรหัสอีกครั้ง เราช่วยเธอและเห็นการออกแบบอื่นที่นั่น:

ค้นหาช่องโหว่ใน UC Browser

คำแนะนำหลังจาก BLX ดูเหมือนจะไม่สมเหตุสมผลนัก มันเหมือนกับการกระจัดบางอย่างมากกว่า ลองดูที่ sub_4964:

ค้นหาช่องโหว่ใน UC Browser

และแน่นอนว่าที่นี่มีการนำ dword ไปยังที่อยู่ที่อยู่ใน LR แล้วเพิ่มไปยังที่อยู่นี้ หลังจากนั้นค่าของที่อยู่ผลลัพธ์จะถูกนำไปวางบนสแต็ก นอกจากนี้ 4 จะถูกเพิ่มเข้าไปใน LR ดังนั้นหลังจากกลับมาจากฟังก์ชันแล้ว ออฟเซ็ตเดียวกันนี้จะถูกข้ามไป หลังจากนั้นคำสั่ง POP {R1} จะนำค่าผลลัพธ์จากสแต็ก หากคุณดูสิ่งที่อยู่ที่ที่อยู่ 0xB4BA + 0xEA = 0xB5A4 คุณจะเห็นสิ่งที่คล้ายกับตารางที่อยู่:

ค้นหาช่องโหว่ใน UC Browser

ในการแพตช์การออกแบบนี้ คุณจะต้องได้รับพารามิเตอร์สองตัวจากโค้ด: ออฟเซ็ตและหมายเลขรีจิสเตอร์ที่คุณต้องการใส่ผลลัพธ์ สำหรับการลงทะเบียนแต่ละครั้ง คุณจะต้องเตรียมโค้ดไว้ล่วงหน้า

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 Browser

นอกจากโครงสร้างที่กล่าวไปแล้ว โค้ดยังประกอบด้วยสิ่งต่อไปนี้:

ค้นหาช่องโหว่ใน UC Browser

เช่นเดียวกับในกรณีก่อนหน้านี้ หลังจากคำสั่ง BLX จะมีการชดเชย:

ค้นหาช่องโหว่ใน UC Browser

เรานำการชดเชยไปยังที่อยู่จาก LR เพิ่มลงใน 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 Browser

เมื่อทุกอย่างได้รับการแพตช์ในฟังก์ชันแล้ว คุณสามารถชี้ IDA ไปยังจุดเริ่มต้นที่แท้จริงได้ มันจะรวมโค้ดฟังก์ชันทั้งหมดเข้าด้วยกัน และสามารถถอดรหัสได้โดยใช้ HexRays

ถอดรหัสสตริง

เราได้เรียนรู้ที่จะจัดการกับการทำให้รหัสเครื่องสับสนในไลบรารี libsgmainso-6.4.36.so จาก UC Browser และได้รับโค้ดฟังก์ชัน 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);

ในการทำงาน sub_73E24 ชื่อคลาสกำลังถูกถอดรหัสอย่างชัดเจน ตัวชี้ไปยังข้อมูลที่คล้ายกับข้อมูลที่เข้ารหัส บัฟเฟอร์และตัวเลขจะถูกส่งผ่านเป็นพารามิเตอร์ของฟังก์ชันนี้ แน่นอนว่าหลังจากเรียกใช้ฟังก์ชันแล้ว จะมีบรรทัดที่ถอดรหัสอยู่ในบัฟเฟอร์ เนื่องจากจะถูกส่งต่อไปยังฟังก์ชัน ค้นหาคลาสซึ่งใช้ชื่อคลาสเป็นพารามิเตอร์ตัวที่สอง ดังนั้นตัวเลขจึงเป็นขนาดของบัฟเฟอร์หรือความยาวของเส้น ลองถอดรหัสชื่อคลาสดูน่าจะบอกเราได้ว่าเรากำลังไปถูกทางหรือไม่ มาดูสิ่งที่เกิดขึ้นกันดีกว่า 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;
}

ฟังก์ชัน sub_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/ไร้สาย/ความปลอดภัย/อะแดปเตอร์/JNICLibrary. ยอดเยี่ยม! เรามาถูกทางแล้ว

ต้นไม้คำสั่ง

ตอนนี้เราต้องหาความท้าทาย ลงทะเบียนNativesซึ่งจะนำเราไปสู่ฟังก์ชั่น ทำCommandNative. มาดูฟังก์ชันที่ถูกเรียกจากกัน JNI_OnLoad, และเราพบมันใน 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;
}

และแท้จริงแล้ว มีการลงทะเบียนวิธีการดั้งเดิมที่มีชื่อไว้ที่นี่ ทำCommandNative. ตอนนี้เรารู้ที่อยู่ของเขาแล้ว มาดูกันว่าเขาทำอะไร

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 Browser

ต้นไม้ถูกเติมเต็มแบบไดนามิก JNI_OnLoad.
ตัวเลขสามตัวเข้ารหัสเส้นทางในแผนผัง แต่ละใบของต้นไม้มีที่อยู่ที่ถูกแทงของฟังก์ชันที่เกี่ยวข้อง คีย์อยู่ในโหนดหลัก การค้นหาตำแหน่งในโค้ดที่ฟังก์ชันที่เราต้องการถูกเพิ่มลงในแผนผังนั้นไม่ใช่เรื่องยากหากคุณเข้าใจโครงสร้างทั้งหมดที่ใช้ (เราไม่ได้อธิบายไว้เพื่อไม่ให้บทความมีขนาดค่อนข้างใหญ่อยู่แล้ว)

สับสนมากขึ้น

เราได้รับที่อยู่ของฟังก์ชันที่ควรถอดรหัสการรับส่งข้อมูล: 0x5F1AC แต่ยังเร็วเกินไปที่จะชื่นชมยินดี: ผู้พัฒนา UC Browser ได้เตรียมความประหลาดใจอีกอย่างให้กับเรา

หลังจากได้รับพารามิเตอร์จากอาร์เรย์ที่สร้างขึ้นในโค้ด Java เราก็จะได้รับ
ไปยังฟังก์ชันตามที่อยู่ 0x4D070 และนี่คือโค้ดอีกประเภทหนึ่งที่กำลังรอเราอยู่

เราใส่ดัชนีสองตัวไว้ใน R7 และ R4:

ค้นหาช่องโหว่ใน UC Browser

เราเปลี่ยนดัชนีแรกเป็น R11:

ค้นหาช่องโหว่ใน UC Browser

หากต้องการรับที่อยู่จากตาราง ให้ใช้ดัชนี:

ค้นหาช่องโหว่ใน UC Browser

หลังจากไปยังที่อยู่แรกแล้ว ดัชนีที่สองจะถูกใช้ซึ่งอยู่ใน R4 มีองค์ประกอบ 230 รายการในตาราง

จะทำอย่างไรกับมัน? คุณสามารถบอก IDA ได้ว่านี่คือสวิตช์: แก้ไข -> อื่น ๆ -> ระบุสำนวนสวิตช์

ค้นหาช่องโหว่ใน UC Browser

รหัสผลลัพธ์ที่ได้นั้นน่ากลัว แต่เมื่อคุณเดินผ่านป่าของมัน คุณจะสังเกตเห็นการเรียกไปยังฟังก์ชันที่เราคุ้นเคยอยู่แล้ว sub_6115C:

ค้นหาช่องโหว่ใน UC Browser

มีสวิตช์ซึ่งในกรณีที่ 3 มีการถอดรหัสโดยใช้อัลกอริธึม RC4 และในกรณีนี้ โครงสร้างที่ส่งไปยังฟังก์ชันจะถูกเติมจากพารามิเตอร์ที่ส่งไปยัง ทำCommandNative. จำไว้ว่าเรามีอะไรบ้างที่นั่น เมจิกอินท์ ด้วยค่า 16 เราดูกรณีที่เกี่ยวข้อง - และหลังจากการเปลี่ยนแปลงหลายครั้งเราจะพบโค้ดที่สามารถระบุอัลกอริทึมได้

ค้นหาช่องโหว่ใน UC Browser

นี่คือเออีเอส!

อัลกอริธึมมีอยู่ สิ่งที่เหลืออยู่คือการได้รับพารามิเตอร์: โหมด คีย์ และอาจเป็นเวกเตอร์การเริ่มต้น (การมีอยู่ของมันขึ้นอยู่กับโหมดการทำงานของอัลกอริทึม AES) โครงสร้างจะต้องถูกสร้างขึ้นที่ไหนสักแห่งก่อนการเรียกใช้ฟังก์ชัน sub_6115Cแต่โค้ดส่วนนี้มีความซับซ้อนเป็นพิเศษ ดังนั้นจึงเกิดแนวคิดที่จะแก้ไขโค้ดเพื่อให้พารามิเตอร์ทั้งหมดของฟังก์ชันถอดรหัสถูกทิ้งลงในไฟล์

ปะ

เพื่อไม่ให้เขียนโค้ดแพตช์ทั้งหมดในภาษาแอสเซมบลีด้วยตนเอง คุณสามารถเปิด Android Studio เขียนฟังก์ชันที่นั่นซึ่งรับพารามิเตอร์อินพุตเดียวกันกับฟังก์ชันถอดรหัสของเราและเขียนลงในไฟล์ จากนั้นคัดลอกและวางโค้ดที่คอมไพเลอร์จะ สร้าง.

เพื่อนของเราจากทีม UC Browser ยังดูแลเรื่องความสะดวกในการเพิ่มโค้ดอีกด้วย ให้เราจำไว้ว่าที่จุดเริ่มต้นของแต่ละฟังก์ชั่นเรามีรหัสขยะที่สามารถแทนที่ด้วยรหัสอื่นได้อย่างง่ายดาย สะดวกมาก 🙂 อย่างไรก็ตามที่จุดเริ่มต้นของฟังก์ชันเป้าหมายมีพื้นที่ไม่เพียงพอสำหรับโค้ดที่จะบันทึกพารามิเตอร์ทั้งหมดลงในไฟล์ ฉันต้องแยกมันออกเป็นส่วนๆ และใช้บล็อกขยะจากฟังก์ชันข้างเคียง มีทั้งหมดสี่ส่วน

ส่วนแรก:

ค้นหาช่องโหว่ใน UC Browser

ในสถาปัตยกรรม ARM พารามิเตอร์ฟังก์ชันสี่ตัวแรกจะถูกส่งผ่านรีจิสเตอร์ R0-R3 ส่วนที่เหลือ (ถ้ามี) จะถูกส่งผ่านสแต็ก เครื่องลงทะเบียน LR มีที่อยู่สำหรับส่งคืน ทั้งหมดนี้จำเป็นต้องได้รับการบันทึกเพื่อให้ฟังก์ชันสามารถทำงานได้หลังจากที่เราดัมพ์พารามิเตอร์ของมัน เรายังจำเป็นต้องบันทึกรีจิสเตอร์ทั้งหมดที่เราจะใช้ในกระบวนการด้วย ดังนั้นเราจึงทำ PUSH.W {R0-R10,LR} ใน R7 เราได้รับที่อยู่ของรายการพารามิเตอร์ที่ส่งไปยังฟังก์ชันผ่านสแต็ก

การใช้ฟังก์ชัน เปิด มาเปิดไฟล์กันดีกว่า /data/local/tmp/aes ในโหมด "ab"
นั่นคือสำหรับการเพิ่มเติม ใน R0 เราโหลดที่อยู่ของชื่อไฟล์ใน R1 - ที่อยู่ของบรรทัดที่ระบุโหมด และที่นี่รหัสขยะสิ้นสุดลง ดังนั้นเราจึงไปยังฟังก์ชันถัดไป เพื่อให้มันทำงานต่อไปได้เราได้ใส่การเปลี่ยนไปใช้โค้ดจริงของฟังก์ชันตั้งแต่เริ่มต้นโดยข้ามขยะและแทนที่จะเพิ่มขยะเราเพิ่มความต่อเนื่องของแพตช์

ค้นหาช่องโหว่ใน UC Browser

โทร เปิด.

พารามิเตอร์สามตัวแรกของฟังก์ชัน AES มีประเภท int. เนื่องจากเราบันทึกรีจิสเตอร์ลงในสแต็กตั้งแต่เริ่มต้น เราจึงสามารถส่งผ่านฟังก์ชันได้ เขียน ที่อยู่ของพวกเขาบนสแต็ก

ค้นหาช่องโหว่ใน UC Browser

ต่อไป เรามีโครงสร้างสามโครงสร้างที่มีขนาดข้อมูลและตัวชี้ไปยังข้อมูลสำหรับคีย์ เวกเตอร์การเริ่มต้น และข้อมูลที่เข้ารหัส

ค้นหาช่องโหว่ใน UC Browser

ในตอนท้าย ให้ปิดไฟล์ คืนค่ารีจิสเตอร์ และถ่ายโอนการควบคุมไปยังฟังก์ชันจริง AES.

เรารวบรวม APK ที่มีไลบรารี่ที่ได้รับแพตช์ ลงนาม อัปโหลดไปยังอุปกรณ์/โปรแกรมจำลอง และเปิดใช้งาน เราเห็นว่าดัมพ์ของเราถูกสร้างขึ้นและมีการเขียนข้อมูลจำนวนมากที่นั่น เบราว์เซอร์ใช้การเข้ารหัสไม่เพียงแต่สำหรับการรับส่งข้อมูลเท่านั้น และการเข้ารหัสทั้งหมดต้องผ่านฟังก์ชันที่เป็นปัญหา แต่ด้วยเหตุผลบางอย่างไม่มีข้อมูลที่จำเป็นและคำขอที่จำเป็นไม่ปรากฏในการรับส่งข้อมูล เพื่อไม่ให้รอจนกว่า UC Browser จะทำตามคำขอที่จำเป็น ให้เรารับการตอบสนองที่เข้ารหัสจากเซิร์ฟเวอร์ที่ได้รับก่อนหน้านี้และแก้ไขแอปพลิเคชันอีกครั้ง: เพิ่มการถอดรหัสไปที่ onCreate ของกิจกรรมหลัก

    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 และเพิ่มใบรับรองเบราว์เซอร์กระรอกที่นั่น

เราประกอบ ลงนาม ติดตั้ง เปิดตัว บิงโก! เรามีกุญแจ!

มิตรไมตรี

เราได้รับคีย์และเวกเตอร์การเริ่มต้นเท่ากับคีย์ ลองถอดรหัสการตอบสนองของเซิร์ฟเวอร์ในโหมด CBC

ค้นหาช่องโหว่ใน UC Browser

เราเห็น URL ที่เก็บถาวร ซึ่งคล้ายกับ MD5 “extract_unzipsize” และตัวเลข เราตรวจสอบแล้ว: MD5 ของไฟล์เก็บถาวรเหมือนกัน ขนาดของไลบรารีที่คลายแพ็กจะเท่ากัน เรากำลังพยายามแก้ไขไลบรารีนี้และมอบให้กับเบราว์เซอร์ เพื่อแสดงว่าไลบรารี่ที่แพตช์ของเราโหลดแล้ว เราจะเปิดตัว Intent เพื่อสร้าง SMS พร้อมข้อความ "PWNED!" เราจะแทนที่สองคำตอบจากเซิร์ฟเวอร์: puds.ucweb.com/upgrade/index.xhtml และเพื่อดาวน์โหลดไฟล์เก็บถาวร ในครั้งแรกเราจะแทนที่ MD5 (ขนาดจะไม่เปลี่ยนแปลงหลังจากการแตกไฟล์) ในครั้งที่สองเราจะให้ไฟล์เก็บถาวรพร้อมกับไลบรารีที่ได้รับการติดตั้ง

เบราว์เซอร์พยายามดาวน์โหลดไฟล์เก็บถาวรหลายครั้งหลังจากนั้นก็เกิดข้อผิดพลาด เห็นได้ชัดว่ามีอะไรบางอย่าง
เขาไม่ชอบ. จากการวิเคราะห์รูปแบบที่มืดมนนี้ ปรากฎว่าเซิร์ฟเวอร์ส่งขนาดของไฟล์เก็บถาวรด้วย:

ค้นหาช่องโหว่ใน UC Browser

มันถูกเข้ารหัสใน LEB128 หลังจากแพตช์ ขนาดของไฟล์เก็บถาวรที่มีไลบรารีเปลี่ยนไปเล็กน้อย ดังนั้นเบราว์เซอร์จึงพิจารณาว่าไฟล์เก็บถาวรนั้นถูกดาวน์โหลดอย่างคดโกง และหลังจากพยายามหลายครั้งก็เกิดข้อผิดพลาด

เราปรับขนาดของไฟล์เก็บถาวร... และ – ชัยชนะ! 🙂 ผลลัพธ์อยู่ในวิดีโอครับ 🙂

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

ผลที่ตามมาและปฏิกิริยาของนักพัฒนา

ในทำนองเดียวกัน แฮกเกอร์สามารถใช้คุณสมบัติที่ไม่ปลอดภัยของ UC Browser เพื่อแจกจ่ายและเรียกใช้ไลบรารีที่เป็นอันตราย ไลบรารีเหล่านี้จะทำงานในบริบทของเบราว์เซอร์ ดังนั้นไลบรารีเหล่านี้จึงจะได้รับสิทธิ์ของระบบทั้งหมด เป็นผลให้ความสามารถในการแสดงหน้าต่างฟิชชิ่งรวมถึงการเข้าถึงไฟล์การทำงานของกระรอกจีนสีส้มรวมถึงการเข้าสู่ระบบ รหัสผ่าน และคุกกี้ที่เก็บไว้ในฐานข้อมูล

เราติดต่อผู้พัฒนา UC Browser และแจ้งให้ทราบเกี่ยวกับปัญหาที่เราพบ พยายามชี้ให้เห็นถึงช่องโหว่และอันตราย แต่พวกเขาไม่ได้พูดคุยอะไรกับเรา ในขณะเดียวกัน เบราว์เซอร์ยังคงแสดงคุณสมบัติที่เป็นอันตรายอย่างต่อเนื่อง แต่เมื่อเราเปิดเผยรายละเอียดของจุดอ่อนแล้ว ก็ไม่สามารถเพิกเฉยได้เหมือนเมื่อก่อนอีกต่อไป วันที่ 27 มีนาคมเป็น
UC Browser 12.10.9.1193 เวอร์ชันใหม่เปิดตัวแล้ว ซึ่งเข้าถึงเซิร์ฟเวอร์ผ่าน HTTPS: puds.ucweb.com/upgrade/index.xhtml.

นอกจากนี้ หลังจาก "แก้ไข" และจนกว่าจะถึงเวลาเขียนบทความนี้ การพยายามเปิด PDF ในเบราว์เซอร์ทำให้เกิดข้อความแสดงข้อผิดพลาดพร้อมข้อความ "อ๊ะ มีบางอย่างผิดพลาด!" ไม่มีการร้องขอไปยังเซิร์ฟเวอร์เมื่อพยายามเปิด PDF แต่มีการร้องขอเมื่อมีการเปิดตัวเบราว์เซอร์ ซึ่งบ่งบอกถึงความสามารถอย่างต่อเนื่องในการดาวน์โหลดโค้ดที่ปฏิบัติการได้ซึ่งละเมิดกฎของ Google Play

ที่มา: will.com

เพิ่มความคิดเห็น