← Home

Lenovo mobile spying on its customers

tl;dr: Lenovo smartphones come with a preinstalled spying service. It sends plaintext reports of screen resolution, imei, imsi, coordinates, os events and much more.


Casual find

The last week I was investigating on the functionality of a bike sharing app to look for vulnerabilities and logical errors in the protocol.
Before reversing that app i configured my Android device, a Lenovo A660 with a modified stock ROM (for completeness, the ROM is this), to pass all traffic trough BurpSuite.

I soon noticed bizarre POST requests like these ones:

POST /reaper/server/report HTTP/1.1
User-Agent: 1.5.5/1 (Linux; U; Android 4.0.4; en-gb; A660; Build/IMM76D; ; Lenovo)
Content-Length: 1887
Content-Type: binary/octet-stream
Host: fsr.lenovomm.com
Connection: Keep-Alive

ctx=1.8.7.2!0!<UniqueDeviceID>!<IMEI>!<AndroidID>!1822452687!<ScreenResolution>!1305262191!<FirstPowerOnTimestamp>!<PowerOnTimestamp>!<UnknownTimestamp>!<RequestTimestamp>!132!3!imei!<IMEI>!All&evt=OSEvent!S28!!105!

ctx=1.8.7.2!0!<UniqueDeviceID>!<IMEI>!<AndroidID>!845659937!<ScreenResolution>!<BuildTimestamp>!<FirstPowerOnTimestamp>!<PowerOnTimestamp>!<UnknownTimestamp>!<RequestTimestamp>!132!3!imei!<IMEI>!All&evt=OSEvent!S12!!1!(2!version!9!3)(3!pkg!com.paessler.prtgandroid!3)

ctx=1.8.7.2!0!<UniqueDeviceID>!<IMEI>!<AndroidID>!584814439!<ScreenResolution>!<BuildTimestamp>!<FirstPowerOnTimestamp>!<PowerOnTimestamp>!<UnknownTimestamp>!<RequestTimestamp>!132!3!imei!<IMEI>!All&evt=OSEvent!S08!!1!(1!status!on!3)

As you might see this send A LOT of personal information without asking any permission, silently in backgroud.

So, just looking at the string i was able to identify most of the value meanings, which are already substituded in the example above.
Also, other than the value identified is possible to notice the specification on the event that generated the report. On the second request for example OSEvent!S12! means the installation of an application, and (3!pkg!com.paessler.prtgandroid!3) is the name of the application.

Googling about the hostame it comes ou that another user already noticed the backdoor, but he didn’t investigate further.
Also Lenovo didn’t replied to his post on the official forum.

Getting the spyware

After inspecting the network activity i decided to find exactly which component of the preinstalled Lenovo shit was originating the requests.
Standing on the fast analysis of the user who posted on reddit, everything is his opinion was backdoor, so i copied the whole /system/app to my computer and just chose to analyze the apk that in my opinion looked suspect.

Here is the list of apk with Lenovo in the filename:

LenovoBackupRestore.apk
LenovoOTA.apk
LenovoServiceFramework.apk 
LenovoUEService.apk  

I picked up first LenovoEUService.apk, unzipped it, ran dex2jarand the jd-gui.
Here is the tree of the result:


├── android
│   ├── annotation
│   │   ├── SuppressLint.java
│   │   └── TargetApi.java
│   └── content
│       └── pm
│           └── IPackageStatsObserver.java
└── com
    └── lenovo
        ├── lps
        │   └── reaper
        │       └── sdk
        │           ├── AnalyticsTracker.java
        │           ├── AnalyticsTrackerBuilder.java
        │           ├── ReaperActivity.java
        │           ├── ReaperListActivity.java
        │           ├── ReaperTabActivity.java
        │           ├── api
        │           │   ├── CustomParameter.java
        │           │   ├── CustomParameterManager.java
        │           │   ├── DispatchCallback.java
        │           │   ├── Event.java
        │           │   └── EventDao.java
        │           ├── config
        │           │   └── Configuration.java
        │           ├── request
        │           │   ├── ConfigurationUpdateTask.java
        │           │   ├── HttpRequestHandler.java
        │           │   ├── ReaperServerAddressQueryTask.java
        │           │   ├── ReportManager.java
        │           │   ├── TaskHandler.java
        │           │   └── WorkerTask.java
        │           ├── storage
        │           │   ├── EventStorage.java
        │           │   ├── FileEventDaoImpl.java
        │           │   ├── FileStorage.java
        │           │   ├── FileStorageMeta.java
        │           │   └── ServerConfigStorage.java
        │           └── util
        │               ├── AnalyticsTrackerUtils.java
        │               ├── CompressUtil.java
        │               ├── Constants.java
        │               ├── Messages.java
        │               ├── PlusUtil.java
        │               ├── ReaperAppManager.java
        │               └── TLog.java
        └── ue
            └── service
                ├── BuildConfig.java
                ├── DataReaperHandler.java
                ├── ExternalStorageInfoReceiver.java
                ├── GpsStateObserver.java
                ├── LenovoUEServiceActivity.java
                ├── LenovoUEServiceReceiver.java
                ├── PackageInfoReceiver.java
                ├── R.java
                ├── ReaperService.java
                ├── TrafficStatsReceiver.java
                └── util
                    └── Constants.java

File: /com/lenovo/lps/reaper/sdk/config/Configuration.java


  private static final String DEFAULT_ANALYTICS_TRACKER_BUILDER_CLASS = "com.lenovo.lps.reaper.sdk.AnalyticsTrackerBuilder";
  private static final int EVENT_CACHE_SIZE = 50;
  private static final long MAX_CACHE_INTERVAL_MS = 5000L;
  private static final int MAX_CONNECTIONS_PER_TASK = 5;
  private static final int MAX_EVENTS_PER_REQUEST = 30;
  private static final String REAPER_SERVER_CONTEXT_CONFIGURATION = "/reaper/server/config";
  private static final String REAPER_SERVER_CONTEXT_POST = "/reaper/server/post";
  private static final String REAPER_SERVER_CONTEXT_REPORT = "/reaper/server/report";

File: /com/lenovo/lps/reaper/sdk/request/ReaperServerAddressQueryTask.java

private static final String TAG = "ReaperServerAddressQueryTask";
private static final String defaultReaperServerUrl = "http://fsr.lenovomm.com";
private static final String reaperServerAddressQueryUrl = "http://lds.lenovomm.com/addr/1.0/query?sid=rfsr001&didt=1";

