Tìm kiếm lỗ hổng trong UC Browser

Tìm kiếm lỗ hổng trong UC Browser

Giới thiệu

Cuối tháng XNUMX chúng tôi báo cáo, họ đã phát hiện ra khả năng ẩn để tải và chạy mã chưa được xác minh trong UC Browser. Hôm nay chúng ta sẽ xem xét chi tiết cách quá trình tải xuống này diễn ra và cách tin tặc có thể sử dụng nó cho mục đích riêng của chúng.

Cách đây một thời gian, UC Browser đã được quảng cáo và phân phối rất rầm rộ: nó được cài đặt trên thiết bị của người dùng bằng phần mềm độc hại, được phân phối từ nhiều trang web khác nhau dưới dạng tệp video (ví dụ: người dùng nghĩ rằng họ đang tải xuống, chẳng hạn như một video khiêu dâm, nhưng thay vào đó đã nhận được APK với trình duyệt này), đã sử dụng các biểu ngữ đáng sợ với thông báo rằng trình duyệt đã lỗi thời, dễ bị tấn công và những nội dung tương tự. Trong nhóm UC Browser chính thức trên VK có chủ đề, trong đó người dùng có thể phàn nàn về việc quảng cáo không công bằng, có rất nhiều ví dụ ở đó. Năm 2016 thậm chí còn có quảng cáo video bằng tiếng Nga (vâng, quảng cáo cho trình duyệt chặn quảng cáo).

Tại thời điểm viết bài, UC Browser có hơn 500 lượt cài đặt trên Google Play. Điều này thật ấn tượng - chỉ Google Chrome mới có nhiều hơn thế. Trong số các bài đánh giá, bạn có thể thấy khá nhiều lời phàn nàn về quảng cáo và chuyển hướng đến một số ứng dụng trên Google Play. Đây là lý do cho nghiên cứu của chúng tôi: chúng tôi quyết định xem liệu UC Browser có đang làm điều gì xấu hay không. Và hóa ra là anh ấy làm được!

Trong mã ứng dụng, khả năng tải xuống và chạy mã thực thi đã được phát hiện, trái với quy định về xuất bản ứng dụng trên Google Play. Ngoài việc tải xuống mã thực thi, UC Browser còn thực hiện việc này theo cách không an toàn, có thể được sử dụng để khởi động cuộc tấn công MitM. Hãy xem liệu chúng ta có thể thực hiện một cuộc tấn công như vậy không.

Mọi nội dung được viết bên dưới đều phù hợp với phiên bản UC Browser có sẵn trên Google Play tại thời điểm nghiên cứu:

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

Vectơ tấn công

Trong bảng kê khai UC Browser, bạn có thể tìm thấy một dịch vụ có tên dễ hiểu com.uc.deployment.UpgradeDeployService.

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

Khi dịch vụ này khởi động, trình duyệt sẽ tạo một yêu cầu POST tới puds.ucweb.com/upgrade/index.xhtml, có thể được nhìn thấy trong giao thông một thời gian sau khi bắt đầu. Đáp lại, anh ta có thể nhận được lệnh tải xuống một số bản cập nhật hoặc mô-đun mới. Trong quá trình phân tích, máy chủ không đưa ra các lệnh như vậy, nhưng chúng tôi nhận thấy rằng khi chúng tôi cố mở một tệp PDF trong trình duyệt, nó sẽ đưa ra yêu cầu thứ hai tới địa chỉ được chỉ định ở trên, sau đó nó sẽ tải xuống thư viện gốc. Để thực hiện cuộc tấn công, chúng tôi quyết định sử dụng tính năng này của UC Browser: khả năng mở PDF bằng thư viện gốc, không có trong APK và tải xuống từ Internet nếu cần. Điều đáng chú ý là về mặt lý thuyết, UC Browser có thể bị buộc phải tải xuống nội dung nào đó mà không có sự tương tác của người dùng - nếu bạn cung cấp phản hồi đúng định dạng cho yêu cầu được thực thi sau khi trình duyệt được khởi chạy. Nhưng để làm được điều này, chúng tôi cần nghiên cứu giao thức tương tác với máy chủ chi tiết hơn, vì vậy chúng tôi quyết định rằng việc chỉnh sửa phản hồi bị chặn và thay thế thư viện để làm việc với PDF sẽ dễ dàng hơn.

Vì vậy, khi người dùng muốn mở tệp PDF trực tiếp trong trình duyệt, có thể thấy các yêu cầu sau trong lưu lượng truy cập:

Tìm kiếm lỗ hổng trong UC Browser

Đầu tiên có một yêu cầu POST tới puds.ucweb.com/upgrade/index.xhtml, sau đó
Một kho lưu trữ có thư viện để xem các định dạng PDF và văn phòng sẽ được tải xuống. Thật hợp lý khi giả định rằng yêu cầu đầu tiên truyền thông tin về hệ thống (ít nhất là kiến ​​trúc để cung cấp thư viện được yêu cầu) và để đáp lại yêu cầu đó, trình duyệt sẽ nhận được một số thông tin về thư viện cần được tải xuống: địa chỉ và có thể , thứ gì khác. Vấn đề là yêu cầu này đã được mã hóa.

