Reverse Engineering Xiaomi’s Analytics app

I own a Xiaomi Mi4 and I discovered it comes with a pre-installed app called AnalyticsCore, package name com.miui.analytics, that’s running in the background. I’m not a big fan of apps gaining information without my permissions, so I started investigating its activities. For those who don’t know, Xiaomi is the largest smartphone manufacturer in China and actively growing worldwide.

For this I downloaded dex2jar and Java Decompiler and started AnalyticsCore.apk in it. The APK is downloadable here if you want to take a look yourself.
I first googled what its purpose is, and I found a single thread on the Xiaomi forums, but there is no response or explanation on what it does. See this thread.

Inside Java Decompiler there are mainly three interesting classes in how AnalyticsCore gets his updates, named c.class, e.class and f.class by Java Decompiler. Here is the code of a function inside f.class. (all decompiled code)

private boolean I()
  {
    boolean bool = false;
    if (b.t()) {}
    for (;;)
    {
      return bool;
      long l2 = J();
      m.d("Analytics-UpdateManager", "last update check time is " + new Date(l2).toString());
      long l1 = new Random(System.currentTimeMillis()).nextLong();
      if (System.currentTimeMillis() - l2 >= (l1 % (2L * 43200000L) + 2L * 43200000L) % (2L * 43200000L) - 43200000L + 86400000L) {
        bool = true;
      }
    }
  }

