📜 ⬆️ ⬇️

We are looking for vulnerabilities in UC Browser


Introduction


At the end of March, we reported that we discovered the hidden possibility of downloading and running untested code in the UC Browser. Today we will examine in detail how this download happens and how hackers can use it for their own purposes.

Some time ago, UC Browser was advertised and distributed very aggressively: it was installed on users' devices using malicious programs, distributed from various sites under the guise of video files (i.e., users thought they downloaded, for example, pornography, and received an APK with this browser), used frightening banners with messages that the browser is outdated, vulnerable and stuff like that. In the official UC Browser group in VK there is a topic in which users can complain about unfair advertising, there are many examples. In 2016, there was even video advertising in Russian (yes, advertising ads that block ads).

At the time of this writing, UC Browser has accumulated over 500,000,000 installations on Google Play. This is impressive - only Google Chrome. Among the reviews, you can see a lot of complaints about advertising and redirects to some applications on Google Play. This was the reason for the study: we decided to see if the UC Browser is doing something wrong. And it turned out that it does!

The application code found the ability to download and run executable code, which is contrary to the rules for publishing applications on Google Play. Besides the fact that the UC Browser downloads the executable code, it makes it insecure, which can be used to conduct a MitM attack. Let's see if we can make such an attack.
')
Everything that is written below is relevant for the UC Browser version that was present on Google Play at the time of the research:

package: com.UCMobile.intl versionName: 12.10.8.1172 versionCode: 10598 sha1 APK-: f5edb2243413c777172f6362876041eb0c3a928c 

Attack vector


In the UC Browser manifest, you can find a service with the talking name com.uc.deployment.UpgradeDeployService .

  <service android:exported="false" android:name="com.uc.deployment.UpgradeDeployService" android:process=":deploy" /> 

When you start this service, the browser performs a POST request to puds.ucweb.com/upgrade/index.xhtml , which can be seen in traffic some time after the start. In response, he may receive a command to download any update or new module. In the course of the analysis, the server did not give such commands, but we noticed that when trying to open a PDF browser in the browser, it makes a repeated request to the above address, after which it downloads the native library. For the attack, we decided to use this feature of UC Browser: the ability to open PDF using the native library, which is not in the APK and which, if necessary, it loads from the Internet. It is worth noting that, theoretically, UC Browser can be made to download something without user interaction - if you give a properly formed response to a request that is executed after the browser is started. But for this, we need to study the interaction protocol with the server in more detail, so we decided that it was easier to edit the intercepted response and replace the library for working with PDF.

So, when the user wants to open the PDF directly in the browser, in traffic you can see the following requests:



First there is a POST request to puds.ucweb.com/upgrade/index.xhtml , after which
An archive with a library for viewing PDF and office formats is being downloaded. It is logical to assume that the first request sends information about the system (at least, the architecture to give the necessary library), and in response the browser receives some information about the library that needs to be downloaded: the address and perhaps something else. The problem is that this request is encrypted.

Query fragment

Response fragment







The library itself is packaged in ZIP and is not encrypted.



Search traffic decryption code



Let's try to decrypt the server response. We look at the code of the com.uc.deployment.UpgradeDeployService class: from the onStartCommand method , go to com.uc.deployment.bx , and from it to 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); } 

We see here the formation of a POST request. We pay attention to the creation of an array of 16 bytes and its filling: 0x5F, 0, 0x1F, -50 (= 0xCE). Same as what we saw in the query above.

In the same class you can notice the nested class, in which there is another interesting method:

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

The method takes an array of bytes as input and checks that the zero byte is 0x60 or the third byte is 0xD0 and the second byte is 1, 11, or 0x1F. We look at the answer from the server: zero byte - 0x60, the second - 0x1F, the third - 0x60. It looks like what we need. Judging by the lines (“up_decrypt”, for example), a method should be invoked here that will decode the server response.
Go to the gj method. Note that, as the first argument, it sends a byte at offset 2 (i.e., 0x1F in our case), and as the second, the server’s response without
first 16 bytes.

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

Obviously, there is a choice of decryption algorithm, and the same byte, which in our
The case is equal to 0x1F, indicates one of three possible options.

We continue the analysis of the code. After a couple of jumps, we fall into the method with the speaking name decryptBytesByKey .

Here two more bytes are separated from our answer, and a string is obtained from them. It is clear that in this way the key is chosen to decrypt the message.

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

Looking ahead, we note that at this stage it is not the key that is obtained, but only its “identifier”. Getting the key is a little more complicated.

