The gift that keeps on taking
A digital photo frame bought on Amazon joins a botnet
Sometimes an innocuous word in one language translates to something starkly different in another. Take, for example, “gift”. In English, as a noun, it’s synonymous with a present:
Something that you are given, without asking for it, on a special occasion, especially to show friendship, or to say thank you
Meanwhile, :
Somewhat fitting, for a recent experience where a Christmas present (coincidentally from a relative in Germany) certainly ticked the box of providing something nobody asked for.
My white hat says “dunce” on it
Set your expectations low. Lower. I am not a security researcher. I am not a developer. What I know about Android can be described in the time taken to boil an egg. I hope you like the yolk runny.
What follows are my efforts as a one-time engineer and some-time tinkerer to solve a problem — one which happened to reveal malware and botnet activity.
There were no references I could find for this particular malware infecting this particular type of thing, so I’ve written what I know. I don’t know much, but it might be useful to someone.
I tend to ramble. Feel free to jump down to here for the interesting bit.
The setup
My father’s internet connection is via 4G. That’s another story, but it comes down to BT and Openreach being fascinatingly inept in keeping a length of copper wire a) in one piece and b) conducting low voltage electricity. Basic phone service was the aspiration. Broadband? you’d have more success transmitting data over sea kelp.
With a fifth fault in four continuous months of outage, we’d finally had it. Escaping the inertia of 45 years’ loyalty, we invited BT to get lost. BT, seeing a way out of an expensive engineering problem, happily got lost.
Nowadays, my dad’s internet connection supports a VOIP service for his landline and an Android tablet. His usage is very light: SIP, browsing for unspeakable bric-a-brac on eBay, and the occasional WhatsApp video call. The usage pattern is well established. We use a dual-SIM Teltonika router, it providing a degree of resiliency as well as the fabulous Teltonika Remote Management Service (RMS). RMS is especially useful as he is a two-hour drive and ferry crossing away from me. He also has a smart TV that, for all its claims, remains something of an underachiever, relegated to receiving FTA broadcast channels.
It is unexciting technology. Bruised by years of clever things breaking horribly, I very much like unexciting technology.
Omens of ill tidings
The first sign something durn gone wrong down on the old homestead came via news of an unexpectedly sharp increase in data usage.
An email from the 4G provider reported almost all of the data plan’s 50 GB allowance had been used. This was followed by a second email, three days later, and still only part way through the month, letting us know that we’d used our data allowance up.
A change of 10 GB in 3 days is equivalent to a month of normal data usage, and exhausting the plan meant an overall monthly usage of more than five times that normally expected.
With the main SIM off in a snit, the router failed over to its standby SIM on a separate provider.
The investigation was mobilised in great haste. Photos pinned with strings run between, and persons of interest questioned. Had someone set up Netflix? been monstering the connection from their own devices while visiting? All suspects swore nothing of the sort, even under duress. Enquiries were put on hold, pending forensics and polygraphs.
Remote possibilities
No clearer on what was happening, the problems worsened. Attempts to access the router remotely were frustrated by persistent timeouts. RMS showed that the telemetry service on the router had been trying to call home, but utterly tired of wading through molasses-like network conditions, had given up.
People who have supported on-prem infrastructure will well know the wash of dread that accompanies Something Very Far Away (and usually Very Important) dropping off the network.
The equipment was powered off and back on by a family member closer to the scene. This was a last gasp effort at getting the gear doing something sensible before accepting fate’s dealings and taking to the seas. Surprisingly, and altogether magically, this provided a brief lull in whatever had been going on and allowed access back into the router.
Even then it remained a tortuously slow experience. Something very unusual was going on — and it was going on right now.
The little engine that shouldn’t
That morning alone, shuffling packets along with its usual quiet efficiency it had a been a busy little router. Very busy, in fact:
4 GB may not appear much by the standards of the average metropolis, but it’s all relative: this is a rural cellular connection that will manage 60ish Mbps down on its very bestest day, and even then, at latencies I’m too embarrassed to put in writing.
And this recent usage pattern was just, well, weird. The traffic was oddly symmetrical. Normally we’d expect to see downstream traffic outstripping upstream by an order of magnitude.
With eyes back on the telemetry, it was clear that this undefined “something” had been going on a while:
Whatever it was, it had started slowly: around or shortly after the 22nd of February. The download and upload stats started to creep nauseatingly closer before settling more or less in step. Was my father torrenting or something? He’s 80. With his general disdain for anything invented after penicillin, I’d think it unlikely.
Means, motive, and opportunity
This setup had run brilliantly for more than a year. The router had been completely reliable, and the Ubiquiti AP likewise. Interventions were rare. It was dull and just, well, there. Something had changed. But what?
- Clearly not a provider network issue or billing error
- Unlikely the router is at fault — maybe repeatedly trying and failing to apply an update? No, not that.
- Smart TV? Never used as such and is unplugged at night, it doesn’t match the traffic pattern
- The Unifi AP is happy, nothing noticeably awry
- The Android tablet (and arguably the most likely suspect here) — reliably informed by local hands and eyes nothing had changed, no new apps, no new websites visited, and it was up to date.
I couldn’t think of a likely cause. The phones, router, and AP being reputable brands, any compromise would have surely have met with a flurry of CVEs and vendor advisories. The tablet being unchanged I just had to take on trust for now.
Surprise and delight
Looking at DHCP clients, I don’t see anything unusual:
The two SIP handsets, Samsung tablet, and the AP. “Qingdao Intelligent & Precise Electronics Co. Ltd.” delivered a jolt of surprise, but a search soon identified this as his Hisense TV. All known and good.
Moving onto the real time stats, and — hold on — a new device enters the fray. One I don’t recognise. And it’s pretty busy.
Watching for a minute or two, it maintains several persistent connections as a good few others come and go. The volume of data wasn’t huge, but it was steadily increasing, and noticeably more so than other connections. A quick search on ports 12341 and 12342 finding suggests those are known botnet command and control (C2) ports. Oh good.
Deeper into the router logs shows this thing, whatever it is, previously leasing a DHCP address. The same address it had now, in fact.
…and some others too, with rotating MAC addresses
Confusion. Has the device set its leased address as a static IP, or is the router fibbing to me about active leases? With no reason to doubt the router, I suspect the former. I also learn that Android devices generating dynamic MAC addresses isn’t unheard of. Well, that’s helpful.
What are you, .233? Where did you come from?
The killer is calling from inside the house
Some situational awareness to aid you, friend and reader: This is a house in a remote part of Scotland where the nearest neighbour is closing on a mile away. The walls of the house are solid stone and getting on for a metre thick. The only other nearby entities are sheep. Sheep are not known for their offensive security chops. Mainly, well, just chops. There is nowhere this device could credibly be other than in the house itself.
What am I missing? As frustration, irritation and impostor syndrome duke it out for cranial supremacy, something emerges from the hazy recesses of my brain. Did someone mention buying him a digital photo frame for Christmas? I check. They did.
I call. Having of course totally forgotten the other end is SIP. Two men irritably shout at their own echoes for some time before giving up and falling back to the exquisitely slow delivery of WhatsApp. At a conversational cadence closer to faxes, I ask what the deal is with that photo frame. Does it work? Yes, it does. Is it on? “No no, it’s turned off”. Hmm.
“Ok”, I ask, “but is it still switched on at the plug?”
“Oh yes”
“Unplug it, please.”
Well then.
Cybersec LARPing
I gave instructions for the device to be carefully extracted, encased in lead and made ready for transport under protective guard. My sister flung it in the boot of her car.
I received it later in the week. I had low hopes in being able get into it, let alone figure out what it was doing. I find out that that the device was bought on Amazon. You can buy them , but I suggest you don’t.
I’ll save you the click, it’s one of these:
A TID10, made by Tibuta (who?) and running software provided by Uhale, who seemingly also go by Whale depending on who you talk to. The company behind [U|W]hale
and operating the cloud aspects of this thing appear to be called Zeasn.
It powered up and began merrily cycling through family photographs. I ignored its plea for WiFi and turned it face down, sparing the possibility of seeing my own gormless face grinning back at me.
Connecting the device over USB to an isolated freshly-built Linux desktop, the frame rebooted and ADB connected right away. This was… something of an anticlimax. I’d expected much more of a fight, perhaps even getting the opportunity to feel manly doing something with JTAG. Poking around via ADB revealed the device to be both rooted and running Android 6.0.1 (Marshmallow), which I think went end of life oh, around 2017 was it? Lovely.
Somewhat deflated by its early surrender, I hadn’t put much thought (and by ‘thought’ I mean frenetic googling) into what to do next.
I resorted to the tried and tested Big Dumb Guy methodology: Something looked to have changed around the 22nd of Feb — what had been happening on the device’s file system that would coincide? Nothing clever on show here, just find . -mtime -<somedays>
type of affair from the ADB shell.
Sifting through the files, amongst the noise I was too lazy to filter out one item near the top catches my attention:
So com.zeasn.frame
was updated around that time. This, it turns out, is the main photo frame application (Uhale or Whale, U-Haul — whatever). There looked to be considerable change in /data/data/com.zeasn.frame
consistent with OTA update at that time. A few files in there were newer still.
Pulling that whole directory over to the desktop, I notice a hidden directory with some seemingly randomly numbered APKs. As any Macrodata Refiner will tell you, numbers can be scary.
/frame/data/data/com.zeasn.frame$ tree -a
.
├── app_dex
│ ├── 1227742970.dex
│ ├── 1346721161.dex
│ ├── 2344618596.dex
│ ├── 487665853.dex
│ └── 709627556.dex
...
├── files
│ ├── .honor
│ │ ├── 1227742970.apk
│ │ ├── 1346721161.apk
│ │ ├── 1628853355.dex
│ │ ├── 1628853355.jar
│ │ ├── 2344618596.apk
│ │ ├── 29b62c09212ab438822c51d240c63a57
│ │ ├── 487665853.apk
│ │ ├── 709627556.apk
│ │ ├── crash
│ │ ├── eb78526f19628e53917601a05df2a1ca
│ │ └── ss
│ │ ├── 0199b402194abadd47d6a38d65210596
│ │ │ └── libcxfj.so
│ ├── PersistedInstallation.W0RFRkFVTFRd+MTozNTcyOTU2OTcwNTY6YW5kcm9pZDozMzY2NzE1ODQ5NmMxMmVkY2FlZGM1.json
│ └── xfjclient
│ └── log
│ └── xfj_cid
└── shared_prefs
├── 745a2f442935879b71d7c1592e411ba9.xml
├── com.google.android.gms.measurement.prefs.xml
├── com.google.firebase.crashlytics.xml
├── com.zeasn.frame_preferences.xml
├── detect.xml
├── file_sp_new.xml
├── FirebaseAppHeartBeat.xml
├── popa.xml
├── SP.xml
├── weather.xml
└── WebViewChromiumPrefs.xml
I was getting into awfully deep water with comically short legs, but pushed on, expecting a brick wall to arrive pretty imminently.
I tried something unsophisticated. I threw the first APK into VirusTotal — does it even scan mobile apps? Let’s find out. It does. It quickly popped up the results from a known hash, confirming it was marked as a trojan by a raft of mobile device scanning engines. Oh boy.
It was the same for all the remaining APKs too.
Then there were those DEX files matching the APK’s numbering, in the app_dex
directory.
Zero scores apiece. To be fair, I’d no idea if VirusTotal’s engines should be capable of scanning DEX files, but flushed with success from the APKs I’d just thrown them on in there. Can’t win ’em all.
I wanted to know more. A brief side quest in dismantling DEX files went hilariously wrong in a sea of bad CRCs, inappropriate endianness, and other stuff wholly incompatible with ignorance and an attention deficit. So I reached for a reliable old hammer from the rusty toolbox — strings
. As the usual detritus whizzed by, we finally hit some honest-to-goodness words.
Yep. there’s no doubt these are related. Evident a whole family of these loathsome little oiks had wandered in here and were making themselves right at home. And with that, intellectual curiosity was slowly tipping over into anger.
I lucked out with a well phrased search and hit on the by XLab, indicating that malware in the com.mz
namespace is part of what they’ve termed the MZmess malware suite. It’s a wonderfully detailed analysis. Well worth the read, and light years ahead of my own abilities.
We now have a rough idea of what this thing is. And it isn’t good. A framework facilitating DDOS attacks, account stealing, brute forcing and other malevolence, carried out in distributed attacks hidden behind residential internet connections. Botnet 101.
Something of a relief, this did mean that greater minds had been here first. The XLab writeup didn’t completely match what I’d seen, but there was enough similarity to know we had something in the neighbourhood.
The XML files made some easy pickings for extra snooping, providing further confirmation. file_sp_new.xml
containing a list of plugins matching the malicious APKs found earlier:
?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="key_saved_area">United Kingdom of Great Britain and Northern Ireland/Scotland/Edinburgh</string>
<long name="MZ_DOWNLOAD_com.app.mz.xfj01_3" value="1740728472684" />
<long name="http://8.219.89.234:80/sdkbin" value="1741204263492" />
<long name="PLUG_MANAGER_com.app.mz.popat1n_1" value="1741209684251" />
<long name="PLUG_MANAGER_com.app.mz.zhon_3" value="1741208783513" />
<long name="MZ_DOWNLOAD_com.app.mz.zhon_3" value="1740580948857" />
<long name="PLUG_MANAGER_com.app.mz.popan_8" value="1741207884178" />
<long name="http://8.219.89.234:80/pluginbin" value="1741210596394" />
<long name="MZ_DOWNLOAD_com.app.mz.popat1n_1" value="1740369278366" />
<long name="PLUG_MANAGER_com.app.mz.s101_2" value="1741210602150" />
<long name="PLUG_MANAGER_com.app.mz.xfj01_3" value="1741122982939" />
</map>
And popa.xml
. From the Xlab article, this plugin is understood to be a proxy service:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="popa.uuid">0ea6e6fa-0152-4d6d-8df6-793cb4628d36</string>
<string name="popa.asn">206067</string>
<string name="popa.publisher">yuki_t1</string>
<string name="popa.stateid">Scotland</string>
<string name="popa.domains">gmslb.net,phonemesh.org,linkmob.org,peercon.org,phonegrid.org,safernetwork.io,lbk-sol.com,sklstech.com,kyc-holdings.com,</string>
<string name="popa.countryid">UK</string>
<boolean name="popa.isForeground" value="false" />
<string name="popa.servers">s1324:6000,s206:6000,s1374:6000,s1376:6000,s209:6000,s213:6000,s205:6000,s1370:6000,s1294:6000,s1326:6000,s1372:6000,s1328:6000,s1330:6000,s1322:6000,s1436:6000,s1332:6000,s1296:6000,s212:6000,s1310:6000,s1312:6000,s1434:6000,</string>
<string name="popa.extra">""</string>
<string name="popa.cityid">Glasgow</string>
<string name="popa.ver">2.0.26</string>
</map>
The APKs were of course signed by a test key, surprising precisely no-one. The main frame app was, however signed by a vendor of some description.
I say a vendor — who, exactly? I think we’re back to this:
Nobody likes a show-off
I’d got further than I thought I would. Heck, I’d got further than I deserved. Compelling evidence that the device was (or certainly had been) infected, and had been participating in a botnet. Interestingly, some press articles on botnet activity coincided with things breaking in a house in the middle of nowhere in Scotland. My father, a possible accessory to a What a time to be alive.
But here’s the thing: I didn’t see it doing anything suspicious while I had it under observation.
Emboldened by getting this far, I decided to have a go at intercepting the device’s traffic to see if there was anything of interest. For this, I used the mitmrouter
script on . It is a lovely example of the UNIX philosophy, composing a series of small tools into a simple and effective solution.
Being an old version of Android with few protections, wielding the fearsome power of root it was fairly trivial to install mitmproxy’s root certificate as a trusted CA on the device, using ADB shell to mount, push and unmount. I attached the dirty Linux machine to the internet, being careful to isolate it from the rest of the LAN and sending its traffic over a VPN.
The results were, well. unexciting.
This looked very much like… the sort of traffic a digital photo frame would generate.
Wireshark running in the background did not pick up anything of interest either: no attempted connections or resolution for anything exotic or unusual.
Again, this is where my reach exceeds my grasp. Evident from casual checks earlier, many of the URLs it was once using were now dead: DNS records obliterated, or sinkholed by those fighting the good fight. Did it detect being under observation and hide itself? Had it simply served its purpose and been stood down?
Perhaps one clue was a file with a timestamp of 5th March 2025 — again in the .honor
directory:
{
"list": [
{
"md5": "966af88904837b4866a599a613fa973e",
"packageName": "com.app.mz.s101",
"status": 0,
"url": "http://cdn.webtencent.com/sdkfile/966af88904837b4866a599a613fa973e.apk?t=1741210591256&r=4aGhH1qVkwMYqvbZ&s=c1d4397f4cfbbd8acdce114513f0a00b",
"versionNo": 2,
"PLUG_DEFAULT": 0,
"PLUG_DISABLE": 1
},
...
]
}
This had "PLUG_DISABLE": 1
for all the malicious APKs. Certainly suggestive of components having been deactivated. The tencent URL was dead at time of checking.
I couldn’t immediately see anything that had remained resident on the system. The malicious packages were not installed as best as I could tell, and none of the binaries looked to have been modified or replaced, at least on first glance. It seemed unlikely a valid target would be abandoned, so I could only work on the assumption that the malware is still present, and altogether cleverer than me.
I was starting to run out of road.
Scratching will only make it worse
Over the next 24 hours I stepped away. I came back to it. I got annoyed with myself and stepped away again. I started writing this.
I couldn’t shake the feeling I’d discovered a payload, but not the means by which it had gotten there. There was an ecosystem in play here, no doubt, but I couldn’t see it.
This set to idly wondering…too obvious maybe, but could the frame app itself be playing a part in this? Surely not.
I installed and decompiled the running com.zeasn.frame
from the 22nd. While I know hilariously little about Java, I do know how to search for interesting patterns in source code. It’s gotten me out of the stink in more than one job.
Referring back to the Xlab article , I see mention of the com.nasa.cook
DEXloader. Decent of the folks at NASA to take time out from exploring the universe to help build IOT trinkets. I go digging. And there it is.
s101, huh. We’ve seen that before. Then there’s this:
That’ll be that hidden directory we previously found containing the payload. Fishiness intensifies.
It occurs to me then that I haven’t actually thrown this APK into VirusTotal. VirusTotal, for its part, is a total diva about the file size, making me identify multiple fire hydrants. Eventually it agrees to scan the file.
Oh. I should have done that first, really. But well caught, Dr Web.
This is smelling very much like a supply chain attack.
The broken record gets an encore
My stumbling approach to problem solving thus far has been “what changed?”, and plugging away from there. One final reprise.
We know it was updated on the 22nd of February. Using adb shell dumpsys package com.zeasn.frame
reveals a previous version.
And we’re in luck, it’s still on the file system.
The timestamp matches most other files existing before 22/2/25 on the device — I assume these were from the device image at time of manufacturing.
Pulling the older version of the APK over to the desktop, I hash it to confirm it is different to the one running following the update, and as expected it is. Throwing it into this version has a zero score. That’s not cast-iron proof of anything, of course, but it does support the notion that something was introduced in the newer version.
I decompile the older APK with JADX and again go looking for what changed, this time using git as a blunt instrument to diff the decompiled code across releases, splatting the new over the old.
Well, hello therecom.nasa.nothing.to.see.here
making its debut in the 22/2 update.
I get to wondering who’s been signing what here, exactly. First the older version:
And then the currently running version:
Both are signed by… er, the fine people at… that place. Who I can only assume are the developers. Or developers, anyway. Both packages are signed with the same key, albeit in the badlands far from the reassuring protections of the Play store. A raft of other things changed too, including all manner of crazy in the Android manifest, but the unwelcome addition above was the one that really catches the eye.
One final thing. We know this bundle of nonsense has its own update process. Via mitmproxy we’ve captured its very frequent checks for updates. I want to see if I can get the update service to point me to the latest APK available, just in case there’s been a sneaky switcheroo now the bad stuff has made its way into the wild.
With some light mauling of the original request, I replay it, substituting a made up lower version number, in the hope that the update service isn’t clever enough to validate that, or verify the user agent or anything.
Surprise! It’s not. But it ponies up an APK. I’ll be grabbing that, thanks.
Upon comparison, yes, the update service is still offering up the same version that we have running , containing com.nasa.not.malware.honest
, among who knows what other odious little treatlets.
Zeasn, or UWhale, or Yehk… Yghe.. those guys. Do they ever have a treat in store for you.
This time I truly had hit the wall. I was at the tippy-top of what I knew how to do, and had ran out of things I didn’t know how to do but would try anyway.
Besides, I’d seen enough.
Gathering my thoughts, and also my laundry
What to make of this, other than a bonfire.
It reminds me of the . This was where Allwinner/Rockchip devices were found to be positively brimming with malware — straight from the factory! Now that’s convenience.
Those were on sale on Amazon at the time — the T95 box being a gaudily ubiquitous example. I see this is no longer the case. Unclear if Amazon withdrew them from sale, or if the vendors twigged there was no longer a market for syphilitic hellboxes.
I’d argue there’s a difference this time. Going out on a limb, I’d suggest people buying those TV boxes had, in most cases, ahem, a specific purpose in mind, and the legality of that purpose had a somewhat greyish tint.
Digital photo frames are a much clearer-cut example in my mind: an innocent and approachable device, and the sort of thing much more likely to be gifted.
Whether the vendor would have been complicit or ignorant of this happening is almost immaterial. Neither is a good look.
With consumer IOT devices such as this becoming insultingly cheap, simple to use, and — giving some industrial-grade benefit of the doubt here — irresponsibly engineered, that’s a terrifying combination.
Postscript
A device bought in good faith from a huge online retailer went on to deny internet service, using the resources it stole to make the owner an unwitting accessory to orchestrated criminal activity. It’s ludicrous and maddening in equal measure.
UK Trading Standards has , claiming that
…all internet connected smart devices will be required by law to meet minimum-security standards.
They speaks mainly to weak/guessable passwords. That’s something I guess. A rooted, non Play-protected device that has trouble deciding if its day job is displaying snaps or serving as a zombified malware conduit is very much not meeting “minimum-security standards”.
The two scary things:
Firstly, this was only caught as the internet connection in use is metered, and someone passingly technical noticed (hi!). Would I have noticed if the data plan was unlimited and we had ignored paternal grumbles about the phone service? Likely not. On a regular residential broadband connection this would very easily slip by, completely unnoticed.
Perhaps more insidious, this is a demonstration of how IOT devices are commonly viewed, and indeed a reminder of what the “T” in IOT stands for. People buying these are buying a thing. There’s a computer under that plastic? Nobody cares, it’s a photo frame.
Do people worry that a colander might harbour malicious intent? Are dark patterns in hairdryers a source of breathless public concern? Cmon.
This is a consumer product. I can’t see any amount of education that can make up for the fact that this is not just dangerous garbage but subtly dangerous garbage. The only answer I see is affording better consumer protection.
Far from the internet, the photo frame now has one job: to warn people what it truly is.
Hey, Amazon
If you’re reading this, please take heed of my ‘report an issue with this product’ submission.
It’s not the , with your brothers in retail pulling stock from the shelves on previous occasions.
When I read the reviews for this device, it’s clear that buying this for a relative, especially older relatives, is very common:
When I think of all the people that have allowed this thing into their homes, perhaps received a well-intended gift, it saddens and annoys me.
“100+ sold in the last month”. That’s not the boast it looks to be.
Your customers trust you. Don’t make that trust misplaced. Make good on your . Recall, refund, and remove from sale.