Đoạn yêu cầu

Đoạn trả lời

Tìm kiếm lỗ hổng trong UC Browser

Tìm kiếm lỗ hổng trong UC Browser

Bản thân thư viện được đóng gói dưới dạng ZIP và không được mã hóa.

Tìm kiếm lỗ hổng trong UC Browser

Tìm kiếm mã giải mã lưu lượng truy cập

Hãy thử giải mã phản hồi của máy chủ. Hãy nhìn vào mã lớp com.uc.deployment.UpgradeDeployService: từ phương thức onStartCommand đi đến com.uc.deployment.bx, và từ đó đến com.uc.browser.core.dcfe:

    public final void e(l arg9) {
int v4_5;
String v3_1;
byte[] v3;
byte[] v1 = null;
if(arg9 == null) {
v3 = v1;
}
else {
v3_1 = arg9.iGX.ipR;
StringBuilder v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]product:");
v4.append(arg9.iGX.ipR);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]version:");
v4.append(arg9.iGX.iEn);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]upgrade_type:");
v4.append(arg9.iGX.mMode);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]force_flag:");
v4.append(arg9.iGX.iEo);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_mode:");
v4.append(arg9.iGX.iDQ);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_type:");
v4.append(arg9.iGX.iEr);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_state:");
v4.append(arg9.iGX.iEp);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_file:");
v4.append(arg9.iGX.iEq);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apk_md5:");
v4.append(arg9.iGX.iEl);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]download_type:");
v4.append(arg9.mDownloadType);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]download_group:");
v4.append(arg9.mDownloadGroup);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]download_path:");
v4.append(arg9.iGH);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_child_version:");
v4.append(arg9.iGX.iEx);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_series:");
v4.append(arg9.iGX.iEw);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_cpu_arch:");
v4.append(arg9.iGX.iEt);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_cpu_vfp3:");
v4.append(arg9.iGX.iEv);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_cpu_vfp:");
v4.append(arg9.iGX.iEu);
ArrayList v3_2 = arg9.iGX.iEz;
if(v3_2 != null && v3_2.size() != 0) {
Iterator v3_3 = v3_2.iterator();
while(v3_3.hasNext()) {
Object v4_1 = v3_3.next();
StringBuilder v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_name:");
v5.append(((au)v4_1).getName());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_ver_name:");
v5.append(((au)v4_1).aDA());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_ver_code:");
v5.append(((au)v4_1).gBl);
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_req_type:");
v5.append(((au)v4_1).gBq);
}
}
j v3_4 = new j();
m.b(v3_4);
h v4_2 = new h();
m.b(v4_2);
ay v5_1 = new ay();
v3_4.hS("");
v3_4.setImsi("");
v3_4.hV("");
v5_1.bPQ = v3_4;
v5_1.bPP = v4_2;
v5_1.yr(arg9.iGX.ipR);
v5_1.gBF = arg9.iGX.mMode;
v5_1.gBI = arg9.iGX.iEz;
v3_2 = v5_1.gAr;
c.aBh();
v3_2.add(g.fs("os_ver", c.getRomInfo()));
v3_2.add(g.fs("processor_arch", com.uc.b.a.a.c.getCpuArch()));
v3_2.add(g.fs("cpu_arch", com.uc.b.a.a.c.Pb()));
String v4_3 = com.uc.b.a.a.c.Pd();
v3_2.add(g.fs("cpu_vfp", v4_3));
v3_2.add(g.fs("net_type", String.valueOf(com.uc.base.system.a.Jo())));
v3_2.add(g.fs("fromhost", arg9.iGX.iEm));
v3_2.add(g.fs("plugin_ver", arg9.iGX.iEn));
v3_2.add(g.fs("target_lang", arg9.iGX.iEs));
v3_2.add(g.fs("vitamio_cpu_arch", arg9.iGX.iEt));
v3_2.add(g.fs("vitamio_vfp", arg9.iGX.iEu));
v3_2.add(g.fs("vitamio_vfp3", arg9.iGX.iEv));
v3_2.add(g.fs("plugin_child_ver", arg9.iGX.iEx));
v3_2.add(g.fs("ver_series", arg9.iGX.iEw));
v3_2.add(g.fs("child_ver", r.aVw()));
v3_2.add(g.fs("cur_ver_md5", arg9.iGX.iEl));
v3_2.add(g.fs("cur_ver_signature", SystemHelper.getUCMSignature()));
v3_2.add(g.fs("upgrade_log", i.bjt()));
v3_2.add(g.fs("silent_install", String.valueOf(arg9.iGX.iDQ)));
v3_2.add(g.fs("silent_state", String.valueOf(arg9.iGX.iEp)));
v3_2.add(g.fs("silent_file", arg9.iGX.iEq));
v3_2.add(g.fs("silent_type", String.valueOf(arg9.iGX.iEr)));
v3_2.add(g.fs("cpu_archit", com.uc.b.a.a.c.Pc()));
v3_2.add(g.fs("cpu_set", SystemHelper.getCpuInstruction()));
boolean v4_4 = v4_3 == null || !v4_3.contains("neon") ? false : true;
v3_2.add(g.fs("neon", String.valueOf(v4_4)));
v3_2.add(g.fs("cpu_cores", String.valueOf(com.uc.b.a.a.c.Jl())));
v3_2.add(g.fs("ram_1", String.valueOf(com.uc.b.a.a.h.Po())));
v3_2.add(g.fs("totalram", String.valueOf(com.uc.b.a.a.h.OL())));
c.aBh();
v3_2.add(g.fs("rom_1", c.getRomInfo()));
v4_5 = e.getScreenWidth();
int v6 = e.getScreenHeight();
StringBuilder v7 = new StringBuilder();
v7.append(v4_5);
v7.append("*");
v7.append(v6);
v3_2.add(g.fs("ss", v7.toString()));
v3_2.add(g.fs("api_level", String.valueOf(Build$VERSION.SDK_INT)));
v3_2.add(g.fs("uc_apk_list", SystemHelper.getUCMobileApks()));
Iterator v4_6 = arg9.iGX.iEA.entrySet().iterator();
while(v4_6.hasNext()) {
Object v6_1 = v4_6.next();
v3_2.add(g.fs(((Map$Entry)v6_1).getKey(), ((Map$Entry)v6_1).getValue()));
}
v3 = v5_1.toByteArray();
}
if(v3 == null) {
this.iGY.iGI.a(arg9, "up_encode", "yes", "fail");
return;
}
v4_5 = this.iGY.iGw ? 0x1F : 0;
if(v3 == null) {
}
else {
v3 = g.i(v4_5, v3);
if(v3 == null) {
}
else {
v1 = new byte[v3.length + 16];
byte[] v6_2 = new byte[16];
Arrays.fill(v6_2, 0);
v6_2[0] = 0x5F;
v6_2[1] = 0;
v6_2[2] = ((byte)v4_5);
v6_2[3] = -50;
System.arraycopy(v6_2, 0, v1, 0, 16);
System.arraycopy(v3, 0, v1, 16, v3.length);
}
}
if(v1 == null) {
this.iGY.iGI.a(arg9, "up_encrypt", "yes", "fail");
return;
}
if(TextUtils.isEmpty(this.iGY.mUpgradeUrl)) {
this.iGY.iGI.a(arg9, "up_url", "yes", "fail");
return;
}
StringBuilder v0 = new StringBuilder("[");
v0.append(arg9.iGX.ipR);
v0.append("]url:");
v0.append(this.iGY.mUpgradeUrl);
com.uc.browser.core.d.c.i v0_1 = this.iGY.iGI;
v3_1 = this.iGY.mUpgradeUrl;
com.uc.base.net.e v0_2 = new com.uc.base.net.e(new com.uc.browser.core.d.c.i$a(v0_1, arg9));
v3_1 = v3_1.contains("?") ? v3_1 + "&dataver=pb" : v3_1 + "?dataver=pb";
n v3_5 = v0_2.uc(v3_1);
m.b(v3_5, false);
v3_5.setMethod("POST");
v3_5.setBodyProvider(v1);
v0_2.b(v3_5);
this.iGY.iGI.a(arg9, "up_null", "yes", "success");
this.iGY.iGI.b(arg9);
}

