Looking for vulnerabilities in UC Browser

Looking for vulnerabilities in UC Browser

Introduction

At the end of March we Reportedthat discovered the hidden possibility of downloading and running unverified code in UC Browser. Today we will analyze in detail how this download occurs 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 malware, distributed from various sites under the guise of video files (i.e., users thought they were downloading, for example, a porn video, but instead they received an APK with this browser), used scary banners with messages that the browser was outdated, vulnerable, and stuff like that. The official UC Browser group on VK has topic, in which users can complain about unfair advertising, there are many examples. In 2016 there was even video advertising in Russian (yes, ads for a browser that blocks ads).

At the time of writing, UC Browser has over 500 installs on Google Play. This is impressive - only Google Chrome has more. Among the reviews, you can see quite a lot of complaints about ads and redirects to some applications on Google Play. This was the reason for the study: we decided to see if UC Browser was doing something bad. And it turns out that it does!

The application code found the ability to download and run executable code, which is against the application publishing rules on Google Play. In addition to downloading the executable code, UC Browser does so in an insecure way that can be used to launch a MitM attack. Let's see if we can carry out such an attack.

Everything that is written below is relevant for the version of UC Browser that was present on Google Play at the time of the study:

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 a telling name com.uc.deployment.UpgradeDeployService.

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

When this service is started, the browser makes 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 an update or a new module. During 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 second request to the address indicated above, after which it downloads the native library. To carry out the attack, we decided to use this feature of UC Browser: the ability to open PDF using a native library that is not in the APK and which it downloads from the Internet if necessary. It is worth noting that, theoretically, UC Browser can be forced to download something without user interaction - if you give a well-formed response to a request that is executed after the browser is launched. But for this we need to study the protocol of interaction with the server in more detail, so we decided that it would be easier to edit the intercepted response and replace the library for working with PDF.

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

Looking for vulnerabilities in UC Browser

The first is a POST request to puds.ucweb.com/upgrade/index.xhtmlThen
an archive with a library for viewing PDF and office formats is downloaded. It is logical to assume that information about the system is transmitted in the first request (at least, the architecture to give the desired library), and in response to it, the browser receives some information about the library that needs to be downloaded: the address and, possibly, something else. The problem is that this request is encrypted.

Request Fragment

Answer fragment

Looking for vulnerabilities in UC Browser

Looking for vulnerabilities in UC Browser

The library itself is packaged in ZIP and not encrypted.

Looking for vulnerabilities in UC Browser

Search for traffic decryption code

Let's try to decipher the server's response. Look at the class code com.uc.deployment.UpgradeDeployService: from method onStartCommand 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();
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);
}

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 see a nested class that has 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 = 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");
}
}

The method receives 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 response from the server: the zero byte is 0x60, the second is 0x1F, the third is 0x60. Looks like what we need. Judging by the lines (β€œup_decrypt”, for example), a method should be called here that will decrypt the server response.
Let's move on to the method. gj. Note that the byte at offset 2 (i.e. 0x1F in our case) is passed to it as the first argument, and the server response without
first 16 bytes.

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

Obviously, here the decryption algorithm is chosen, and the same byte that is in our
case is equal to 0x1F, denotes one of the three possible options.

We continue to analyze the code. After a couple of jumps, we get into a method with a speaking name decryptBytesByKey.

Here, two more bytes are separated from our response, and a string is obtained from them. It is clear that in this way a 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 yet a key, but only its β€œidentifier”. Getting the key is a little more complicated.

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

    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 method staticBinarySafeDecryptNoB64 interface com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. There are no classes in the main application code that implement this interface. This class is in the file lib/armeabi-v7a/libsgmain.so, which is not actually .so, but .jar. The method we are interested in is implemented as follows:

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

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

After the next chain of transitions, we find a class that implements the interface IRouterComponent and method 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);
}
}

And also the class JNICLibrary, in which the native method is declared doCommandNative:

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

So, we need to find a method in native code doCommandNative. And this is where the fun begins.

Machine code obfuscation

In file libsgmain.so (which is actually a .jar and in which we found the implementation of some interfaces related to encryption a little higher) there is one native library: libsgmainso-6.4.36.so. We 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 on purpose to complicate the analysis.

Looking for vulnerabilities in UC Browser

