
Looking for Vulnerabilities in UC Browser

Introduction
At the end of March, we reported that we discovered a hidden ability to download and run 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 downloaded, for example, a porn clip, but instead received an APK with this browser), used frightening banners with messages saying that the browser is outdated, vulnerable and stuff like that. The official UC Browser group in VK has a themein which users can complain about unfair advertising, there are many examples. In 2016, there was even video advertising in Russian (yes, ads from a browser that blocks ads).
At the time of writing, UC Browser has more than 500,000,000 installations on Google Play. This is impressive - only Google Chrome has more. 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 UC Browser is doing something bad. And it turned out that he was doing it!
The application code revealed the ability to download and run executable code, which contradicts the rules for publishing applicationson Google Play. In addition to the fact that UC Browser downloads executable code, it makes it unsafe, which can be used to carry out a MitM attack. Let's see if we manage to 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 called com.uc.deployment.UpgradeDeployService .
When this service starts, the browser makes a POST request to puds.ucweb.com/upgrade/index.xhtml, which can be noticed in the traffic some time after the start. In response, he may receive a command to download an update or a new module. In the process of analysis, the server did not give such commands, but we noticed that when trying to open the PDF in the browser, it makes a second request at the above address, 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, which is not in the APK and which, if necessary, is downloaded from the Internet. It is worth noting that theoretically, UC Browser can be forced to download something without user interaction - if you give a correctly formed response to a request that is executed after the browser starts. But for this you need to study the protocol of interaction with the server in more detail,
So, when the user wants to open the PDF directly in the browser, the following requests can be seen in the traffic:

First, there is a POST request to puds.ucweb.com/upgrade/index.xhtml , after which the
archive with the library for viewing PDF and office formats is downloaded. It is logical to assume that in the first request information about the system is transmitted (at least the architecture in order to give the necessary library), and in response to it the browser receives some information about the library that needs to be downloaded: address and, possibly, something else. The problem is that this request is encrypted.
Request snippet | Response snippet |
![]() | ![]() |
The library itself is packaged in ZIP and not encrypted.