Chúng ta thấy sự hình thành của một yêu cầu POST ở đây. Chúng tôi chú ý đến việc tạo một mảng 16 byte và điền vào nó: 0x5F, 0, 0x1F, -50 (=0xCE). Trùng hợp với những gì chúng tôi thấy trong yêu cầu ở trên.

Trong cùng một lớp, bạn có thể thấy một lớp lồng nhau có một phương thức thú vị khác:

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

Phương thức này lấy một mảng byte làm đầu vào và kiểm tra xem byte 0 là 60x0 hay byte thứ ba là 0xD1 và byte thứ hai là 11, 0 hoặc 1x0F. Chúng tôi xem xét phản hồi từ máy chủ: byte 60 là 0x1, byte thứ hai là 0x60F, byte thứ ba là XNUMXxXNUMX. Nghe có vẻ như những gì chúng ta cần. Đánh giá theo các dòng (ví dụ: “up_decrypt”), một phương thức nên được gọi ở đây sẽ giải mã phản hồi của máy chủ.
Hãy chuyển sang phương pháp gj. Lưu ý rằng đối số đầu tiên là byte ở offset 2 (tức là 0x1F trong trường hợp của chúng tôi) và đối số thứ hai là phản hồi của máy chủ không có
16 byte đầu tiên.

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

Rõ ràng, ở đây chúng tôi chọn một thuật toán giải mã và cùng một byte trong
trường hợp bằng 0x1F, biểu thị một trong ba tùy chọn có thể.

Chúng tôi tiếp tục phân tích mã. Sau một vài lần nhảy, chúng tôi thấy mình đang ở trong một phương thức có tên dễ hiểu giải mãBytesByKey.

Ở đây, hai byte nữa được tách ra khỏi phản hồi của chúng tôi và một chuỗi được lấy từ chúng. Rõ ràng là theo cách này, khóa giải mã tin nhắn đã được chọn.

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

Nhìn về phía trước, chúng tôi lưu ý rằng ở giai đoạn này, chúng tôi chưa nhận được khóa mà chỉ có “mã định danh” của nó. Lấy chìa khóa phức tạp hơn một chút.