The above function checks some time within every 24 hours for a new Analytics update. It makes the following request every day within 24 hours, which is very often if you ask me:

  public void run()
  {
    int i = 0;
    long l1 = System.currentTimeMillis();
    for (;;)
    {
      int j = i + 1;
      if (i < 2) {}
      try
      {
        Object localObject2 = new java/lang/StringBuilder;
        ((StringBuilder)localObject2).();
        Object localObject1 = f.a(this.A);
        ((StringBuilder)localObject2).append("currentApiVersion0.0.0");
        Object localObject3 = new java/lang/StringBuilder;
        ((StringBuilder)localObject3).();
        ((StringBuilder)localObject2).append("currentCoreVersion" + k.t(f.b(this.A)));
        localObject3 = new java/lang/StringBuilder;
        ((StringBuilder)localObject3).();
        ((StringBuilder)localObject2).append("imei" + com.miui.analytics.internal.a.k.j(f.b(this.A)));
        localObject3 = new java/lang/StringBuilder;
        ((StringBuilder)localObject3).();
        ((StringBuilder)localObject2).append("mac" + com.miui.analytics.internal.a.k.k(f.b(this.A)));
        localObject3 = new java/lang/StringBuilder;
        ((StringBuilder)localObject3).();
        ((StringBuilder)localObject2).append("model" + com.miui.analytics.internal.a.k.getModel());
        localObject3 = new java/lang/StringBuilder;
        ((StringBuilder)localObject3).();
        ((StringBuilder)localObject2).append("nonce" + (String)localObject1);
        localObject3 = new java/lang/StringBuilder;
        ((StringBuilder)localObject3).();
        ((StringBuilder)localObject2).append("package" + f.b(this.A).getPackageName());
        localObject3 = new java/lang/StringBuilder;
        ((StringBuilder)localObject3).();
        ((StringBuilder)localObject2).append("ts" + l1);
        ((StringBuilder)localObject2).append("[email protected]#)(*[email protected]!#");
        localObject3 = n.getMd5Digest(((StringBuilder)localObject2).toString()).toLowerCase(Locale.getDefault());
        localObject2 = new java/lang/StringBuilder;
        ((StringBuilder)localObject2).("http://sdkconfig.ad.xiaomi.com/api/checkupdate/lastusefulversion?");
        ((StringBuilder)localObject2).append("currentApiVersion=0.0.0");
        Object localObject4 = new java/lang/StringBuilder;
        ((StringBuilder)localObject4).();
        ((StringBuilder)localObject2).append("¤tCoreVersion=" + k.t(f.b(this.A)));
        localObject4 = new java/lang/StringBuilder;
        ((StringBuilder)localObject4).();
        ((StringBuilder)localObject2).append("&imei=" + com.miui.analytics.internal.a.k.j(f.b(this.A)));
        localObject4 = new java/lang/StringBuilder;
        ((StringBuilder)localObject4).();
        ((StringBuilder)localObject2).append("&mac=" + com.miui.analytics.internal.a.k.k(f.b(this.A)));
        localObject4 = new java/lang/StringBuilder;
        ((StringBuilder)localObject4).();
        ((StringBuilder)localObject2).append("&model=" + URLEncoder.encode(com.miui.analytics.internal.a.k.getModel(), "utf-8"));
        localObject4 = new java/lang/StringBuilder;
        ((StringBuilder)localObject4).();
        ((StringBuilder)localObject2).append("&nonce=" + (String)localObject1);
        localObject1 = new java/lang/StringBuilder;
        ((StringBuilder)localObject1).();
        ((StringBuilder)localObject2).append("&package=" + f.b(this.A).getPackageName());
        localObject1 = new java/lang/StringBuilder;
        ((StringBuilder)localObject1).();
        ((StringBuilder)localObject2).append("&ts=" + l1);
        localObject1 = new java/lang/StringBuilder;
        ((StringBuilder)localObject1).();
        ((StringBuilder)localObject2).append("&sign=" + (String)localObject3);
        localObject1 = new java/net/URL;
        ((URL)localObject1).(((StringBuilder)localObject2).toString());
        localObject1 = (HttpURLConnection)((URL)localObject1).openConnection();
        ((HttpURLConnection)localObject1).setRequestMethod("GET");
        ((HttpURLConnection)localObject1).setConnectTimeout(5000);
        ((HttpURLConnection)localObject1).connect();
        localObject2 = new java/lang/String;
        ((String)localObject2).(d.a(((HttpURLConnection)localObject1).getInputStream()));
        localObject1 = new java/lang/StringBuilder;
        ((StringBuilder)localObject1).();
        m.d("Analytics-UpdateManager", "result " + (String)localObject2);
        localObject1 = new org/json/JSONObject;
        ((JSONObject)localObject1).((String)localObject2);
        localObject3 = ((JSONObject)localObject1).optString("url");
        i = ((JSONObject)localObject1).optInt("code", 0);
        localObject2 = ((JSONObject)localObject1).optString("v");
        f.a(this.A, ((JSONObject)localObject1).optInt("force", 0));
        f.a(this.A, ((JSONObject)localObject1).optBoolean("wifi", true));
        if ((!TextUtils.isEmpty((CharSequence)localObject3)) && (!TextUtils.isEmpty((CharSequence)localObject2)))
        {
          localObject4 = new com/miui/analytics/internal/a;
          ((a)localObject4).((String)localObject2);
          if ((b.q()) || (((a)localObject4).a == 0))
          {
            f.a(this.A, ((JSONObject)localObject1).optString("md5"));
            f.b(this.A, (String)localObject3);
            f.c(this.A).execute(this.A.aP);
          }
        }
        while (i != -8) {
          return;
        }
        long l2 = f.c(this.A, ((JSONObject)localObject1).optString("failMsg"));
        l1 = l2;
        i = j;
      }
      catch (Exception localException)
      {
        f.a(this.A, 0L);
        m.e("Analytics-UpdateManager", "exception ", localException);
        i = j;
      }
    }

As you can see, it makes a request to http://sdkconfig.ad.xiaomi.com/api/checkupdate/lastusefulversion? which is of course an official Xiaomi domain. It sends some parameters with it: including IMEI, MAC address, Model, Nonce, Package name and signature.

After the above code has been executed, it might get an (updated) apk file back. Inside e.class this APK file gets downloaded:

public void run()
  {
    try
    {
      if ((!k.m(f.b(this.A))) && (f.d(this.A))) {}
      for (;;)
      {
        return;
        Object localObject1 = new java/net/URL;
        ((URL)localObject1).(f.e(this.A));
        localObject1 = (HttpURLConnection)((URL)localObject1).openConnection();
        ((HttpURLConnection)localObject1).setRequestMethod("GET");
        ((HttpURLConnection)localObject1).setConnectTimeout(5000);
        ((HttpURLConnection)localObject1).connect();
        if (((HttpURLConnection)localObject1).getResponseCode() == 200)
        {
          Object localObject2 = d.a(((HttpURLConnection)localObject1).getInputStream());
          localObject1 = localObject2;
          Object localObject3;
          if (!TextUtils.isEmpty(f.f(this.A)))
          {
            localObject3 = a.a((byte[])localObject2);
            localObject1 = localObject2;
            if (!f.f(this.A).equalsIgnoreCase((String)localObject3)) {
              localObject1 = null;
            }
          }
          if (localObject1 != null)
          {
            Log.d("Analytics-UpdateManager", "download apk success.");
            localObject2 = new java/io/File;
            ((File)localObject2).(f.g(this.A));
            localObject3 = new java/io/FileOutputStream;
            ((FileOutputStream)localObject3).((File)localObject2);
            ((FileOutputStream)localObject3).write((byte[])localObject1);
            ((FileOutputStream)localObject3).close();
            f.h(this.A);
          }
        }
      }
...


The download location for the APK is set in f.class, where also the 24h time check was placed:

private String G()
  {
    try
    {
      Object localObject = new java/lang/StringBuilder;
      ((StringBuilder)localObject).();
      localObject = this.mContext.getExternalCacheDir().getAbsolutePath() + "/Analytics.apk";
      return (String)localObject;
    }
    catch (Exception localException)
    {
      for (;;)
      {
        String str = "";
      }
    }
  }


Now the question is, where does this APK gets installed? I couldn’t find any proof inside the Analytics app itself, so I’m guessing that a higher privileged Xiaomi app runs the installation in the background. The question is then: does it verify the correctness of the APK, and does it make sure that it is in fact an Analytics app? If it does not, that means Xiaomi can install any app on your device it wants, as long as it’s named Analytics.apk.

Update 12:31: Someone told me the package gets installed from l.class, with following code:

try
    {
      paramContext.getPackageManager().getClass().getMethod("installPackage", new Class[] { Uri.class, Class.forName("android.content.pm.IPackageInstallObserver"), Integer.TYPE, String.class }).invoke(paramContext.getPackageManager(), new Object[] { Uri.parse(paramString), null, paramContext.getPackageManager().getClass().getField("INSTALL_REPLACE_EXISTING").get(null), null });
      m.d("AppInstaller", "install apk success.");
      return;
    }
...


It seems like there indeed is no validation on what APK is getting installed. So it looks like Xiaomi can replace any (signed?) package they want silently on your device within 24 hours. And I’m not sure when this AppInstaller gets called, but I wonder if it’s possible to place your own Analytics.apk inside the correct dir, and wait for it to get installed (edit: getExternalCacheDir() is inside the app’s sandbox, so probably not). But this sounds like a vulnerability to me anyhow, since they have your IMEI and Device Model, they can install any apk for your device specifically.

If you own a Xiaomi device yourself, you might want to block all access to Xiaomi related domains, because by far this isn’t the only request to a Xiaomi site. I use AdAway for this. It does require root access, but that should be no problem if you run the International ROM. I don’t know if the official rom supports root access out of the box.
My AdAway:

Here is a link to a post with other bloatware apps you can safely remove from your device, next to Analytics: https://forum.xda-developers.com/xiaomi-mi-3/general/tip-safe-to-remove-bloatware-list-miui-t2999283

If anyone has tips or a comment, please email or contact me.

Script for removing Windows 7 updates that should “ease the upgrade to Windows 10”

For those who are running Windows 7 and (just like me) don’t want to upgrade to Windows 10, here’s a script to remove all Microsoft updates contributing to the upgrade. It fetches the documentation for all installed updates (not security related and starting from 2015) and looks for certain keywords inside that documentation. Currently it removes everything mentioning “Windows 10”, “ease the upgrade experience”, “upgrading” or “telemetry”. You can change this to your own words inside the script.
Note, this should also remove KB3035583 that gives the Windows 10 upgrade icon in the taskbar. That tray icon should be gone after running the script.
It also tries to remove all telemetry features which collects all sorts of user activity and sends it to Microsoft. It is enabled in Windows 10 by default and Microsoft hasn’t provided an option to completely disable it for Home and Pro users. Some telemetry updates are installed in Windows 7 as well, for example KB3068708.

The script is written in Python2. If you don’t have it, install the latest version here: https://www.python.org/downloads/. I have only tried it on Windows 7 x86, but it probably works on 8(.1) and 64-bit as well.

import subprocess
import urllib2
import sys
import time

keywords = [" ease the upgrade experience ", " Windows 10 ", " upgrading ", " telemetry "] #keywords to look for inside the documentation
startKB = 0 #start from a certain KB number

removing = []
kbs = []

def run_command(command):
    p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    return iter(p.stdout.readline, b'')

print "Getting installed updates list..."
command = 'wmic qfe list'.split()
for line in run_command(command):
     for str in line.split(" "):
          if str.startswith("KB") and not "Security Update" in line and "2015" in line:
               kb = str[2:]
               if int(kb) > int(startKB):
                    kbs.append(kb)

print "Found %s updates" % (len(kbs))
print "Fetching KB documentations (this might take a while)..."
i=1
for kb in kbs:
    time.sleep(1)
    url = "https://support.microsoft.com/api/content/kb/"+kb
    req = urllib2.Request(url)
    try:
        f = urllib2.urlopen(req)
        html = f.read()
        if any(x in html for x in keywords):
            removing.append(kb)
            print "[%i] KB%s: Bad, see %s" % (i, kb, url)
        else:
            print "[%i] KB%s: Good" % (i, kb)
    except urllib2.URLError as e:
        print "[%i] KB%s: %s" % (i, kb, e.reason)
    i=i+1

print
if len(removing) == 0:
    print "Nothing found!"
    sys.exit(1)

print "Ready to remove following updates:"
print removing
print "They will be uninstalled one by one. You can choose individually whether you want it removed or not."
proceed =  raw_input("Continue? [Y,n] ")
print
if (proceed == "") or (proceed == "y") or (proceed == "Y") or (proceed == "yes"):
    for kb in removing:
        print "Removing KB%s..." % kb
        subprocess.call(["wusa.exe","/uninstall","/kb:"+kb,"/norestart"], shell=True)

 

Bruteforcing coupon codes for discount

I sometimes buy stuff from Chinese webstores because of their low prices. Now the yuan value is dropping it is now cheaper than ever to ship products from China.

Banggood.com is one of the more popular Chinese webshops. I was looking around on their website and comparing prices when I found the coupon code b185f7 by googling for 5% off.

I noticed there were more coupons of form b185f7: six characters long, only letters and numbers. I tried them with caps and without caps; it didn’t matter. That means there are only 36 possibilities for every character (0-9a-z) which gives a total possibility of 36^6. That’s not very much: enough to try a bruteforce (a full bruteforce will still take some time; I’m trying it randomly in this post).
Edit: possibly it’s in Hex, which limits the possibilities to 16^6 (0-9a-f), which is even lower and much faster to bruteforce. I didn’t test it, however.

A further thing I noticed that it is unfortunately not possible to enter more than one coupon on a single order. You can only use one at a time. That is a bummer because now two coupons with 5% off won’t give you more discount.
One thing that is good in my case, is that you can endlessly try to enter coupons. It doesn’t matter if they are valid or not; it won’t disable the field after a few wrong tries. And it doesn’t give a captcha to solve.

I wrote the little bash script below to try many coupon possibilities by randomly generating them (not really bruteforcing, just hoping we’re lucky):

#!/bin/bash

while [ 1 ]
do

  couponcode=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 6 | head -n 1)
  curling=$(curl -sS --data "com=shopcart&t=useCoupon&coupon_code=$couponcode" -H 'Cookie: banggood_SID=0f18fd4cf40bfb1dec646807c7fa5522' "https://www.banggood.com/index.php")

  if [[ $curling == *"Coupon is only allowed"* ]] || [[ $curling == *"Invalid"* ]] || [[ $curling == *"expired"* ]] || [[ $curling == "" ]]
then
    echo "$couponcode invalid";
  else
    echo "$couponcode => $curling" >> win.txt;
    echo "$couponcode VALID";
  fi

  sleep 5
done

As you see, it sends a curl request to the banggood website with my session id connected to my cart. I’m trying a infinite amount of time if a random coupon code I get is valid or not. If it gives the message “Coupon is not allowed” or “Invalid Coupon Code” if the code is invalid.
I’m using /dev/urandom as a randomness source and with tr and fold I make sure it is 6 characters long and only contains numbers and letters. As mentioned, caps or not does not matter.

I ran the script for a few hours and it didn’t take long to find valid ones. Unfortunately, they are either only 5% off or only for a specific user account. My hopes were I found more than 5% discount but that wasn’t the case.

I contacted banggood customer service (with only mail address on their site) on the 17th of August but they did not respond. I explained to them what was wrong with their coupon code system and how to fix it. I explained that 6 characters case insensitive isn’t cryptographically secure enough to prevent people from cheating. As a solution I told them to set a maximum on the amount of wrong coupon codes that can be entered. That would solve the problem without breaking functionality.

Unfortunately they didn’t respond. Here are some coupon codes I found while running the script for a short period of time. They are valid coupon codes for 5% discount each:

5bf7bb is 5%
edb44d is 5%
533876 is 5%
e7e744 is 5%
cc9ec5 is 5%
25d342 is 5%

If you are a website owner or developer and you run a webshop, make sure your coupon codes are not easily crackable. Many webshops use original names for their coupon codes instead of generating them, like ‘christmas5off’ or something. These are more difficult to bruteforce (but dictionary may have small chance of success) and therefore more safe to use.