Search traffic decryption code
Попробуем расшифровать ответ сервера. Смотрим код класса com.uc.deployment.UpgradeDeployService: из метода onStartCommand переходим в com.uc.deployment.b.x, а из него в com.uc.browser.core.d.c.f.e:
public final void e(l arg9) {
int v4_5;
String v3_1;
byte[] v3;
byte[] v1 = null;
if(arg9 == null) {
v3 = v1;
}
else {
v3_1 = arg9.iGX.ipR;
StringBuilder v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]product:");
v4.append(arg9.iGX.ipR);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]version:");
v4.append(arg9.iGX.iEn);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]upgrade_type:");
v4.append(arg9.iGX.mMode);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]force_flag:");
v4.append(arg9.iGX.iEo);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_mode:");
v4.append(arg9.iGX.iDQ);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_type:");
v4.append(arg9.iGX.iEr);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_state:");
v4.append(arg9.iGX.iEp);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_file:");
v4.append(arg9.iGX.iEq);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apk_md5:");
v4.append(arg9.iGX.iEl);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]download_type:");
v4.append(arg9.mDownloadType);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]download_group:");
v4.append(arg9.mDownloadGroup);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]download_path:");
v4.append(arg9.iGH);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_child_version:");
v4.append(arg9.iGX.iEx);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_series:");
v4.append(arg9.iGX.iEw);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_cpu_arch:");
v4.append(arg9.iGX.iEt);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_cpu_vfp3:");
v4.append(arg9.iGX.iEv);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_cpu_vfp:");
v4.append(arg9.iGX.iEu);
ArrayList v3_2 = arg9.iGX.iEz;
if(v3_2 != null && v3_2.size() != 0) {
Iterator v3_3 = v3_2.iterator();
while(v3_3.hasNext()) {
Object v4_1 = v3_3.next();
StringBuilder v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_name:");
v5.append(((au)v4_1).getName());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_ver_name:");
v5.append(((au)v4_1).aDA());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_ver_code:");
v5.append(((au)v4_1).gBl);
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_req_type:");
v5.append(((au)v4_1).gBq);
}
}
j v3_4 = new j();
m.b(v3_4);
h v4_2 = new h();
m.b(v4_2);
ay v5_1 = new ay();
v3_4.hS("");
v3_4.setImsi("");
v3_4.hV("");
v5_1.bPQ = v3_4;
v5_1.bPP = v4_2;
v5_1.yr(arg9.iGX.ipR);
v5_1.gBF = arg9.iGX.mMode;
v5_1.gBI = arg9.iGX.iEz;
v3_2 = v5_1.gAr;
c.aBh();
v3_2.add(g.fs("os_ver", c.getRomInfo()));
v3_2.add(g.fs("processor_arch", com.uc.b.a.a.c.getCpuArch()));
v3_2.add(g.fs("cpu_arch", com.uc.b.a.a.c.Pb()));
String v4_3 = com.uc.b.a.a.c.Pd();
v3_2.add(g.fs("cpu_vfp", v4_3));
v3_2.add(g.fs("net_type", String.valueOf(com.uc.base.system.a.Jo())));
v3_2.add(g.fs("fromhost", arg9.iGX.iEm));
v3_2.add(g.fs("plugin_ver", arg9.iGX.iEn));
v3_2.add(g.fs("target_lang", arg9.iGX.iEs));
v3_2.add(g.fs("vitamio_cpu_arch", arg9.iGX.iEt));
v3_2.add(g.fs("vitamio_vfp", arg9.iGX.iEu));
v3_2.add(g.fs("vitamio_vfp3", arg9.iGX.iEv));
v3_2.add(g.fs("plugin_child_ver", arg9.iGX.iEx));
v3_2.add(g.fs("ver_series", arg9.iGX.iEw));
v3_2.add(g.fs("child_ver", r.aVw()));
v3_2.add(g.fs("cur_ver_md5", arg9.iGX.iEl));
v3_2.add(g.fs("cur_ver_signature", SystemHelper.getUCMSignature()));
v3_2.add(g.fs("upgrade_log", i.bjt()));
v3_2.add(g.fs("silent_install", String.valueOf(arg9.iGX.iDQ)));
v3_2.add(g.fs("silent_state", String.valueOf(arg9.iGX.iEp)));
v3_2.add(g.fs("silent_file", arg9.iGX.iEq));
v3_2.add(g.fs("silent_type", String.valueOf(arg9.iGX.iEr)));
v3_2.add(g.fs("cpu_archit", com.uc.b.a.a.c.Pc()));
v3_2.add(g.fs("cpu_set", SystemHelper.getCpuInstruction()));
boolean v4_4 = v4_3 == null || !v4_3.contains("neon") ? false : true;
v3_2.add(g.fs("neon", String.valueOf(v4_4)));
v3_2.add(g.fs("cpu_cores", String.valueOf(com.uc.b.a.a.c.Jl())));
v3_2.add(g.fs("ram_1", String.valueOf(com.uc.b.a.a.h.Po())));
v3_2.add(g.fs("totalram", String.valueOf(com.uc.b.a.a.h.OL())));
c.aBh();
v3_2.add(g.fs("rom_1", c.getRomInfo()));
v4_5 = e.getScreenWidth();
int v6 = e.getScreenHeight();
StringBuilder v7 = new StringBuilder();
v7.append(v4_5);
v7.append("*");
v7.append(v6);
v3_2.add(g.fs("ss", v7.toString()));
v3_2.add(g.fs("api_level", String.valueOf(Build$VERSION.SDK_INT)));
v3_2.add(g.fs("uc_apk_list", SystemHelper.getUCMobileApks()));
Iterator v4_6 = arg9.iGX.iEA.entrySet().iterator();
while(v4_6.hasNext()) {
Object v6_1 = v4_6.next();
v3_2.add(g.fs(((Map$Entry)v6_1).getKey(), ((Map$Entry)v6_1).getValue()));
}
v3 = v5_1.toByteArray();
}
if(v3 == null) {
this.iGY.iGI.a(arg9, "up_encode", "yes", "fail");
return;
}
v4_5 = this.iGY.iGw ? 0x1F : 0;
if(v3 == null) {
}
else {
v3 = g.i(v4_5, v3);
if(v3 == null) {
}
else {
v1 = new byte[v3.length + 16];
byte[] v6_2 = new byte[16];
Arrays.fill(v6_2, 0);
v6_2[0] = 0x5F;
v6_2[1] = 0;
v6_2[2] = ((byte)v4_5);
v6_2[3] = -50;
System.arraycopy(v6_2, 0, v1, 0, 16);
System.arraycopy(v3, 0, v1, 16, v3.length);
}
}
if(v1 == null) {
this.iGY.iGI.a(arg9, "up_encrypt", "yes", "fail");
return;
}
if(TextUtils.isEmpty(this.iGY.mUpgradeUrl)) {
this.iGY.iGI.a(arg9, "up_url", "yes", "fail");
return;
}
StringBuilder v0 = new StringBuilder("[");
v0.append(arg9.iGX.ipR);
v0.append("]url:");
v0.append(this.iGY.mUpgradeUrl);
com.uc.browser.core.d.c.i v0_1 = this.iGY.iGI;
v3_1 = this.iGY.mUpgradeUrl;
com.uc.base.net.e v0_2 = new com.uc.base.net.e(new com.uc.browser.core.d.c.i$a(v0_1, arg9));
v3_1 = v3_1.contains("?") ? v3_1 + "&dataver=pb" : v3_1 + "?dataver=pb";
n v3_5 = v0_2.uc(v3_1);
m.b(v3_5, false);
v3_5.setMethod("POST");
v3_5.setBodyProvider(v1);
v0_2.b(v3_5);
this.iGY.iGI.a(arg9, "up_null", "yes", "success");
this.iGY.iGI.b(arg9);
}
Видим тут формирование POST-запроса. Обращаем внимание на создание массива из 16 байт и его заполнение: 0x5F, 0, 0x1F, -50 (=0xCE). Совпадает с тем, что мы видели в запросе выше.
В этом же классе можно заметить вложенный класс, в котором есть другой интересный метод:
public final void a(l arg10, byte[] arg11) {
f v0 = this.iGQ;
StringBuilder v1 = new StringBuilder("[");
v1.append(arg10.iGX.ipR);
v1.append("]:UpgradeSuccess");
byte[] v1_1 = null;
if(arg11 == null) {
}
else if(arg11.length < 16) {
}
else {
if(arg11[0] != 0x60 && arg11[3] != 0xFFFFFFD0) {
goto label_57;
}
int v3 = 1;
int v5 = arg11[1] == 1 ? 1 : 0;
if(arg11[2] != 1 && arg11[2] != 11) {
if(arg11[2] == 0x1F) {
}
else {
v3 = 0;
}
}
byte[] v7 = new byte[arg11.length - 16];
System.arraycopy(arg11, 16, v7, 0, v7.length);
if(v3 != 0) {
v7 = g.j(arg11[2], v7);
}
if(v7 == null) {
goto label_57;
}
if(v5 != 0) {
v1_1 = g.P(v7);
goto label_57;
}
v1_1 = v7;
}
label_57:
if(v1_1 == null) {
v0.iGY.iGI.a(arg10, "up_decrypt", "yes", "fail");
return;
}
q v11 = g.b(arg10, v1_1);
if(v11 == null) {
v0.iGY.iGI.a(arg10, "up_decode", "yes", "fail");
return;
}
if(v0.iGY.iGt) {
v0.d(arg10);
}
if(v0.iGY.iGo != null) {
v0.iGY.iGo.a(0, ((o)v11));
}
if(v0.iGY.iGs) {
v0.iGY.a(((o)v11));
v0.iGY.iGI.a(v11, "up_silent", "yes", "success");
v0.iGY.iGI.a(v11);
return;
}
v0.iGY.iGI.a(v11, "up_silent", "no", "success");
}
}
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 answer from the server: zero byte - 0x60, second - 0x1F, third - 0x60. Looks like what we need. Judging by the lines (“up_decrypt”, for example), a method should be called here that decrypts the server’s response.
We pass to the gj method . Note that the byte at offset 2 (that is, 0x1F in our case) is transferred to it as the first argument, and the server response without the
first 16 bytes is used as the second argument .
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 selected, and the same byte, which in our
case is 0x1F, indicates one of three possible options.
We continue to analyze the code. After a couple of jumps, we get into a method with the talking 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 selected 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 the key is not obtained yet, but only its “identifier”. Getting the key is a little more complicated.
In the next method, two more 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 (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 staticBinarySafeDecryptNoB64 method of the com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent interface . There are no classes in the main application code that implement this interface. Such a class is in the file lib / armeabi-v7a / libsgmain.so , which is actually not .so, but .jar. The method of interest to us 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 is our list of parameters is supplemented with two more integers 2 and 0. By
around 2 decoding means, 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 transition chain, 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);
}
}
And also the JNICLibrary class , in which the doCommandNative native 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 then the fun begins.
Machine Code Obfuscation
В файле libsgmain.so (который на самом деле .jar и в котором мы чуть выше нашли реализацию некоторых интерфейсов, связанных с шифрованием) есть одна нативная библиотека: libsgmainso-6.4.36.so. Открываем её в IDA и получаем кучу диалоговых окон с ошибками. Проблема в том, что таблица секций (section header table) – невалидная. Это сделано специально, чтобы усложнить анализ.