The following file contains the description of all the events logged: /com/lenovo/ue/service/DataReaperHandler.java

 public void reportBatteryEvent(int paramInt)
  {
    analyticsTracker.trackEvent("OSEvent", "S24", "", paramInt);
  }

  void reportBootEvent()
  {
    long l = SystemClock.elapsedRealtime() - 30000L;
    analyticsTracker.trackEvent("OSEvent", "S03", "", (int)((float)l / 1000.0F));
    TelephonyManager localTelephonyManager = (TelephonyManager)this.context.getSystemService("phone");
    analyticsTracker.setParam(1, "deviceidtype", getDeviceidType(localTelephonyManager));
    analyticsTracker.setParam(2, "imsi", getSubscriberIdByTelMgr(localTelephonyManager));
    analyticsTracker.setParam(3, "devicebrand", Build.BRAND);
    AnalyticsTracker localAnalyticsTracker = analyticsTracker;
    if ("".equals(localTelephonyManager.getSimOperator()));
    for (String str = "unknown"; ; str = localTelephonyManager.getSimOperator())
    {
      localAnalyticsTracker.setParam(4, "simoperator", str);
      analyticsTracker.setParam(5, "fwversion", getSoftwareVersion("framework"));
      analyticsTracker.trackEvent("OSEvent", "S21", "", 1);
      return;
    }
  }

  public void reportBuletoothEnabledEvent(int paramInt)
  {
    switch (paramInt)
    {
    case 11:
    default:
    case 10:
    case 12:
    }
    while (true)
    {
      return;
      analyticsTracker.trackEvent("OSEvent", "S23", "", 100);
      continue;
      analyticsTracker.trackEvent("OSEvent", "S23", "", 101);
    }
  }

  public void reportExternalStorageEvent(long paramLong1, long paramLong2)
  {
    analyticsTracker.setParam(1, "totalsize", String.valueOf(paramLong1));
    analyticsTracker.setParam(2, "availablesize", String.valueOf(paramLong2));
    analyticsTracker.trackEvent("OSEvent", "S35", "", 1);
  }

  void reportGpsDataEvent()
  {
    Location localLocation = ((LocationManager)this.context.getSystemService("location")).getLastKnownLocation("gps");
    if (localLocation == null);
    while (true)
    {
      return;
      long l = System.currentTimeMillis();
      double d1 = localLocation.getLongitude();
      double d2 = localLocation.getLatitude();
      String str = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(l));
      analyticsTracker.setParam(1, "gpstime", str);
      analyticsTracker.setParam(2, "position-longitude", String.valueOf(d1));
      analyticsTracker.setParam(3, "position-latitude", String.valueOf(d2));
      analyticsTracker.trackEvent("OSEvent", "S09", "", 1);
    }
  }

  void reportGpsEnableEvent(boolean paramBoolean)
  {
    AnalyticsTracker localAnalyticsTracker = analyticsTracker;
    if (paramBoolean);
    for (String str = "on"; ; str = "off")
    {
      localAnalyticsTracker.setParam(1, "status", str);
      analyticsTracker.trackEvent("OSEvent", "S06", "", 1);
      return;
    }
  }

  public void reportHeadsetPlugEvent(Intent paramIntent)
  {
    if (paramIntent.getIntExtra("state", 0) == 0)
      analyticsTracker.trackEvent("OSEvent", "S34", "", 116);
    while (true)
    {
      return;
      analyticsTracker.trackEvent("OSEvent", "S34", "", 115);
    }
  }

  public void reportInstalledPackagesEvent(String paramString1, int paramInt, String paramString2, String paramString3, String paramString4)
  {
    analyticsTracker.setParam(1, "pkg", paramString1);
    analyticsTracker.setParam(2, "appname", paramString2);
    analyticsTracker.setParam(3, "version", Integer.toString(paramInt));
    analyticsTracker.setParam(4, "pkgtype", paramString4);
    analyticsTracker.trackEvent("OSEvent", "S26", paramString3, 1);
  }

  public void reportMediaPlugEvent(int paramInt)
  {
    analyticsTracker.trackEvent("OSEvent", "S33", "", paramInt);
  }

  void reportMobileTrafficStats()
  {
    long l = (TrafficStats.getMobileRxBytes() + TrafficStats.getMobileTxBytes()) / 1024L;
    analyticsTracker.trackEvent("OSEvent", "S02", "", (int)l);
  }

  public void reportNetWorkEnabledEvent(NetworkInfo.State paramState)
  {
    if (NetworkInfo.State.CONNECTED == paramState)
      analyticsTracker.setParam(1, "status", "on");
    while (true)
    {
      analyticsTracker.trackEvent("OSEvent", "S22", "", 1);
      return;
      analyticsTracker.setParam(1, "status", "off");
    }
  }

  void reportPackageEvent(String paramString1, String paramString2, int paramInt, String paramString3, long paramLong, String paramString4)
  {
    analyticsTracker.setParam(1, "appname", paramString2);
    analyticsTracker.setParam(2, "version", Integer.toString(paramInt));
    analyticsTracker.setParam(3, "pkg", paramString3);
    if (paramString4 != null)
      analyticsTracker.setParam(4, "source", paramString4);
    if (paramLong > 0L)
      analyticsTracker.setParam(5, "pkgsize", String.valueOf(paramLong));
    analyticsTracker.trackEvent("OSEvent", paramString1, "", 1);
  }

  public void reportPowerConnectionChangeEvent(int paramInt)
  {
    analyticsTracker.trackEvent("OSEvent", "S31", "", paramInt);
  }

  void reportPushSettingEvent(boolean paramBoolean)
  {
    AnalyticsTracker localAnalyticsTracker = analyticsTracker;
    if (paramBoolean);
    for (String str = "on"; ; str = "off")
    {
      localAnalyticsTracker.setParam(1, "status", str);
      analyticsTracker.trackEvent("OSEvent", "S07", "", 1);
      return;
    }
  }

  public void reportRingerModeChangeEvent(int paramInt)
  {
    int i = getRingerMode(paramInt);
    analyticsTracker.trackEvent("OSEvent", "S29", "", i);
  }

  public void reportSIMChangeEvent(String paramString)
  {
    analyticsTracker.trackEvent("OSEvent", "S32", paramString, 1);
  }

  public void reportScreenEvent(int paramInt)
  {
    analyticsTracker.trackEvent("OSEvent", "S28", "", paramInt);
  }

  void reportShutdownEvent()
  {
    reportTotalTrafficStats();
    reportMobileTrafficStats();
    analyticsTracker.trackEvent("OSEvent", "S04", "", 1);
  }

  public void reportTimeZoneChangeEvent(Intent paramIntent)
  {
    analyticsTracker.trackEvent("OSEvent", "S25", paramIntent.getStringExtra("time-zone"), 1);
  }

  void reportTotalTrafficStats()
  {
    long l = (TrafficStats.getTotalRxBytes() + TrafficStats.getTotalTxBytes()) / 1024L;
    analyticsTracker.trackEvent("OSEvent", "S01", "", (int)l);
  }

  public void reportTrafficStatsEvent(long paramLong, String paramString)
  {
    analyticsTracker.setParam(1, "size", String.valueOf(paramLong));
    analyticsTracker.setParam(2, "type", paramString);
    analyticsTracker.trackEvent("OSEvent", "S27", "", 1);
  }

  void reportWifiEnabledEvent(int paramInt)
  {
    switch (paramInt)
    {
    case 2:
    default:
    case 1:
    case 3:
    }
    while (true)
    {
      return;
      analyticsTracker.setParam(1, "status", "off");
      analyticsTracker.trackEvent("OSEvent", "S08", "", 1);
      continue;
      analyticsTracker.setParam(1, "status", "on");
      analyticsTracker.trackEvent("OSEvent", "S08", "", 1);
    }
  }

  public void setScreenOff(boolean paramBoolean)
  {
    isScreenOff = paramBoolean;
  }