Trong phương thức tiếp theo, hai tham số nữa được thêm vào các tham số hiện có, tạo thành bốn tham số trong số đó: số ma thuật 16, mã định danh khóa, dữ liệu được mã hóa và một chuỗi khó hiểu (trong trường hợp của chúng tôi là trống).

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

Sau một loạt các chuyển đổi, chúng ta đi đến phương pháp staticBinarySafeDecryptNoB64 giao diện com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. Không có lớp nào trong mã ứng dụng chính triển khai giao diện này. Có một lớp như vậy trong tập tin lib/armeabi-v7a/libsgmain.so, thực ra không phải là .so mà là .jar. Phương pháp chúng tôi quan tâm được thực hiện như sau:

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

Ở đây danh sách các tham số của chúng tôi được bổ sung thêm hai số nguyên: 2 và 0. Đánh giá theo
mọi thứ, 2 có nghĩa là giải mã, như trong phương thức doFinal lớp hệ thống javax.crypto.Cipher. Và tất cả điều này được chuyển đến một Bộ định tuyến nhất định có số 10601 - đây rõ ràng là số lệnh.

Sau chuỗi chuyển đổi tiếp theo, chúng ta tìm thấy một lớp thực hiện giao diện IRouterThành phần và phương pháp 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);
}
}

Và cả lớp Thư viện JNIC, trong đó phương thức gốc được khai báo doCommandNative:

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

Điều này có nghĩa là chúng ta cần tìm một phương thức trong mã gốc doCommandNative. Và đây là nơi niềm vui bắt đầu.

Làm xáo trộn mã máy

Trong tập tin libsgmain.so (thực ra là một .jar và trong đó chúng tôi đã tìm thấy cách triển khai một số giao diện liên quan đến mã hóa ở trên) có một thư viện riêng: libsgmamainso-6.4.36.so. Chúng tôi mở nó trong IDA và nhận được một loạt hộp thoại có lỗi. Vấn đề là bảng tiêu đề phần không hợp lệ. Điều này được thực hiện nhằm mục đích làm phức tạp thêm việc phân tích.

Tìm kiếm lỗ hổng trong UC Browser

Nhưng điều đó là không cần thiết: để tải chính xác tệp ELF và phân tích nó, bảng tiêu đề chương trình là đủ. Do đó, chúng tôi chỉ cần xóa bảng phần, loại bỏ các trường tương ứng trong tiêu đề.

Tìm kiếm lỗ hổng trong UC Browser

Mở lại file trong IDA.

Có hai cách để cho máy ảo Java biết chính xác vị trí của thư viện gốc triển khai một phương thức được khai báo trong mã Java là vị trí gốc. Đầu tiên là đặt tên loài Java_package_name_ClassName_MethodName.

Thứ hai là đăng ký nó khi tải thư viện (trong hàm JNI_Đang tải)
sử dụng lệnh gọi hàm Đăng ký.

Trong trường hợp của chúng tôi, nếu chúng tôi sử dụng phương thức đầu tiên, tên sẽ như thế này: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

Không có chức năng nào như vậy trong số các chức năng được xuất, có nghĩa là bạn cần tìm kiếm cuộc gọi Đăng ký.
Hãy đi đến chức năng JNI_Đang tải và chúng ta thấy bức ảnh này:

Tìm kiếm lỗ hổng trong UC Browser

Những gì đang xảy ra ở đây? Thoạt nhìn, phần bắt đầu và kết thúc của hàm này là đặc trưng của kiến ​​trúc ARM. Lệnh đầu tiên trên ngăn xếp lưu trữ nội dung của các thanh ghi mà hàm sẽ sử dụng trong hoạt động của nó (trong trường hợp này là R0, R1 và R2), cũng như nội dung của thanh ghi LR, chứa địa chỉ trả về từ hàm . Lệnh cuối cùng khôi phục các thanh ghi đã lưu và địa chỉ trả về ngay lập tức được đặt vào thanh ghi PC - do đó quay trở lại từ hàm. Nhưng nếu quan sát kỹ, bạn sẽ nhận thấy rằng lệnh áp chót sẽ thay đổi địa chỉ trả về được lưu trên ngăn xếp. Hãy tính xem sau này sẽ như thế nào
thực thi mã. Một địa chỉ nhất định 1xB0 được tải vào R130, 5 được trừ đi, sau đó nó được chuyển sang R0 và 0x10 được thêm vào đó. Hóa ra 0xB13B. Do đó, IDA cho rằng lệnh cuối cùng là một hàm trả về bình thường, nhưng thực tế nó đang đi đến địa chỉ được tính toán là 0xB13B.

Điều đáng nhắc lại ở đây là bộ xử lý ARM có hai chế độ và hai bộ hướng dẫn: ARM và Thumb. Bit ít quan trọng nhất của địa chỉ cho bộ xử lý biết tập lệnh nào đang được sử dụng. Nghĩa là, địa chỉ thực sự là 0xB13A và một trong bit có trọng số thấp nhất biểu thị chế độ Thumb.