Но она и не нужна: чтобы корректно загрузить ELF-файл и проанализировать его, вполне достаточно таблицы сегментов (program header table). Поэтому просто удаляем таблицу секций, зануляя соответствующие поля в заголовке.

Снова открываем файл в IDA.
There are two ways to tell the Java virtual machine exactly where in the native library the implementation of the method declared in the Java code as native is located. The first is to give it a name of the form Java_package_name_ClassName_Method_name .
The second is to register it when loading the library (in the JNI_OnLoad function )
by calling the RegisterNatives function .
In our case, if you use the first method, the name should be like this: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative .
Among exported functions, there is none such, so you need to look for the RegisterNatives call .
We 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 register LR, in which the return address from the function is located. 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 look closely, 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, the IDA thinks that in the last instruction there is a normal return from the function, but 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 least significant bit of the address tells the processor which instruction set is being used. That is, the address is actually 0xB13A, and the unit in the low bit indicates Thumb mode.
A similar “adapter” and
junk code has been added to the beginning of each function in this library . Further we will not dwell on them in detail - we just remember
that the real beginning of almost all functions is a little further.
Since there is no explicit transition to 0xB13A in the code, the IDA itself did not recognize that the code was in this place. For the same reason, it does not recognize most of the code in the library as code, which makes analysis somewhat difficult. We tell the IDA that the code is here, and this is what it turns out:

The table obviously starts on 0xB144. What about sub_494C?

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

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 addresses. And scripts and the ability to patch code in the 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: the

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

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

And really, here the dword is taken at the address lying in LR, added to this address, after which the value at the received address is taken and pushed onto the stack. Also, 4 is added to LR, so that after returning from the function to jump this same offset. Then the POP {R1} command pulls the received value from the stack. If you look at the address 0xB4BA + 0xEA = 0xB5A4, you can see something similar to the address table:

To patch this design, you 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] == '\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"
We put the cursor at the beginning of the construct that we want to replace - 0xB4B2 - and run the script:

In addition to the constructions already named

, the following also appear in the code: As in the previous case, after the BLX instruction, there is an offset:

Take the offset to the address from LR, add it to LR and go there. 0x72044 + 0xC = 0x72050. The script for this design is very 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"
The result of the script:

After everything is patched in the function, you can point the IDA to its real beginning. It will collect all the function code in pieces, and it can be decompiled using HexRays.
Decoding strings
We learned how to deal with machine code obfuscation in the libsgmainso-6.4.36.so library from UC Browser and got the code for the JNI_OnLoad function .
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 carefully the following lines:
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 to data similar to encrypted, a buffer and a number is passed. Obviously, after the function is called, the buffer will contain a decrypted line, because it is passed to the FindClass function , which takes the class name as the second parameter. Therefore, a number is the size of a buffer or the length of a 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 the container for byte arrays of the specified size (we will not dwell on these containers in detail). Two such containers are created here: in one the line “DcO / lcK + h? M3c * q @” is placed (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 sub_6115C function . Also note in this structure a field with the value 3. 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 structure field is passed, which was previously assigned the value 3. We look at case 3: parameters from the structure that were added there in the previous function, that is, the key and encrypted data, are transferred to the sub_6364C function . 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 decrypt the class name. Here's what happened: com / taobao / wireless / security / adapter / JNICLibrary . Excellent! We are on the right track.
Team tree
Now we need to find the RegisterNatives call , which will point to the doCommandNative function . We look through 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 called 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 there is an entry point for all the functions that the developers decided to transfer to the native library. We are interested in the function with the number 10601.
According to the code, we can see that three numbers are obtained from the command number: command / 10000 , command% 10000/100 and command% 10 , that is, in our case, 1, 6 and 1. These three numbers, as well as a pointer to JNIEnv and arguments passed to functions are added to the structure and passed on. Using the obtained three numbers (we denote them N1, N2 and N3), a team tree is constructed.
Something like this:

The tree is populated dynamically in JNI_OnLoad .
Three numbers encode a path in a tree. Each leaf of the tree contains the pockered 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 we understand all the structures used (we will not give a description of them so as not to inflate an already rather big article).
More obfuscation
We got 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 generated in the Java code, we get
into the function at the address 0x4D070. And here we are waiting for another type of code obfuscation.
We put two indexes in R7 and R4:

We transfer the first index to R11:

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

After going to the first address, we use the second index, which is in R4. The table has 230 items.
What to do about it? You can tell the IDA that this is a switch: Edit -> Other -> Specify switch idiom.

The resulting code is scary. But, making his way through its jungle, you can notice the call to a function that is already familiar to ussub_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 out from the parameters passed to doCommandNative . We recall that we had magicInt there with a value of 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, the initialization vector (its presence depends on the mode of operation of the AES algorithm). The structure with them should be formed somewhere before the sub_6115C function call, but this part of the code is obfuscated especially well, so the idea is to patch the code so that all parameters of the decryption function 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 it to a file, 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 junk code that can easily be replaced with any other. Very convenient :) True, at the beginning of the objective function, there is not enough space for a code that saves all parameters to a file. I had to divide it into parts and use the garbage blocks of neighboring functions. A total of 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 any, through the stack. In the register LR the return address is transmitted. All this needs to be preserved so that the function can work out 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 through the stack.
Using the fopen function, open the file / data / local / tmp / aes in "ab" mode,
i.e. to add. In R0 we load the address of the file name, in R1 - the address of the line indicating the mode. And then the garbage code ends, so go to the next function. So that it continues to work, we put in the beginning the transition to the real function code bypassing garbage, and instead of garbage we add the continuation of the patch.

