寻找UC浏览器的漏洞

寻找UC浏览器的漏洞

介绍

三月底我们 报道发现了在 UC 浏览器中下载和运行未经验证的代码的隐藏可能性。 今天我们将详细分析这种下载是如何发生的,以及黑客如何利用它来达到自己的目的。

不久前,UC 浏览器的广告和分发非常激进:它使用恶意软件安装在用户的设备上,并以视频文件的形式从各个网站分发(即,用户认为他们正在下载色情视频,但实际上是这样)而是通过该浏览器收到 APK),使用可怕的横幅,其中包含浏览器已过时、易受攻击等消息。 VK 上的官方 UC 浏览器群组有 主题,其中用户可以投诉不公平广告的例子有很多。 2016年甚至还有 视频广告 俄语(是的,阻止广告的浏览器的广告)。

截至撰写本文时,UC 浏览器在 Google Play 上的安装量已超过 500 亿次。 这令人印象深刻 - 只有 Google Chrome 拥有更多。 在评论中,你可以看到相当多关于广告和重定向到 Google Play 上某些应用程序的投诉。 这就是这项研究的原因:我们决定看看 UC 浏览器是否做了坏事。 事实证明确实如此!

应用程序代码找到了下载和运行可执行代码的能力, 这违反了应用程序发布规则 在 Google Play 上。 除了下载可执行代码之外,UC 浏览器还以不安全的方式执行此操作,可用于发起 MitM 攻击。 让我们看看是否可以进行这样的攻击。

下面写的所有内容都与研究时 Google Play 上存在的 UC 浏览器版本相关:

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

攻击向量

在 UC 浏览器清单中,您可以找到具有明显名称的服务 com.uc.deployment.UpgradeDeployService.

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

当该服务启动时,浏览器会向 puds.ucweb.com/upgrade/index.xhtml,可以在开始后一段时间在交通中看到。 作为响应,他可以接收下载更新或新模块的命令。 在分析过程中,服务器没有发出此类命令,但我们注意到,当尝试在浏览器中打开 PDF 浏览器时,它会向上述地址发出第二次请求,然后下载本机库。 为了实施攻击,我们决定使用 UC 浏览器的此功能:能够使用 APK 中不存在的本机库打开 PDF,必要时可以从 Internet 下载该库。 值得注意的是,理论上,如果您对浏览器启动后执行的请求给出格式正确的响应,则 UC 浏览器可以在没有用户交互的情况下强制下载某些内容。 但为此,我们需要更详细地研究与服务器交互的协议,因此我们决定编辑截获的响应并替换用于处理 PDF 的库会更容易。

因此,当用户想要直接在浏览器中打开 PDF 时,可以在流量中看到以下请求:

寻找UC浏览器的漏洞

第一个是 POST 请求 puds.ucweb.com/upgrade/index.xhtml之后
下载带有用于查看 PDF 和 Office 格式的库的存档。 可以合理地假设,有关系统的信息在第一个请求中传输(至少是提供所需库的体系结构),并且作为响应,浏览器会收到有关需要下载的库的一些信息:地址,可能还有其他东西。 问题是这个请求是加密的。

请求片段

答案片段

寻找UC浏览器的漏洞

寻找UC浏览器的漏洞

该库本身打包在 ZIP 中且未加密。

寻找UC浏览器的漏洞

搜索流量解密代码

让我们尝试破译服务器的响应。 看类代码 com.uc.deployment.UpgradeDeployService:来自方法 启动命令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。 我们看一下服务器的响应:第0个字节是60x0,第二个是1x0F,第三个是60xXNUMX。 看起来像我们需要的。 从这些行(例如“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;
}

显然,这里选择了解密算法,并且与我们的解密算法中的字节相同
case 等于 0x1F,表示三个可能选项之一。

我们继续分析代码。 经过几次跳转后,我们进入了一个有名字的方法 通过密钥解密字节.

在这里,我们的响应中又分离出了两个字节,并从中获取了一个字符串。 很明显,通过这种方式选择密钥来解密消息。

    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 的路由器 - 这显然是命令编号。

在下一个转换链之后,我们找到一个实现该接口的类 IRouter组件 和方法 执行命令:

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

还有班级 JNIC库,其中声明了本机方法 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 虚拟机在本机库中确切的位置是在 Java 代码中声明为本机的方法的实现。 首先是给它起一个物种名称 Java_package_name_ClassName_MethodName.

第二种是在加载库时注册它(在函数中 JNI_加载)
通过函数调用 注册本地人.

在我们的例子中,如果我们使用第一种方法,名称应该是这样的: java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

导出的函数中没有这个函数,所以需要寻找调用 注册本地人.
让我们进入函数 JNI_加载 我们看到这张图片:

寻找UC浏览器的漏洞