Một “bộ điều hợp” tương tự đã được thêm vào đầu mỗi chức năng trong thư viện này và
mã rác. Chúng tôi sẽ không tập trung vào chúng một cách chi tiết hơn - chúng tôi chỉ nhớ
rằng sự khởi đầu thực sự của hầu hết các chức năng còn xa hơn một chút.

Vì mã không nhảy tới 0xB13A một cách rõ ràng nên bản thân IDA cũng không nhận ra rằng mã nằm ở vị trí này. Vì lý do tương tự, nó không nhận ra hầu hết mã trong thư viện là mã, điều này khiến việc phân tích hơi khó khăn. Chúng tôi nói với IDA rằng đây là mã và đây là điều sẽ xảy ra:

Tìm kiếm lỗ hổng trong UC Browser

Bảng rõ ràng bắt đầu ở 0xB144. Có gì trong sub_494C?

Tìm kiếm lỗ hổng trong UC Browser

Khi gọi hàm này trong thanh ghi LR, chúng ta nhận được địa chỉ của bảng được đề cập trước đó (0xB144). Trong R0 - chỉ mục trong bảng này. Tức là giá trị được lấy từ bảng, cộng vào LR và kết quả là
địa chỉ để đi đến. Hãy thử tính toán: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Chúng tôi đi đến địa chỉ đã nhận và xem một số hướng dẫn hữu ích theo đúng nghĩa đen và quay lại 0xB140:

Tìm kiếm lỗ hổng trong UC Browser

Bây giờ sẽ có một quá trình chuyển đổi ở offset có chỉ số 0x20 từ bảng.

Đánh giá theo kích thước của bảng, sẽ có nhiều chuyển đổi như vậy trong mã. Câu hỏi đặt ra là liệu có thể bằng cách nào đó giải quyết vấn đề này một cách tự động hơn mà không cần tính toán địa chỉ theo cách thủ công hay không. Và các tập lệnh cũng như khả năng vá mã trong IDA sẽ hỗ trợ chúng tôi:

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"

Đặt con trỏ trên dòng 0xB26A, chạy tập lệnh và xem quá trình chuyển đổi sang 0xB4B0:

Tìm kiếm lỗ hổng trong UC Browser

IDA lại không công nhận khu vực này là mã. Chúng tôi giúp cô ấy và xem một thiết kế khác ở đó:

Tìm kiếm lỗ hổng trong UC Browser

Những hướng dẫn sau BLX dường như không có nhiều ý nghĩa, nó giống một kiểu dịch chuyển nào đó hơn. Hãy nhìn vào sub_4964:

Tìm kiếm lỗ hổng trong UC Browser

Và thực sự, ở đây một dword được lấy tại địa chỉ nằm trong LR, được thêm vào địa chỉ này, sau đó giá trị tại địa chỉ kết quả được lấy và đưa vào ngăn xếp. Ngoài ra, 4 được thêm vào LR để sau khi trả về từ hàm, phần bù tương tự này sẽ bị bỏ qua. Sau đó lệnh POP {R1} lấy giá trị kết quả từ ngăn xếp. Nếu bạn nhìn vào những gì nằm ở địa chỉ 0xB4BA + 0xEA = 0xB5A4, bạn sẽ thấy một cái gì đó tương tự như một bảng địa chỉ:

Tìm kiếm lỗ hổng trong UC Browser

Để vá thiết kế này, bạn sẽ cần lấy hai tham số từ mã: phần bù và số thanh ghi mà bạn muốn đặt kết quả. Đối với mỗi lần đăng ký có thể, bạn sẽ phải chuẩn bị trước một đoạn mã.

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"

Chúng tôi đặt con trỏ ở đầu cấu trúc mà chúng tôi muốn thay thế - 0xB4B2 - và chạy tập lệnh:

Tìm kiếm lỗ hổng trong UC Browser

Ngoài các cấu trúc đã được đề cập, mã còn chứa các cấu trúc sau:

Tìm kiếm lỗ hổng trong UC Browser

Như trong trường hợp trước, sau lệnh BLX có phần bù:

Tìm kiếm lỗ hổng trong UC Browser

Chúng tôi lấy phần bù vào địa chỉ từ LR, thêm nó vào LR và đến đó. 0x72044 + 0xC = 0x72050. Kịch bản cho thiết kế này khá đơn giản:

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"

Kết quả thực thi script:

Tìm kiếm lỗ hổng trong UC Browser

Sau khi mọi thứ đã được vá trong hàm, bạn có thể trỏ IDA đến điểm bắt đầu thực sự của nó. Nó sẽ ghép tất cả các mã chức năng lại với nhau và có thể được dịch ngược bằng HexRays.

Giải mã chuỗi

Chúng tôi đã học cách giải quyết tình trạng mã máy bị xáo trộn trong thư viện libsgmamainso-6.4.36.so từ UC Browser và nhận được mã chức năng JNI_Đang tải.

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

Chúng ta hãy xem xét kỹ hơn các dòng sau:

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