But it is not needed: in order to correctly load the ELF file and analyze it, a segment table (program header table) is enough. Therefore, we simply delete the section table, nulling the corresponding fields in the header.

Looking for vulnerabilities in UC Browser

Open the file again in IDA.

There are two ways to tell the Java Virtual Machine where exactly in the native library is the implementation of a method declared as native in the Java code. The first is to give it a species name Java_package_name_ClassName_MethodName.

The second is to register it when loading the library (in the function JNI_OnLoad)
with a function call RegisterNatives.

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, so you need to look for a call RegisterNatives.
Let's go to the function JNI_OnLoad and we see this picture:

Looking for vulnerabilities in UC Browser

What's going on here? At first glance, the beginning and end of the function are typical for the ARM architecture. The first instruction on the stack saves 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. The last instruction restores the saved registers, and the return address is immediately placed in the PC register - this is how the function returns. But if you look closely, you can see that the penultimate instruction changes the return address stored on the stack. Calculate what it will be like after
code execution. A certain address 1xB0 is loaded into R130, 5 is subtracted from it, then it is shifted to R0 and 0x10 is added to it. It turns out 0xB13B. Thus, IDA thinks that in the last instruction there is a normal return from the function, but in fact there is a jump to the computed address 0xB13B.

It is worth recalling here that ARM processors have two modes and two instruction sets: ARM and Thumb. The least significant 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 least significant bit indicates the Thumb mode.

A similar "adapter" is added to the beginning of each function in this library and
junk code. We will not dwell on them further - just remember that
that the real beginning of almost all functions is a little further.

Since there is no explicit jump to 0xB13A in the code, the IDA itself did not recognize that the code was at that location. For the same reason, it does not recognize most of the code in the library as code, which makes analysis somewhat difficult. We tell IDA that there is code, and this is what happens:

Looking for vulnerabilities in UC Browser

The table clearly starts at 0xB144. What's in sub_494C?

Looking for vulnerabilities in UC Browser

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

Looking for vulnerabilities in UC Browser

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

Judging by the size of the table, there are many such transitions in the code. The question arises whether it is possible to somehow deal with this more automatically, without manually calculating the addresses. And scripts and the ability to patch the code in IDA come to our aid:

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"

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

Looking for vulnerabilities in UC Browser

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

Looking for vulnerabilities in UC Browser

The instructions after BLX don't look very meaningful, it's more like some sort of offset. Look in sub_4964:

Looking for vulnerabilities in UC Browser

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

Looking for vulnerabilities in UC Browser

To patch this construction, you will need to get two parameters from the code: the offset and the register number 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] == '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"

We put the cursor at the beginning of the structure we want to replace - 0xB4B2 - and run the script:

Looking for vulnerabilities in UC Browser

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

Looking for vulnerabilities in UC Browser

As in the previous case, the BLX instruction is followed by an offset:

Looking for vulnerabilities in UC Browser

We take the offset at the address from LR, add it to LR and go there. 0x72044 + 0xC = 0x72050. The script for this design 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"

Result of script execution:

Looking for vulnerabilities in UC Browser

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

String decryption

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

Let's take a closer look at the following lines:

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

In function sub_73E24 the class name is explicitly decoded. As parameters to this function, a pointer to data similar to encrypted data, a certain buffer, and a number are passed. Obviously, after calling the function, the buffer will contain the decoded string, since it is passed to the function FindClass, 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 class name, it should tell us if 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;
}

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

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, a field of the structure is passed, which was previously assigned the value 3. We look at case 3: into the function sub_6364C parameters are passed from the structure that were added there in the previous function, i.e. the key and encrypted data. If you look closely at sub_6364C, you can recognize the RC4 algorithm in it.

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

Command Tree

Now I need to find a challenge RegisterNatives, which will point us to the function doCommandNative. Looking 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;
}

Indeed, a native method with the name doCommandNative. 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 the name, you can guess that here is the entry point for all the functions that the developers decided to transfer to the native library. We are interested in the function number 10601.

From the code, you can see that three numbers are obtained from the command number: command/10000, command % 10000 / 100 ΠΈ command % 10, i.e., in our case, 1, 6 and 1. These three numbers, as well as a 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 by N1, N2 and N3), a command tree is built.

Something like this:

Looking for vulnerabilities in UC Browser