这里发生了什么? 乍一看,函数的开头和结尾是 ARM 架构的典型特征。 堆栈上的第一条指令保存函数在其工作中将使用的寄存器的内容(在本例中为 R0、R1 和 R2),以及 LR 寄存器的内容,其中包含函数的返回地址。 最后一条指令恢复保存的寄存器,返回地址立即放入PC寄存器中——这就是函数返回的方式。 但如果仔细观察,您可以看到倒数第二条指令更改了堆栈中存储的返回地址。 计算一下之后会是什么样子
代码执行。 将某个地址1xB0加载到R130中,从中减去5,然后将其移至R0并添加0x10。 结果是0xB13B。 因此,IDA 认为在最后一条指令中,函数正常返回,但实际上跳转到了计算地址 0xB13B。

这里值得回顾的是,ARM 处理器有两种模式和两种指令集:ARM 和 Thumb。 地址的最低有效位告诉处理器正在使用哪个指令集。 也就是说,地址实际上是0xB13A,最低有效位的XNUMX表示Thumb模式。

类似的“适配器”被添加到该库中每个函数的开头,并且
垃圾代码。 我们不会进一步讨论它们 - 只要记住这一点
几乎所有功能的真正开始都有点远。

由于代码中没有显式跳转到 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 中的地址处取出一个双字,添加到该地址,然后在接收到的地址处取出该值并将其放入堆栈中。 另外,LR 中添加了 4,以便在从函数返回后跳过这个偏移量。 之后,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 中获取地址处的偏移量,将其添加到 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_加载.

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 类名被显式解码。 作为该函数的参数,传递类似于加密数据的数据指针、特定缓冲区和数字。 显然,调用函数后,缓冲区将包含解码后的字符串,因为它被传递给函数 查找类,它将类名作为第二个参数。 所以这个数字就是缓冲区的大小或字符串的长度。 让我们尝试破译类名,它应该告诉我们我们的方向是否正确。 让我们仔细看看发生了什么 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;
}

功能 子_7AF78 为指定大小的字节数组创建一个容器实例(我们不会详细讨论这些容器)。 此处创建了两个这样的容器:线路放置在一个容器中 “DcO/lcK+h?m3c*q@” (很容易猜到这就是密钥),在另一个-加密数据。 此外,这两个对象都被放置在特定的结构中,该结构被传递给函数 子_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;
}

作为 switch 参数,传递了一个结构体字段,该字段之前被赋值为 3。我们看情况 3:进入函数 子_6364C 参数是从上一个函数中添加的结构传递的,即密钥和加密数据。 如果你仔细观察 子_6364C,你可以认出里面的RC4算法。

我们有一个算法和一个密钥。 让我们尝试破译类名。 事情是这样的: com/taobao/wireless/security/adapter/JNICLibrary。 伟大的! 我们走在正确的轨道上。

命令树

现在我需要找到一个挑战 注册本地人,这将向我们指出该函数 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。这三个数字以及指向 JNI环境 传递给函数的参数将添加到结构中并继续传递。 借助获得的三个数字(我们用 N1、N2 和 N3 表示它们),构建了一个命令树。

像这样的东西:

寻找UC浏览器的漏洞

树是动态填充的 JNI_加载.
这三个数字对树中的路径进行编码。 树的每个叶子都包含相应函数的地址。 关键在父节点中。 如果您了解所使用的所有结构,那么在代码中找到将我们需要的函数添加到树中的位置并不困难(没有给出它们的描述,以免使已经相当大的文章膨胀)。

更多混淆

我们收到了应该解密流量的函数的地址:0x5F1AC。 但现在高兴还为时过早:UC浏览器的开发者又为我们准备了另一个惊喜。

从Java代码中形成的数组接收参数后,我们得到
到 0x4D070 处的函数。 而这里我们正在等待另一种类型的代码混淆。

我们在 R7 和 R4 中放置两个索引:

寻找UC浏览器的漏洞

我们将第一个索引移至 R11:

寻找UC浏览器的漏洞

要从表中获取地址,请使用索引:

寻找UC浏览器的漏洞

跳转到第一个地址后,使用第二个索引,该索引位于 R4 中。 表中有230个元素。

该怎么办? 你可以告诉 IDA 这是这样一个开关:编辑 -> 其他 -> 指定开关习惯用法。

寻找UC浏览器的漏洞

结果代码很糟糕。 但是,当我们穿过丛林时,您可以注意到对已经熟悉的函数的调用 子_6115C:

寻找UC浏览器的漏洞

有一个开关,其中在情况 3 中使用 RC4 算法进行解密。 在这种情况下,传递给函数的结构由传递给的参数填充 doCommandNative。 我们记得我们有过 魔法整数 值为 16。我们查看相应的情况 - 经过几次转换后,我们找到了可以识别算法的代码。

寻找UC浏览器的漏洞

是AES!

有一种算法,它仍然需要获取其参数:模式、密钥以及可能的初始化向量(它的存在取决于 AES 算法的操作模式)。 必须在函数调用之前的某个地方形成包含它们的结构 子_6115C,但这部分代码被混淆得特别好,因此产生了修补代码的想法,以便将解密函数的所有参数都转储到文件中。

补丁

