在UC浏览器中寻找漏洞


引言


3月底,我们报告说 ,我们发现了一种隐藏的功能,可以在UC浏览器中下载和运行未经验证的代码。 今天,我们将详细分析这种下载的发生方式以及黑客如何将其用于自己的目的。

不久前,UC浏览器得到了广泛的宣传和分发:它使用恶意软件安装在用户的设备上,并以视频文件的形式从各个站点分发(例如,用户以为他们下载了例如色情剪辑,但却收到了与此相关的APK)浏览器),使用令人恐惧的横幅广告,标明浏览器已过时,易受攻击,并且存在类似问题。 VK中的官方UC浏览器小组的主题是用户可以抱怨广告不公平的例子,有很多例子。 2016年,甚至有俄语视频广告 (是的,来自阻止广告的浏览器中的广告)。

在撰写本文时,UC浏览器在Google Play上的安装量已超过500,000,000。 这令人印象深刻-只有Google Chrome浏览器有更多功能。 在评论中,您会看到很多关于广告的投诉,并重定向到Google Play上的某些应用程序。 这就是进行这项研究的原因:我们决定查看UC浏览器是否做得不好。 事实证明,他正在这样做!

该应用程序代码显示了下载和运行可执行代码的能力, 这与在Google Play上发布应用程序的规则相矛盾 。 除了UC Browser下载可执行代码之外,它还使它不安全,可用于进行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执行POST请求,启动后的一段时间内就会在流量中注意到该请求。 作为响应,他可能会收到下载更新或新模块的命令。 在分析过程中,服务器未提供此类命令,但是我们注意到,当尝试在浏览器中打开PDF时,它在上述地址发出了第二个请求,然后下载了本机库。 为了进行攻击,我们决定使用UC浏览器的此功能:使用本机库打开PDF的功能,该本机不在APK中,并且如有必要,可从Internet下载。 值得注意的是,从理论上讲,如果您对浏览器启动后执行的请求给出正确格式的响应,则可以强制UC浏览器下载某些内容而无需用户交互。 但是为此,我们需要更详细地研究与服务器的交互协议,因此我们决定编辑截获的响应并替换用于处理PDF的库更加容易。

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



首先是对puds.ucweb.com/upgrade/index.xhtml的POST请求,之后
下载带有库的存档,以查看PDF和Office格式。 逻辑上是假设在第一个请求中传输了有关系统的信息(至少是体系结构以便提供必要的库),并且响应于此,浏览器收到了一些需要下载的有关库的信息:地址以及可能的其他信息。 问题是此请求已加密。

索取摘要

响应片段







该库本身打包为ZIP格式,未加密。



搜索流量解密码



让我们尝试解密服务器响应。 我们查看com.uc.deployment.UpgradeDeployService类的代码:从onStartCommand方法开始转到com.uc.deployment.bx ,然后从它转到com.uc.browser.core.dcfe

  public final void e(l arg9) { int v4_5; String v3_1; byte[] v3; byte[] v1 = null; if(arg9 == null) { v3 = v1; } else { v3_1 = arg9.iGX.ipR; StringBuilder v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]product:"); v4.append(arg9.iGX.ipR); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]version:"); v4.append(arg9.iGX.iEn); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]upgrade_type:"); v4.append(arg9.iGX.mMode); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]force_flag:"); v4.append(arg9.iGX.iEo); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]silent_mode:"); v4.append(arg9.iGX.iDQ); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]silent_type:"); v4.append(arg9.iGX.iEr); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]silent_state:"); v4.append(arg9.iGX.iEp); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]silent_file:"); v4.append(arg9.iGX.iEq); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]apk_md5:"); v4.append(arg9.iGX.iEl); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]download_type:"); v4.append(arg9.mDownloadType); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]download_group:"); v4.append(arg9.mDownloadGroup); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]download_path:"); v4.append(arg9.iGH); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]apollo_child_version:"); v4.append(arg9.iGX.iEx); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]apollo_series:"); v4.append(arg9.iGX.iEw); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]apollo_cpu_arch:"); v4.append(arg9.iGX.iEt); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]apollo_cpu_vfp3:"); v4.append(arg9.iGX.iEv); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]apollo_cpu_vfp:"); v4.append(arg9.iGX.iEu); ArrayList v3_2 = arg9.iGX.iEz; if(v3_2 != null && v3_2.size() != 0) { Iterator v3_3 = v3_2.iterator(); while(v3_3.hasNext()) { Object v4_1 = v3_3.next(); StringBuilder v5 = new StringBuilder("["); v5.append(((au)v4_1).getName()); v5.append("]component_name:"); v5.append(((au)v4_1).getName()); v5 = new StringBuilder("["); v5.append(((au)v4_1).getName()); v5.append("]component_ver_name:"); v5.append(((au)v4_1).aDA()); v5 = new StringBuilder("["); v5.append(((au)v4_1).getName()); v5.append("]component_ver_code:"); v5.append(((au)v4_1).gBl); v5 = new StringBuilder("["); v5.append(((au)v4_1).getName()); v5.append("]component_req_type:"); v5.append(((au)v4_1).gBq); } } j v3_4 = new j(); mb(v3_4); h v4_2 = new h(); mb(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.baacgetCpuArch())); v3_2.add(g.fs("cpu_arch", com.uc.baacPb())); String v4_3 = com.uc.baacPd(); 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.baacPc())); 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.baacJl()))); v3_2.add(g.fs("ram_1", String.valueOf(com.uc.baahPo()))); v3_2.add(g.fs("totalram", String.valueOf(com.uc.baahOL()))); 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 = gi(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.dci 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.dci$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); mb(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 = gj(arg11[2], v7); } if(v7 == null) { goto label_57; } if(v5 != 0) { v1_1 = gP(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 = gb(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”)来看,应该在此处调用一种解密服务器响应的方法。