The tree is filled dynamically in JNI_OnLoad.
The three numbers encode the path in the tree. Each leaf of the tree contains the poxed 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 difficult if you understand all the structures used (their description is not given so as not to inflate an already rather large article).

More obfuscation

We received the address of the function that should 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 that was formed in the Java code, we get
to the function at 0x4D070. And here we are waiting for another type of code obfuscation.

We put two indices in R7 and R4:

Looking for vulnerabilities in UC Browser

We shift the first index to R11:

Looking for vulnerabilities in UC Browser

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

Looking for vulnerabilities in UC Browser

After jumping to the first address, the second index is used, which is in R4. There are 230 elements in the table.

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

Looking for vulnerabilities in UC Browser

The resulting code is terrible. But, making our way through its jungle, you can notice the call to the function already familiar to us sub_6115C:

Looking for vulnerabilities in UC Browser

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 remember that we had magicInt with a value of 16. We look at the corresponding case - and after several transitions we find a code by which we can identify the algorithm.

Looking for vulnerabilities in UC Browser

It's AES!

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

Patch

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

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 garbage code, which can be easily replaced with any other. Very convenient πŸ™‚ However, at the beginning of the target function, there is not enough space for the code that saves all the parameters to a file. I had to split it into parts and use garbage blocks of neighboring functions. There were four parts in total.

First part:

Looking for vulnerabilities in UC Browser

In the ARM architecture, the first four parameters of the function are passed through registers R0-R3, the rest, if any, are passed through the stack. The return address is transmitted in the LR register. All this must be saved so that the function can work after we dump its parameters. We 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 function fopen open file /data/local/tmp/aes in "ab" mode,
i.e. for addition. In R0 we load the address of the file name, in R1 - the address of the line indicating the mode. And here the garbage code ends, so we move on to the next function. In order for it to continue to work, we put at the beginning the transition to the real code of the function, bypassing the garbage, and instead of the garbage we add the continuation of the patch.

Looking for vulnerabilities in UC Browser

We call fopen.

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

Looking for vulnerabilities in UC Browser

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

Looking for vulnerabilities in UC Browser

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

We assemble the APK with the patched library, sign it, upload it to the device / emulator, and launch it. We see that our dump is being created, and a lot of data is being written there. The browser uses encryption not only for traffic, and all encryption goes through the function in question. But 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, we will take the encrypted response from the server received earlier and patch the application again: add decryption to onCreate of the main activity.

    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, launch. We get a NullPointerException because the method returned null.

In the course of further analysis of the code, a function was discovered in which interesting lines are decoded: "META-INF /" and ".RSA". It looks like the app is verifying its certificate. Or even generate keys from it. I don’t want to deal with what is happening with the certificate at all, so we’ll just slip the correct certificate into it. Let's patch the encrypted line in such a way that instead of "META-INF /" it turns out "BLABLINF /", create a folder with that name in the APK and add the white browser certificate there.

We collect, sign, install, launch. Bingo! We have the key!

MitM

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

Looking for vulnerabilities in UC Browser

We see the URL of the archive, something similar to MD5, "extract_unzipsize" and a number. We check: the MD5 of the archive matches, the size of the unpacked library matches. We are trying to patch this library and give it to the browser. To show that our patched library has loaded, we will launch an 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 replace MD5 (the size does not change after unpacking), in the second, we give the archive with the patched library.

The browser tries to download the archive several times, after which it 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 transmits the size of the archive:

Looking for vulnerabilities in UC Browser

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

We correct the size of the archive ... And - victory! πŸ™‚ Video result.

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

Consequences and developer reaction

In the same way, hackers could use the insecure feature of UC Browser to distribute and run malicious libraries. These libraries will run in the context of the browser, so they will have all of its system permissions. As a result, the ability to show phishing windows, as well as access to the work files of the 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 we found, tried to point out the vulnerability and its danger, but they did not discuss anything with us. Meanwhile, the browser continued to flaunt the dangerous feature in plain sight. But as soon as we revealed the details of the vulnerability, it was no longer possible 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.

Also, after the "fix" and up to the time of writing, trying to open a PDF in a browser resulted in an error message saying "Oops, something went wrong!". The request to the server when trying to open the PDF was not executed, but the request was made when the browser was launched, which hints at the continued ability to download executable code in violation of Google Play rules.

Source: habr.com

Add a comment