We call fopen .
The first three parameters of the aes function are of type int . Since we saved the registers onto the stack at the beginning, we can simply pass the addresses on 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, initialization vector, and encrypted data.

At the end, close the file, restore the registers, and transfer control to the real aes function .
We collect the APK with the patched library, sign it, drop it onto the device / emulator, launch it. 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 the UC Browser deigns to make the necessary request, we take the encrypted response from the server received earlier and patch the application again: add the decryption to the 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 application is verifying its certificate. Or even generates keys from it. I don’t really want to understand what is happening with the certificate, so just slip the right certificate on it. Let's patch the encrypted line so that instead of “META-INF /” it turns out “BLABLINF /”, create a folder with that name in the APK and drop the certificate of the protein browser there.
We collect, sign, install, launch. Bingo! The key is with us!
Mitm
We got the key and the initialization vector equal to the key. Let's try to decrypt the server response in CBC mode.

We see the archive URL, 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 are trying to patch this library and give it to the browser. To show that our patched library has loaded, we will run Intent to create SMS with the text “PWNED!”. We will replace two responses from the server: puds.ucweb.com/upgrade/index.xhtml and for downloading 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 still transfers the size of the archive:

It is encoded in LEB128. After the patch, the size of the archive with the library changed a bit, so the browser decided that the archive downloaded crookedly, and after several attempts it gave an error.
We correct the size of the archive ... And - victory! :) The result on the video.
https://www.youtube.com/watch?v=Nfns7uH03J8
Developer Consequences and Response
In exactly the same way, hackers could use the unsafe UC Browser feature to distribute and run malicious libraries. These libraries will work in the context of the browser, so they will receive all its system permissions. As a result - the ability to display phishing windows, as well as access to orange Chinese squirrel work files, including logins, passwords and cookies stored in the database.
We contacted the developers of UC Browser and informed them of 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 with a dangerous feature in plain sight. But as soon as we revealed the details of the vulnerability, it was no longer possible to ignore this, 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 time of writing, the attempt to open the PDF in the browser led to 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 started, which hints at the continued ability to download executable code in violation of Google Play rules.