我们传递给gj方法。 请注意,偏移量为2的字节(在本例中为0x1F)作为第一个参数传输到该字节,服务器响应不包含
前16个字节。

  public static byte[] j(int arg1, byte[] arg2) { if(arg1 == 1) { arg2 = cc(arg2, c.adu); } else if(arg1 == 11) { arg2 = m.aF(arg2); } else if(arg1 != 0x1F) { } else { arg2 = EncryptHelper.decrypt(arg2); } return arg2; } 

显然,可以选择解密算法和相同的字节,
case为0x1F,表示三个可能的选项之一。

我们继续分析代码。 经过几次跳转后,我们进入了一个具有对话名的方法cryptoBytesByKey

在这里,另外两个字节与我们的答案分开,并从中获得一个字符串。 显然,以这种方式选择了密钥来解密消息。

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

经过一系列转换之后,我们到达com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent接口的staticBinarySafeDecryptNoB64方法 主应用程序代码中没有实现该接口的类。 这样的类在文件lib / armeabi-v7a / libsgmain.so中 ,该文件实际上不是.so,而是.jar。 我们感兴趣的方法实现如下:

 package com.alibaba.wireless.security.ai; // ... 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系统类的doFinal方法一样。 所有这些都被转移到编号为10601的某个路由器-显然,这是命令编号。

在下一个过渡链之后,我们找到一个实现IRouterComponent接口和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); } } 

还有JNICLibrary类,其中声明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中将其打开,并获得一堆带有错误的对话框。 问题在于节头表无效。 这样做是为了使分析复杂化。



但这也不是必需的:为了正确加载ELF文件并进行分析,程序头表就足够了。 因此,我们只需删除节表,即可使标题中的相应字段无效。



再次在IDA中打开文件。

有两种方法可以准确地告知Java虚拟机本机库中Java代码中声明为本机的方法的实现位于何处。 首先是给它一个名称,形式为Java_package_name_ClassName_Method_name

第二种是在加载库时注册它(在JNI_OnLoad函数中)
通过调用RegisterNatives函数。

在我们的情况下,如果使用第一种方法,则名称应类似于: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative

在导出的函数中,没有这样的函数,因此您需要查找对RegisterNatives的调用。
我们转到JNI_OnLoad函数并查看下图:



这是怎么回事 乍一看,该功能的开始和结束是ARM体系结构的典型特征。 堆栈上的第一条指令存储函数将在其工作中使用的寄存器的内容(在本例中为R0,R1和R2),以及寄存器LR的内容,其中包含函数的返回地址。 使用最后一条指令,将恢复保存的寄存器,并将返回地址立即放置在PC寄存器中,从而从函数返回。 但是,如果仔细观察,您会发现倒数第二条指令会更改存储在堆栈中的返回地址。 我们计算之后
代码执行。 将某个地址0xB130装入R1,从中减去5,然后将其传送到R0,并向其中添加0x10。 原来是0xB13B。 因此,IDA认为在最后一条指令中,该函数有正常返回,但实际上存在到计算地址0xB13B的转换。

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

在该库中每个函数的开头都添加了一个类似的“适配器”,
垃圾代码。 此外,我们不会详细介绍它们-我们只记得
几乎所有功能的真正开始还有一点。

由于代码中没有明确过渡到0xB13A,因此IDA本身无法识别该代码在该位置。 由于相同的原因,它无法将库中的大多数代码识别为代码,这使得分析有些困难。 我们告诉IDA代码在这里,结果如下:



表格从0xB144开始。 那sub_494C呢?