Here is the function that builds the parameters that are sent with each request:
File: /com/lenovo/lps/reaper/sdk/request/HttpRequestHandler.java

public class HttpRequestHandler
{
  private static final String EVENT_DATA_URL = "ctx=%s!0!%s!%s!%s!%d!%s!%d!%d!%d!%d!%d!%d!%d!%s!%s!%s&evt=%s!%s!%s!%d!%s";
  private static final String TAG = "HttpRequestHandler";
  private static final String VIEW_DATA_URL = "ctx=%s!%s!%s!%s!%s!%d!%s!%d!%d!%d!%d!%d!%d!%d!%s!%s!%s&evt=%s!%s";
  private Configuration configuration;

  private String getEventData(Event paramEvent)
  {
    String str2;
    if (paramEvent.getCategory().equals("__PAGEVIEW__"))
    {
      Object[] arrayOfObject2 = new Object[19];
      arrayOfObject2[0] = "1.8.7.2";
      arrayOfObject2[1] = Integer.valueOf(paramEvent.getValue());
      arrayOfObject2[2] = paramEvent.getAccount();
      arrayOfObject2[3] = this.configuration.getDeviceId();
      arrayOfObject2[4] = this.configuration.getAndroidId();
      arrayOfObject2[5] = Integer.valueOf(paramEvent.getRandomVal());
      arrayOfObject2[6] = this.configuration.getDisplayScreen();
      arrayOfObject2[7] = Integer.valueOf(paramEvent.getSessionId());
      arrayOfObject2[8] = Long.valueOf(paramEvent.getTimestampFirst());
      arrayOfObject2[9] = Long.valueOf(paramEvent.getTimestampPre());
      arrayOfObject2[10] = Long.valueOf(paramEvent.getTimestampCur());
      arrayOfObject2[11] = Long.valueOf(paramEvent.getTimestampEvent());
      arrayOfObject2[12] = Integer.valueOf(paramEvent.getVisits());
      arrayOfObject2[13] = Integer.valueOf(paramEvent.getNetworkStatus());
      arrayOfObject2[14] = PlusUtil.DeviceIdentify.getDeviceType();
      arrayOfObject2[15] = AnalyticsTrackerUtils.encode(PlusUtil.DeviceIdentify.getDeviceID());
      arrayOfObject2[16] = this.configuration.getChannel();
      arrayOfObject2[17] = AnalyticsTrackerUtils.encode(paramEvent.getAction());
      arrayOfObject2[18] = AnalyticsTrackerUtils.getCustomVariableParams(paramEvent);
      str2 = String.format("ctx=%s!%s!%s!%s!%s!%d!%s!%d!%d!%d!%d!%d!%d!%d!%s!%s!%s&evt=%s!%s", arrayOfObject2);
      return str2;
    }
    Object[] arrayOfObject1 = new Object[21];
    arrayOfObject1[0] = "1.8.7.2";
    arrayOfObject1[1] = paramEvent.getAccount();
    arrayOfObject1[2] = this.configuration.getDeviceId();
    arrayOfObject1[3] = this.configuration.getAndroidId();
    arrayOfObject1[4] = Integer.valueOf(paramEvent.getRandomVal());
    arrayOfObject1[5] = this.configuration.getDisplayScreen();
    arrayOfObject1[6] = Integer.valueOf(paramEvent.getSessionId());
    arrayOfObject1[7] = Long.valueOf(paramEvent.getTimestampFirst());
    arrayOfObject1[8] = Long.valueOf(paramEvent.getTimestampPre());
    arrayOfObject1[9] = Long.valueOf(paramEvent.getTimestampCur());
    arrayOfObject1[10] = Long.valueOf(paramEvent.getTimestampEvent());
    arrayOfObject1[11] = Integer.valueOf(paramEvent.getVisits());
    arrayOfObject1[12] = Integer.valueOf(paramEvent.getNetworkStatus());
    arrayOfObject1[13] = PlusUtil.DeviceIdentify.getDeviceType();
    arrayOfObject1[14] = AnalyticsTrackerUtils.encode(PlusUtil.DeviceIdentify.getDeviceID());
    arrayOfObject1[15] = this.configuration.getChannel();
    arrayOfObject1[16] = AnalyticsTrackerUtils.encode(paramEvent.getCategory());
    arrayOfObject1[17] = AnalyticsTrackerUtils.encode(paramEvent.getAction());
    if (paramEvent.getLabel() == null);
    for (String str1 = ""; ; str1 = AnalyticsTrackerUtils.encode(paramEvent.getLabel()))
    {
      arrayOfObject1[18] = str1;
      arrayOfObject1[19] = Integer.valueOf(paramEvent.getValue());
      arrayOfObject1[20] = AnalyticsTrackerUtils.getCustomVariableParams(paramEvent);
      str2 = String.format("ctx=%s!0!%s!%s!%s!%d!%s!%d!%d!%d!%d!%d!%d!%d!%s!%s!%s&evt=%s!%s!%s!%d!%s", arrayOfObject1);
      break;
    }
  }

The spyware has also other functions like an automatic configuration updater, and other checks that i didn’t inspected due to lack of time (and java skills).

However I'll soon give a look into other stock APKs.

Fix

The fastest fix i thought is just to block the host the report are sent to, so i simply added

127.0.0.1 *.lenovomm.com

to my /etc/hosts file.
It is also possible to directly remove the app (as I did when I ended the analysis), since it does not provide any essential functionality for the phone.
Be aware that the app calls itself User Experience, so look for that when removing.

Downloads

Original APK - Download
sha256 f004bc2ffc93e90e58fc7f931c87000cf36c2f1bcda67bd2ea9a96170d8fb285

Decompiled Source - Download
sha256 559184964591d3f0fde2206beae742b49adfd1747044ecd5df0d0f79c0f2069d