Trong chức năng sub_73E24 tên lớp rõ ràng đang được giải mã. Là tham số của hàm này, một con trỏ tới dữ liệu tương tự như dữ liệu được mã hóa, một bộ đệm nhất định và một số sẽ được truyền. Rõ ràng, sau khi gọi hàm, sẽ có một dòng được giải mã trong bộ đệm, vì nó được truyền cho hàm TìmLớp, lấy tên lớp làm tham số thứ hai. Do đó, số là kích thước của bộ đệm hoặc độ dài của dòng. Hãy thử giải mã tên lớp, nó sẽ cho chúng ta biết liệu chúng ta có đang đi đúng hướng hay không. Chúng ta hãy xem xét kỹ hơn những gì xảy ra trong phụ_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;
}

Chức năng sub_7AF78 tạo một phiên bản của vùng chứa cho các mảng byte có kích thước được chỉ định (chúng tôi sẽ không đề cập chi tiết đến các vùng chứa này). Ở đây có hai vùng chứa như vậy được tạo: một vùng chứa dòng "DcO/lcK+h?m3c*q@" (rất dễ đoán đây là khóa), khóa kia chứa dữ liệu được mã hóa. Tiếp theo, cả hai đối tượng được đặt trong một cấu trúc nhất định, cấu trúc này được truyền cho hàm phụ_6115C. Chúng ta cũng hãy đánh dấu một trường có giá trị 3 trong cấu trúc này. Hãy xem điều gì sẽ xảy ra với cấu trúc này tiếp theo.

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

Tham số switch là trường cấu trúc trước đó được gán giá trị 3. Xem trường hợp 3: đối với hàm phụ_6364C các tham số được truyền từ cấu trúc đã được thêm vào đó trong hàm trước đó, tức là khóa và dữ liệu được mã hóa. Nếu bạn nhìn kỹ vào phụ_6364C, bạn có thể nhận ra thuật toán RC4 trong đó.

Chúng tôi có một thuật toán và một chìa khóa. Hãy thử giải mã tên lớp. Đây là những gì đã xảy ra: com/taobao/không dây/bảo mật/bộ chuyển đổi/JNICLibrary. Tuyệt vời! Chúng tôi đang đi đúng hướng.

Cây lệnh

Bây giờ chúng ta cần tìm một thử thách Đăng ký, nó sẽ chỉ cho chúng ta hàm doCommandNative. Chúng ta hãy xem các chức năng được gọi từ JNI_OnLoad, và chúng tôi tìm thấy nó trong phụ_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;
}

Và thực sự, một phương thức gốc có tên đã được đăng ký tại đây doCommandNative. Bây giờ chúng tôi biết địa chỉ của anh ấy. Hãy xem anh ấy làm gì.

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

Qua tên, bạn có thể đoán rằng đây là điểm vào của tất cả các chức năng mà các nhà phát triển đã quyết định chuyển sang thư viện gốc. Chúng tôi quan tâm đến chức năng số 10601.

Bạn có thể thấy từ mã lệnh number tạo ra ba số: lệnh/10000, lệnh% 10000/100 и lệnh % 10, tức là trong trường hợp của chúng ta là 1, 6 và 1. Ba số này, cũng như một con trỏ tới JNIEnv và các đối số được truyền cho hàm sẽ được thêm vào một cấu trúc và được truyền đi. Sử dụng ba số thu được (ký hiệu là N1, N2 và N3), một cây lệnh sẽ được xây dựng.

Một cái gì đó như thế này:

Tìm kiếm lỗ hổng trong UC Browser

Cây được điền động vào JNI_Đang tải.
Ba số mã hóa đường đi trong cây. Mỗi lá của cây chứa địa chỉ pocked của hàm tương ứng. Chìa khóa nằm ở nút cha. Không khó để tìm vị trí trong mã nơi hàm chúng ta cần được thêm vào cây nếu bạn hiểu tất cả các cấu trúc được sử dụng (chúng tôi không mô tả chúng để không làm cồng kềnh một bài viết vốn đã khá lớn).

Khó hiểu hơn

Chúng tôi đã nhận được địa chỉ của hàm sẽ giải mã lưu lượng: 0x5F1AC. Nhưng còn quá sớm để vui mừng: các nhà phát triển UC Browser đã chuẩn bị một bất ngờ khác cho chúng ta.

Sau khi nhận được các tham số từ mảng được hình thành trong mã Java, chúng ta nhận được
đến hàm tại địa chỉ 0x4D070. Và ở đây một kiểu mã hóa khác đang chờ chúng ta.

Chúng tôi đặt hai chỉ số trong R7 và R4:

Tìm kiếm lỗ hổng trong UC Browser

Chúng tôi chuyển chỉ số đầu tiên sang R11:

Tìm kiếm lỗ hổng trong UC Browser

Để lấy địa chỉ từ một bảng, hãy sử dụng chỉ mục:

Tìm kiếm lỗ hổng trong UC Browser

Sau khi đi đến địa chỉ đầu tiên, chỉ mục thứ hai nằm trong R4 sẽ được sử dụng. Có 230 phần tử trong bảng.

Phải làm gì về nó? Bạn có thể cho IDA biết đây là một switch: Chỉnh sửa -> Khác -> Chỉ định thành ngữ switch.

Tìm kiếm lỗ hổng trong UC Browser

