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("miui_sdkconfig_jafej!@#)(*e@!#");
        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.

Running a separate email domain

For as long as I can remember, I have always had my primary email hosted with my Isp, which was @Home at that time. It has become Ziggo since then but luckily they didn’t give up on my @home alias.

I thought it was time for a change and now I’m the proud owner of a domain with a new tld: broenink.email. I’m expanding my broenink domains, so far I own thijsbroenink.com thijsbroenink.nl and thijs.xyz and now also broenink.email.

There are a few considerations why I decided to go for this new approach. First of all, new tlds are the future. There are appearing new ones often nowadays which is also why I registered thijs.xyz already. Google has shown this new tld can be successful by pronouncing their mother company Alphabet on abc.xyz. And when you’re choosing a domain for email, why not use email as tld itself, so it’s clear to everyone what it’s used for.

Another consideration for me to not just keep a single email address is for spam reasons. I can now sign up on websites with for example [email protected] for Dropbox. I can use a subdomain as alias so I know where spam mails are coming from. If there is a leak or I am receiving spam from a website I trusted, I can lead it back to that site by proving the from address. And I can block certain domains if I don’t want to receive any mails from them anymore. It gives me full control on which emails I want to receive from whom.

That is the why; now the how. I’m still working things out on how to block certain domains. For now, I have a wildcard cname record in my DNS setting, which allows every subdomain to be accepted by an sending email client. For the email service itself, it would be best to install Postfix with SpamAssassin, but I found this to much of a hassle, so I went for a cloud approach: Zoho Mail. They offer mailing for businesses with a single custom domain for free. And they also support subdomain stripping, which forwards every subdomain to my primary email inbox. So [email protected] gets forwarded to [email protected]. And finally, I don’t have to worry about webmail interface, because Zoho already delivers that. And all for free.

If anyone has a tip or similar (better) approach, let me know at [email protected] !

Edit: I created two scripts to add and remove CNAME records directly into DigitalOcean. They override the wildcard CNAME record and forward mails to google.com, in which Google will answer with a Mail Delivery Failed. This allows blacklisting certain [email protected] subdomains (code is on Github).

r = requests.post('https://api.digitalocean.com/v2/domains/'+domain+'/records',
 data={"type":"CNAME","name":value,"data":"google.com.","priority":"null","port":"null","weight":"null"},
 headers={"Authorization":"Bearer "+api_key})