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 thijs@dropbox.broenink.email 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 thijs@dropbox.broenink.email gets forwarded to thijs@broenink.email. 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 thijs@betterapproach.broenink.email !

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 thijs@subdomain.broenink.email 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})

Did you know you can Facebook people by phone or email?

Facebook is the first place you normally go when you need information about someone. But what many people I found out are not aware of is that it’s possible to look people up on Facebook by their mail address and phone number.
 
If someone is mailing you or left his email on a site somewhere, big chance you can find his name, profile picture, social connections (friends and family) and location by just searching for that email address, even when he’s using a pseudonym or nickname. Facebook makes doxing more easy than ever: you don’t even have to know the full name in order to get all his personal details. To do so, just go to Facebook.com, but instead of searching for a name, enter a phone number or email. The corresponding profile should show up now.
 
 

Some use cases: next time you have an unknown caller, just enter the number in Facebook and maybe you’ll find out who it is. Same thing if you want to know the person behind a forum profile but you only have one mail address. Or think about this: an advertisement on Marktplaats.nl / Ebay with a mobile phone number to contact the seller. You can easy verify if it’s legit now. There are many ways this can be an useful tool. I have found a few people myself by this, and people are always surprised when I know their full name and details after just getting one WhatsApp message.

How to disable this for your own profile?

Facebook placed annoying popups a while back to “ask” you to enter your number in order to “make your account more secure”. Great chance you fell for this and also entered your own mobile phone. If you did that, then probably everyone can find you as long as they have your number. Luckily it’s easy to disable. Edit your privacy settings on Facebook. Go to: Settings > Privacy > ‘Who can look me up?’ and change ‘phone number’ to ‘Friends’.

 
Here it’s also possible to (partly) disable lookup by your email address. I have set both to ‘Friends only’ as you can see above. It is always a good idea to check all the privacy settings to make sure you’re not sharing things you don’t want. I personally have set my profile as strict as possible, also for the things I’m posting. People should be careful with what they put on the public web, or at least be aware of what a stranger can see. It is most of the time more than they realize.