Mã kết quả là khủng khiếp. Tuy nhiên, khi đi xuyên qua khu rừng rậm của nó, bạn có thể nhận thấy lời gọi đến một chức năng đã quen thuộc với chúng ta phụ_6115C:

Tìm kiếm lỗ hổng trong UC Browser

Có một công tắc trong đó trong trường hợp 3 có sự giải mã bằng thuật toán RC4. Và trong trường hợp này, cấu trúc được truyền cho hàm được điền từ các tham số được truyền cho doCommandNative. Hãy nhớ những gì chúng ta đã có ở đó ma thuậtInt với giá trị 16. Chúng tôi xem xét trường hợp tương ứng - và sau một số lần chuyển đổi, chúng tôi tìm thấy mã mà thuật toán có thể được xác định.

Tìm kiếm lỗ hổng trong UC Browser

Đây là AES!

Thuật toán tồn tại, tất cả những gì còn lại là lấy các tham số của nó: chế độ, khóa và có thể là vectơ khởi tạo (sự hiện diện của nó phụ thuộc vào chế độ hoạt động của thuật toán AES). Cấu trúc với chúng phải được hình thành ở đâu đó trước khi gọi hàm phụ_6115C, nhưng phần mã này đặc biệt dễ bị xáo trộn, vì vậy nảy sinh ý tưởng vá mã sao cho tất cả các tham số của hàm giải mã được kết xuất vào một tệp.

Để không phải viết tất cả mã vá bằng ngôn ngữ hợp ngữ theo cách thủ công, bạn có thể khởi chạy Android Studio, viết một hàm ở đó nhận các tham số đầu vào giống như hàm giải mã của chúng tôi và ghi vào một tệp, sau đó sao chép-dán mã mà trình biên dịch sẽ phát ra.

Những người bạn của chúng tôi trong nhóm UC Browser cũng quan tâm đến sự tiện lợi của việc thêm mã. Chúng ta hãy nhớ rằng ở đầu mỗi chức năng, chúng ta có mã rác có thể dễ dàng thay thế bằng bất kỳ mã nào khác. Rất thuận tiện 🙂 Tuy nhiên, khi bắt đầu hàm đích không có đủ dung lượng cho mã lưu tất cả tham số vào một tệp. Tôi đã phải chia nó thành nhiều phần và sử dụng các khối rác từ các chức năng lân cận. Tổng cộng có bốn phần.

Phần đầu tiên:

Tìm kiếm lỗ hổng trong UC Browser

Trong kiến ​​trúc ARM, bốn tham số chức năng đầu tiên được truyền qua các thanh ghi R0-R3, phần còn lại, nếu có, được truyền qua ngăn xếp. Thanh ghi LR mang địa chỉ trả về. Tất cả điều này cần được lưu lại để hàm có thể hoạt động sau khi chúng ta kết xuất các tham số của nó. Chúng tôi cũng cần lưu tất cả các thanh ghi mà chúng tôi sẽ sử dụng trong quy trình, vì vậy chúng tôi thực hiện PUSH.W {R0-R10,LR}. Trong R7 chúng ta lấy địa chỉ của danh sách các tham số được truyền cho hàm thông qua ngăn xếp.

Sử dụng chức năng mở hãy mở tập tin /data/local/tmp/aes ở chế độ "ab"
tức là để bổ sung. Trong R0, chúng tôi tải địa chỉ của tên tệp, trong R1 - địa chỉ của dòng chỉ chế độ. Và ở đây mã rác kết thúc, vì vậy chúng ta chuyển sang chức năng tiếp theo. Để nó tiếp tục hoạt động, ngay từ đầu chúng tôi đã thực hiện quá trình chuyển đổi sang mã thực của hàm, bỏ qua rác và thay vì rác, chúng tôi thêm phần tiếp theo của bản vá.

Tìm kiếm lỗ hổng trong UC Browser

Kêu gọi mở.

Ba tham số đầu tiên của hàm aes có loại int. Vì lúc đầu chúng ta đã lưu các thanh ghi vào ngăn xếp nên chúng ta có thể chỉ cần truyền hàm fwrite địa chỉ của chúng trên ngăn xếp.

Tìm kiếm lỗ hổng trong UC Browser

Tiếp theo, chúng ta có ba cấu trúc chứa kích thước dữ liệu và một con trỏ tới dữ liệu cho khóa, vectơ khởi tạo và dữ liệu được mã hóa.

Tìm kiếm lỗ hổng trong UC Browser

Cuối cùng, đóng tệp, khôi phục các thanh ghi và chuyển điều khiển sang chức năng thực aes.

Chúng tôi thu thập APK có thư viện đã được vá, ký tên, tải nó lên thiết bị/trình mô phỏng và khởi chạy nó. Chúng tôi thấy rằng kết xuất của chúng tôi đang được tạo và rất nhiều dữ liệu đang được ghi ở đó. Trình duyệt sử dụng mã hóa không chỉ cho lưu lượng truy cập và tất cả mã hóa đều đi qua chức năng được đề cập. Nhưng vì lý do nào đó, dữ liệu cần thiết không có ở đó và yêu cầu được yêu cầu không hiển thị trong lưu lượng truy cập. Để không phải đợi cho đến khi UC Browser quyết định thực hiện yêu cầu cần thiết, hãy lấy phản hồi được mã hóa từ máy chủ nhận được trước đó và vá lại ứng dụng: thêm giải mã vào onCreate của hoạt động chính.

    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