在LR寄存器中调用此函数时,我们将获得上述表的地址(0xB144)。 在R0中,此表中的索引。 也就是说,从表中获取一个值,将其添加到LR并获得
地址去。 让我们尝试计算它:0xB144 + [0xB144 + 8 * 4] = 0xB144 + 0x120 = 0xB264。 我们转到接收到的地址,仅看到一些有用的说明,然后再次转换到0xB140:



现在,表中的索引偏移量为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的过渡:



IDA再次没有将该站点识别为代码。 我们帮助她,并在那里看到另一种建筑:



BLX之后的指令看起来不是很有意义,更像是某种偏见。 我们看一下sub_4964:



实际上,这里的双字是在位于LR中的地址处提取的,并添加到该地址,然后在接收到的地址处获取值并将其压入堆栈。 另外,在LR中添加了4,以便在从函数返回后跳转相同的偏移量。 然后POP {R1}命令从堆栈中提取接收到的值。 如果查看地址0xB4BA + 0xEA = 0xB5A4,则会看到类似于地址表的内容:



要修补此设计,您需要从代码中获取两个参数:偏移量和要将结果放入其中的寄存器号。 对于每个可能的寄存器,您都必须事先准备一段代码。

 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] == '\x5d\xf8\x04': 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-并运行脚本:



除了代码中已经提到的构造之外,这里还有:



与前面的情况一样,在BLX指令之后,有一个偏移量:



我们将偏移量移到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" 

脚本的结果:



在功能中修补了所有内容之后,您可以将IDA指向其真正的起点。 它会分段收集所有功能代码,并且可以使用HexRays对其进行反编译。

解码字符串


我们从UC浏览器的libsgmainso-6.4.36.so库中了解了如何处理机器代码混淆,并获得了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函数显式解密类名称。作为此函数的参数,传递了类似于加密数据的指针,缓冲区和数字。显然,在调用该函数之后,缓冲区将包含一条解密的行,因为它将传递给FindClass函数,该函数将类名作为第二个参数。因此,数字是缓冲区的大小或字符串的长度。让我们尝试破译类的名称,它应该告诉我们我们是否朝着正确的方向前进。让我们仔细看看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; } 

switch , 3. case 3: sub_6364C , , . . . sub_6364C , RC4.

. . : com/taobao/wireless/security/adapter/JNICLibrary . 太好了! .


RegisterNatives , doCommandNative . , 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; } 

实际上,这里注册了一个名为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.

, : command / 10000 , command % 10000 / 100 command % 10 , . ., , 1, 6 1. , JNIEnv , , . ( N1, N2 N3) .

:



JNI_OnLoad .
. . — . , , , ( , ).


, : 0x5F1AC. : UC Browser .

, Java-,
0x4D070. .

R7 R4 :



R11:



, :



, R4. 230 .

怎么办呢? IDA, switch: Edit -> Other -> Specify switch idiom.



. , , sub_6115C :



switch, case 3 RC4. , doCommandNative . , magicInt 16. case – , .



AES!

, : , , , ( AES). - sub_6115C , , , .


为了不以汇编语言手动编写所有补丁代码,您可以运行Android Studio,在其中编写一个函数以接收与我们的解密函数相同的参数,然后将其写入文件中,然后将其复制粘贴到编译器将生成的代码中。

我们来自UC浏览器团队的朋友也“注意”添加代码的便利性。我们记得在每个函数的开头,我们都有垃圾代码,可以很容易地将其替换为其他代码。非常方便:)没错,在目标函数的开头,没有足够的空间来存储将所有参数保存到文件的代码。我必须将其分成多个部分并使用相邻函数的垃圾块。共有四个部分。

第一部分:



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

使用fopen功能,以“ ab”模式打开文件/ data / local / tmp / aes
即添加。在R0中,我们加载文件名的地址,在R1中-指示模式的行的地址。然后垃圾代码结束,因此转到下一个函数。为了使它继续工作,我们首先绕过了垃圾转移到实函数代码的过渡,并添加了补丁的延续来代替垃圾。



我们称fopenaes

函数的前三个参数的类型为int。由于我们从一开始就将寄存器保存到堆栈中,因此我们可以简单地将堆栈上的地址传递给fwrite函数接下来,我们有三个结构,其中包含数据的大小以及指向密钥,初始化向量和加密数据的数据的指针。







, 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 .

, , , . 宾果! !

MitM


, . CBC.



URL , - MD5, «extract_unzipsize» . : MD5 , . . , , Intent «PWNED!». : puds.ucweb.com/upgrade/index.xhtml . MD5 ( ), .

, . , -
他不喜欢 分析这种混乱格式的结果是,服务器仍在传输归档文件的大小:



它是用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.

Source: https://habr.com/ru/post/zh-CN451936/


All Articles