为了不用手动用汇编语言编写整个补丁代码,您可以运行Android Studio,在那里编写一个函数,该函数接收与我们的解密函数相同的参数作为输入,并写入文件,然后复制粘贴生成的代码由编译器。

我们 UC 浏览器团队的朋友也很关心添加代码的便利性。 我们记得在每个函数的开头都有垃圾代码,可以轻松地用任何其他代码替换。 非常方便 🙂 但是,在目标函数的开头,没有足够的空间用于将所有参数保存到文件的代码。 我必须将其分成几个部分并使用相邻函数的垃圾块。 总共有四个部分。

第一部分:

寻找UC浏览器的漏洞

在ARM架构中,函数的前四个参数通过寄存器R0-R3传递,其余的(如果有的话)通过堆栈传递。 返回地址在LR寄存器中传输。 所有这些都必须保存,以便在我们转储其参数后该函数可以工作。 我们还需要保存在此过程中将使用的所有寄存器,因此我们执行 PUSH.W {R0-R10,LR}。 在 R7 中,我们通过堆栈获取传递给函数的参数列表的地址。

使用功能 打开 打开文件 /数据/本地/tmp/aes 在“ab”模式下,
即用于添加。 在 R0 中我们加载文件名的地址,在 R1 中加载指示模式的行的地址。 垃圾代码到这里就结束了,所以我们继续执行下一个函数。 为了让它继续工作,我们在一开始就将过渡到函数的实际代码,绕过垃圾,而不是垃圾,我们添加了补丁的延续。

寻找UC浏览器的漏洞

打电话 打开.

函数的前三个参数 AES 属于类型 INT。 由于我们一开始就将寄存器保存在堆栈上,因此我们可以简单地传递函数 它们在堆栈上的地址。

寻找UC浏览器的漏洞

接下来,我们有三个结构,其中包含数据的大小和密钥的数据指针、初始化向量和加密数据。

寻找UC浏览器的漏洞

最后,我们关闭文件,恢复寄存器并将控制权转移给真正的函数 AES.

我们使用修补后的库组装 APK,对其进行签名,将其上传到设备/模拟器,然后启动它。 我们看到我们的转储正在创建,并且大量数据正在写入那里。 浏览器不仅对流量使用加密,而且所有加密都会通过相关功能。 但由于某种原因,没有必要的数据,并且必要的请求在流量中不可见。 为了不等到 UC 浏览器设计发出必要的请求,我们将从之前收到的服务器的加密响应中获取并再次修补应用程序:将解密添加到主活动的 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 中创建一个具有该名称的文件夹,并在其中添加白色浏览器证书。

我们收集、签名、安装、启动。 答对了! 我们有钥匙!

MITM

我们得到了一个密钥和一个等于该密钥的初始化向量。 让我们尝试以 CBC 模式解密服务器响应。

寻找UC浏览器的漏洞

我们看到存档的 URL,类似于 MD5、“extract_unzipsize”和一个数字。 我们检查:存档的 MD5 是否匹配、解压库的大小是否匹配。 我们正在尝试修补这个库并将其提供给浏览器。 为了表明我们的修补库已加载,我们将启动一个意图来创建一条带有文本“PWNED!”的短信。 我们将替换来自服务器的两个响应: puds.ucweb.com/upgrade/index.xhtml 并下载存档。 在第一个中,我们替换 MD5(解压后大小不会改变),在第二个中,我们为存档提供修补后的库。

浏览器尝试多次下载存档,然后出现错误。 显然有什么
他不喜欢。 对这种混乱格式的分析结果表明,服务器还传输了存档的大小:

寻找UC浏览器的漏洞

它以 LEB128 编码。 打补丁后,带库的压缩包大小略有变化,因此浏览器认为压缩包下载歪了,多次尝试后报错。

我们纠正了档案的大小......并且 - 胜利! 🙂 视频结果。

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

后果和开发者反应

同样,黑客也可以利用UC浏览器的不安全特性来传播和运行恶意库。 这些库将在浏览器的上下文中运行,因此它们将拥有其所有系统权限。 因此,能够显示网络钓鱼窗口,以及访问橙色中国松鼠的工作文件,包括存储在数据库中的登录名、密码和 cookie。

我们联系了UC浏览器的开发人员,向他们通报了我们发现的问题,试图指出该漏洞及其危险,但他们没有与我们讨论任何内容。 与此同时,浏览器继续在众目睽睽之下炫耀这一危险功能。 但一旦我们披露了该漏洞的详细信息,就不能再像以前那样忽视它了。 27 月 XNUMX 日是
UC浏览器12.10.9.1193新版本发布,通过HTTPS访问服务器: puds.ucweb.com/upgrade/index.xhtml.

此外,在“修复”之后直至撰写本文时,尝试在浏览器中打开 PDF 会导致一条错误消息:“哎呀,出了点问题!”。 尝试打开 PDF 时向服务器发出的请求并未执行,但在启动浏览器时发出了请求,这暗示了继续下载可执行代码的能力,违反了 Google Play 规则。

来源: habr.com

添加评论