Chúng tôi lắp ráp, ký tên, cài đặt, khởi chạy. Chúng tôi nhận được NullPointerException vì phương thức trả về null.

Trong quá trình phân tích sâu hơn về mã, người ta đã phát hiện ra một hàm có thể giải mã các dòng thú vị: “META-INF/” và “.RSA”. Có vẻ như ứng dụng đang xác minh chứng chỉ của nó. Hoặc thậm chí tạo khóa từ nó. Tôi thực sự không muốn giải quyết những gì đang xảy ra với chứng chỉ, vì vậy chúng tôi sẽ chuyển cho nó chứng chỉ chính xác. Hãy vá dòng được mã hóa để thay vì “META-INF/”, chúng tôi nhận được “BLABLINF/”, tạo một thư mục có tên đó trong APK và thêm chứng chỉ trình duyệt sóc vào đó.

Chúng tôi lắp ráp, ký tên, cài đặt, khởi chạy. Chơi lô tô! Chúng tôi có chìa khóa!

MítM

Chúng tôi đã nhận được một khóa và một vectơ khởi tạo bằng khóa. Hãy thử giải mã phản hồi của máy chủ ở chế độ CBC.

Tìm kiếm lỗ hổng trong UC Browser

Chúng tôi thấy URL lưu trữ, tương tự như MD5, “extract_unzipsize” và một số. Chúng tôi kiểm tra: MD5 của kho lưu trữ giống nhau, kích thước của thư viện đã giải nén giống nhau. Chúng tôi đang cố gắng vá thư viện này và đưa nó vào trình duyệt. Để cho thấy rằng thư viện được vá của chúng tôi đã được tải, chúng tôi sẽ khởi chạy Ý định tạo một SMS có nội dung “PWNED!” Chúng tôi sẽ thay thế hai phản hồi từ máy chủ: puds.ucweb.com/upgrade/index.xhtml và để tải xuống kho lưu trữ. Trong lần đầu tiên, chúng tôi thay thế MD5 (kích thước không thay đổi sau khi giải nén), trong lần thứ hai, chúng tôi cung cấp kho lưu trữ với thư viện đã vá.

Trình duyệt cố gắng tải xuống kho lưu trữ nhiều lần, sau đó nó báo lỗi. Rõ ràng là một cái gì đó
anh ấy không thích. Kết quả của việc phân tích định dạng âm u này, hóa ra máy chủ cũng truyền kích thước của kho lưu trữ:

Tìm kiếm lỗ hổng trong UC Browser

Nó được mã hóa trong LEB128. Sau bản vá, kích thước của kho lưu trữ với thư viện đã thay đổi một chút, do đó trình duyệt cho rằng kho lưu trữ đã được tải xuống không đúng cách và sau nhiều lần thử, nó đã báo lỗi.

Chúng tôi điều chỉnh kích thước của kho lưu trữ... Và – chiến thắng! 🙂 Kết quả có trong video.

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

Hậu quả và phản ứng của nhà phát triển

Theo cách tương tự, tin tặc có thể sử dụng tính năng không an toàn của UC Browser để phân phối và chạy các thư viện độc hại. Các thư viện này sẽ hoạt động trong ngữ cảnh của trình duyệt, vì vậy chúng sẽ nhận được tất cả các quyền hệ thống của nó. Kết quả là khả năng hiển thị các cửa sổ lừa đảo, cũng như quyền truy cập vào các tệp đang hoạt động của con sóc Trung Quốc màu cam, bao gồm thông tin đăng nhập, mật khẩu và cookie được lưu trữ trong cơ sở dữ liệu.

Chúng tôi đã liên hệ với các nhà phát triển của UC Browser và thông báo cho họ về vấn đề chúng tôi tìm thấy, cố gắng chỉ ra lỗ hổng và mức độ nguy hiểm của nó, nhưng họ không thảo luận bất cứ điều gì với chúng tôi. Trong khi đó, trình duyệt tiếp tục phô trương tính năng nguy hiểm của nó một cách rõ ràng. Nhưng một khi chúng tôi tiết lộ chi tiết về lỗ hổng thì không thể bỏ qua như trước được nữa. Ngày 27 tháng XNUMX là
phiên bản mới của UC Browser 12.10.9.1193 đã được phát hành, truy cập máy chủ qua HTTPS: puds.ucweb.com/upgrade/index.xhtml.

Ngoài ra, sau khi “sửa lỗi” và cho đến thời điểm viết bài này, việc cố gắng mở tệp PDF trên trình duyệt sẽ dẫn đến thông báo lỗi với nội dung “Rất tiếc, đã xảy ra lỗi!” Yêu cầu tới máy chủ không được thực hiện khi cố mở tệp PDF nhưng yêu cầu được thực hiện khi trình duyệt được khởi chạy, điều này gợi ý về khả năng tiếp tục tải xuống mã thực thi vi phạm quy tắc của Google Play.

Nguồn: www.habr.com

Thêm một lời nhận xét