In the following method, two more are added to the existing parameters, and there are four of them: the magic number 16, the key identifier, the encrypted data and an incomprehensible string (in our case, empty).

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

After a series of transitions, we arrive at the static.binarySafeDecryptNoB64 method of the interface com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. In the main application code there are no classes that implement this interface. This class is in the file lib / armeabi-v7a / libsgmain.so , which is not really .so, but .jar. The method of interest is implemented as follows:

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

Here our parameter list is complemented by two more integers: 2 and 0. Judging by
2 means decoding, as in the doFinal method of the javax.crypto.Cipher system class. And all this is transferred to a certain Router with the number 10601 - this is, apparently, the command number.

After the next chain of transitions we find a class that implements the IRouterComponent interface and the doCommand method:

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

As well as the JNICLibrary class, in which the native doCommandNative method is declared :

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

So, we need to find the doCommandNative method in the native code. And here the fun begins.

Machine code obfuscation


There is one native library in libsgmain.so (which is actually a .jar and in which we have just found an implementation of some interfaces related to encryption): libsgmainso-6.4.36.so . Open it in IDA and get a bunch of dialog boxes with errors. The problem is that the section header table is invalid. This is done specifically to complicate the analysis.



But it is not needed: to correctly load the ELF file and analyze it, the program header table is enough. Therefore, we simply delete the table of sections, nulling the corresponding fields in the header.



Open the file in IDA again.

There are two ways to tell the Java virtual machine exactly where the native library contains the implementation of the method declared in native Java code. The first is to give it the name of the form Java_package_name_Class_NameMethod .

The second is to register it when loading the library (in the JNI_OnLoad function)
by calling the RegisterNatives function.

In our case, if we use the first method, the name should be like this: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative .

There is no such function among the exported functions, which means that you need to look for a call to RegisterNatives .
Go to the JNI_OnLoad function and see the following picture:



What's going on here? At first glance, the beginning and end of the function are typical of the ARM architecture. The first instruction on the stack stores the contents of the registers that the function will use in its work (in this case, R0, R1 and R2), as well as the contents of the LR register, which contains the return address from the function. With the last instruction, the saved registers are restored, and the return address is immediately placed in the PC register, thus returning from the function. But if you take a closer look, you will notice that the penultimate instruction changes the return address stored on the stack. We calculate what it will be after
code execution. A certain address 0xB130 is loaded into R1, 5 is subtracted from it, then it is transferred to R0 and 0x10 is added to it. It turns out 0xB13B. Thus, IDA thinks that in the last instruction a normal return from a function occurs, and in fact there is a transition to the calculated address 0xB13B.

It is worth recalling that ARM processors have two modes and two sets of instructions: ARM and Thumb. The lower bit of the address tells the processor which instruction set is being used. That is, the address is actually 0xB13A, and the one in the low-order bit indicates the Thumb mode.

At the beginning of each function in this library, a similar “adapter” was added and
trash code. Further we will not dwell on them - just remember
that the present beginning of almost all functions is a little further.

Since there is no explicit transition to 0xB13A in the code, IDA itself did not recognize that the code is located in this place. For the same reason, it does not recognize most of the code in the library as code, which makes analysis a little difficult. We say IDA, what is the code, and this is what happens:



At 0xB144, the table clearly begins. What about sub_494C?



When calling this function in the LR register, we get the address of the previously mentioned table (0xB144). In R0, the index in this table. That is, the value from the table is taken, added to the LR and it turns out
address to go to. Let's try to calculate it: 0xB144 + [0xB144 + 8 * 4] = 0xB144 + 0x120 = 0xB264. Go to the received address and see literally a couple of useful instructions and again go to 0xB140:



Now there will be a transition by offset with index 0x20 from the table.

Judging by the size of the table, there will be many such transitions in the code. The question arises whether it is possible to somehow deal with this more automatically, without manually calculating addresses. And we come to the aid of scripts and the ability to patch code in 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" 

Put the cursor on the line 0xB26A, run the script and see the transition to 0xB4B0:



IDA again did not recognize this site as a code. We help her and see another construction there:



Instructions after BLX do not look very meaningful, it is more like some kind of offset. We look in sub_4964:



And indeed, here dword is taken at the address lying in the LR, is added to this address, after which the value at the received address is taken and put on the stack. Also, 4 is added to the LR in order to jump this same offset after returning from the function. Then the POP {R1} command gets the value received from the stack. If you look at what is located at 0xB4BA + 0xEA = 0xB5A4, then you can see something similar to the address table:



To patch this construction, you need to get two parameters from the code: the offset and the number of the register in which you want to put the result. For each possible register, you will have to prepare a piece of code in advance.

 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" 

Put the cursor on the beginning of the construction that we want to replace - 0xB4B2 - and run the script:



In addition to the structures already mentioned, the code also includes the following:



As in the previous case, there is an offset after the BLX instruction:



We take the offset to the address from LR, add it to the LR and go there. 0x72044 + 0xC = 0x72050. The script for this construction is quite simple:

 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" 

Script execution result:



After everything is patched in the function, you can point IDA to its real beginning. It will collect the entire function code in pieces, and it can be decompiled using HexRays.

String decode


We learned how to deal with obfuscation of machine code in the libsgmainso-6.4.36.so library from the UC Browser and got the function code 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; } 

Consider the following lines more closely:

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

The sub_73E24 function explicitly decrypts the class name. As parameters of this function, a pointer is passed to the data, similar to encrypted, a kind of buffer and a number.It is obvious that after the function is called, the decoded line will be in the buffer, since it is passed to the FindClass function , which takes the class name as the second parameter. So the number is the size of the buffer or the length of the string. Let's try to decipher the name of the class, it should tell us whether we are going in the right direction. Let's take a closer look at what happens in 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; } 

The sub_7AF78 function creates an instance of a container for byte arrays of the specified size (we will not dwell on these containers in detail). Two such containers are created here: the line “DcO / lcK + h? M3c * q @” is placed in one (it is easy to guess that this is the key), in the other - the encrypted data. Then both objects are placed in a certain structure, which is passed to the sub_6115C function . Also note in this structure is a field with a value of 3. Let's see what happens with this structure next.

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

As a switch parameter, the structure field is passed, which was previously assigned the value 3. Look at case 3: parameters are transferred to the sub_6364C function from the structure, which were added there in the previous function, i.e. the key and the encrypted data. If you look closely at sub_6364C , you can find out the RC4 algorithm in it.

We have an algorithm and a key. Let's try to decipher the name of the class. This is what happened: com / taobao / wireless / security / adapter / JNICLibrary . Fine! We are on the right track.

Command tree


Now we need to find the RegisterNatives call that will point us to the doCommandNative function . We look at the functions called from JNI_OnLoad and find it in 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; } 

And indeed, a native method with the name doCommandNative is registered here . Now we know his address. Let's see what he does.

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

By name, you can guess that here is the entry point of all the functions that the developers decided to transfer to the native library. We are interested in the function with the number 10601.

From the code you can see that from the command number there are three numbers: command / 10,000 , command% 10,000 / 100 and command% 10 , i.e., in our case, 1, 6 and 1. These the three numbers, as well as the pointer to JNIEnv and the arguments passed to the function, are added to the structure and passed on. With the help of the obtained three numbers (we denote them N1, N2 and N3), a tree of commands is constructed.

Something like this:



The tree is dynamically populated in JNI_OnLoad .
Three numbers encode the path in the tree. Each leaf of the tree contains the poksorenny address of the corresponding function. The key is in the parent node. Finding a place in the code where the function we need is added to the tree is not a big deal if you understand all the structures used (we’re not describing them so as not to inflate an already rather big article).

More obfuscation


We got the address of the function that needs to decrypt the traffic: 0x5F1AC. But it’s too early to rejoice: the developers of UC Browser have prepared another surprise for us.

After receiving the parameters from the array, which was formed in the Java code, we get
to the function at address 0x4D070. And here we are waiting for another type of obfuscation code.

We put two indexes in R7 and R4:



We shift the first index in R11:



To get the address from the table, we use the index:



After moving to the first address, the second index is used, which is in R4. The table has 230 items.

What to do with it?You can tell IDA that this is such a switch: Edit -> Other -> Specify switch idiom.



The resulting code is scary. But, making his way through his jungle, one can notice the call to the already familiar function sub_6115C :



There was a switch in which in case 3 there was a decryption using the RC4 algorithm. And in this case, the structure passed to the function is filled from the parameters passed to doCommandNative . We recall that we had a magicInt with the value 16. We look at the corresponding case - and after several transitions we find the code by which we can identify the algorithm.



This is AES!

There is an algorithm, it remains to get its parameters: mode, key, and possibly initialization vector (its presence depends on the mode of operation of the AES algorithm). The structure with them should be formed somewhere before calling the sub_6115C function , but this part of the code is especially well obfuscated, so the idea arises to patch the code so that all the decryption function parameters are dumped into a file.

Patch


In order not to write all the patch code in assembly language manually, you can run Android Studio, write a function there that receives the same parameters as our decryption function and writes to the file, and then copy the code that the compiler generates.

Our friends from the UC Browser team also “took care of” the convenience of adding code. We recall that at the beginning of each function we have a garbage code, which can easily be replaced with any other. Very convenient :) However, at the beginning of the objective function, there is not enough room for the code that saves all the parameters to a file. I had to divide it into parts and use the garbage blocks of neighboring functions. Total got four parts.

First part:



In the ARM architecture, the first four parameters of the function are passed through the registers R0-R3, the rest, if they are, through the stack. In the LR register, the return address is transmitted. All this needs to be preserved so that the function can work after we change its parameters. You also need to save all the registers that we will use in the process, so we do PUSH.W {R0-R10, LR}. In R7, we get the address of the list of parameters passed to the function via the stack.

Using the fopen function, open the file / data / local / tmp / aes in the "ab" mode,
i.e. to add. In R0, we load the address of the file name; in R1, the address of the string indicating the mode. And here the garbage code ends, so go to the next function. In order for it to continue to work, we put the transition to the real function code to bypass garbage in the beginning, and instead add garbage to continue the patch.



Call fopen .

The first three parameters of the aes function are of type int . Since we saved the registers to the stack at the beginning, you can simply pass the addresses in the stack to the fwrite function .



Next we have three structures that contain the size of the data and a pointer to the data for the key, the initialization vector, and the encrypted data.



At the end, close the file, restore the registers and transfer control to the real function aes .

We collect APK with the patched library, we sign, we throw on the device / emulator, we start. We see that our dump is created, and a lot of data is written there. The browser uses encryption not only for traffic, and all encryption goes through the function in question. And for some reason, there is no necessary data, and the necessary request is not visible in the traffic. In order not to wait until UC Browser deigns to make the necessary request, take the encrypted response from the server received earlier, and patch the application again: add the decryption to the onCreate main activation.

  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 

We collect, sign, install, run. We receive NullPointerException, since the method returned null.

In the course of further analysis of the code, a function was discovered, in which interesting lines are deciphered: "META-INF /" and ".RSA". Looks like the app is checking its certificate. Or even generates keys from it. I don’t want to deal with what is happening with the certificate, so let's just give him the right certificate. Let's patch the encrypted line so that instead of “META-INF /” we get “BLABLINF /”, we will create a folder with this name in the APK and throw a certificate of the browser into it.

We collect, sign, install, run. Bingo! The key is with us!

Mitm


We got the key and initialization vector equal to the key. Let's try to decrypt the server response in CBC mode.



We see the URL of the archive, something similar to MD5, “extract_unzipsize” and a number. Check: the MD5 archive is the same, the size of the unpacked library is the same. We try to patch this library and give it to the browser. To show that our patched library has loaded, we will launch Intent to create an SMS with the text “PWNED!”. We will replace two responses from the server: puds.ucweb.com/upgrade/index.xhtml and to download the archive. In the first we substitute MD5 (the size after unpacking does not change), in the second we give the archive with the patched library.

The browser several times tries to download the archive, and then gives an error. Apparently, something
he does not like. As a result of the analysis of this muddy format, it turned out that the server also transfers the size of the archive:



It is encoded in LEB128. After the patch, the size of the archive with the library changed a little, so the browser decided that the archive was downloaded crookedly, and after several attempts gave an error.

Rule the size of the archive ... And - victory! :) Result on video.

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

Implications and Developer Response


In the same way, hackers could use the insecure feature of the UC Browser to distribute and launch malicious libraries. These libraries will work in the context of the browser, so they will get all its system permissions. As a result, the ability to display phishing windows, as well as access to the working files of an orange Chinese squirrel, including logins, passwords and cookies stored in the database.

We contacted the developers of UC Browser and informed them about the problem found, tried to point out the vulnerability and its danger, but they did not discuss anything with us. Meanwhile, the browser continued to flaunt a dangerous function in plain sight. But as soon as we revealed the details of the vulnerability, it was impossible to ignore it, as before. March 27 was
A new version of UC Browser 12.10.9.1193 was released, which accessed the server via HTTPS: puds.ucweb.com/upgrade/index.xhtml .

In addition, after the “correction” and until the article was written, an attempt to open a PDF in the browser resulted in an error message with the text “Oops, something went wrong!”. The request to the server when trying to open the PDF was not executed, but the request was executed when the browser was started, which hints at the ability to download the executable code in violation of the rules of Google Play.

Source: https://habr.com/ru/post/451936/


All Articles