mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-09-25 06:56:13 +00:00
Some ThinkNode M9 cards shipped with a dormant Windows worm (Elecrow security advisory, September 2026). An infected card seen since carries autorun.inf in the root, launching xlfqf.pif on open, explore and autoplay with random-junk comment lines in between: the Sality autorun pattern. - SD Scan store app (deploy/apps/sdscan/1.0, requires "sd", not seeded): walks the card a small page per tick, lists what it finds and why, and removes it after a confirmation screen with Cancel first. It says on every screen that it only removes files it recognises and that formatting the card is the safe fix. On older firmware it still finds threats by name but cannot remove them. - Firmware: wada.sd.check(path) and wada.sd.remove(path), plus paging for wada.sd.list(path, start, max) and caps().sd_clean. What counts as a threat lives in SdThreat.h: autorun.inf, Windows program, script and shortcut extensions, or a real MZ+PE header under any name. remove() classifies again in firmware and refuses anything else, so no app can use it to delete tiles, backups or chat history. It clears read-only, hidden and system first, because FAT refuses to delete a read-only file. - A warning when a card with Windows malware in its top folder is mounted, at boot or on insert, offering SD Scan (or the Store). - Tests: test/test_sd_threat.cpp, and SD Scan harness scenarios including the real infected card's root. Removal checked on a T-Deck. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
118 lines
4.8 KiB
C++
118 lines
4.8 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
//
|
|
// Host test for the SD-card malware policy (SdThreat.h), the rule wada.sd.remove()
|
|
// enforces for the ThinkNode M9 SD-card worm.
|
|
//
|
|
// c++ -std=c++17 -Wall -Wextra -I src test/test_sd_threat.cpp -o /tmp/sdthreat && /tmp/sdthreat
|
|
|
|
#include <assert.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include "ui-touch/SdThreat.h"
|
|
|
|
using namespace SdThreat;
|
|
|
|
// A minimal 128-byte file that carries a real DOS + PE header, and nothing else:
|
|
// no code, just the two signatures the check looks for.
|
|
static void makePe(uint8_t* buf, size_t n, uint32_t e_lfanew) {
|
|
memset(buf, 0, n);
|
|
buf[0] = 'M'; buf[1] = 'Z';
|
|
buf[0x3C] = (uint8_t)(e_lfanew & 0xFF);
|
|
buf[0x3D] = (uint8_t)((e_lfanew >> 8) & 0xFF);
|
|
buf[0x3E] = (uint8_t)((e_lfanew >> 16) & 0xFF);
|
|
buf[0x3F] = (uint8_t)((e_lfanew >> 24) & 0xFF);
|
|
if (e_lfanew + 4 <= n) { buf[e_lfanew] = 'P'; buf[e_lfanew + 1] = 'E'; }
|
|
}
|
|
|
|
// The whole content check, the way LuaAppHost runs it against a file.
|
|
static bool renamedProgram(const uint8_t* file, uint32_t size) {
|
|
if (!mzHeader(file, size)) return false;
|
|
const uint32_t off = peOffset(file);
|
|
if (!peOffsetPlausible(off, size)) return false;
|
|
return peSignature(file + off, size - off);
|
|
}
|
|
|
|
int main() {
|
|
// What the advisory names, in whatever case Windows wrote it.
|
|
assert(byName("autorun.inf") == Autorun);
|
|
assert(byName("AUTORUN.INF") == Autorun);
|
|
assert(byName("AutoRun.Inf") == Autorun);
|
|
|
|
// The real thing, from an infected M9 card (2026-09-18): autorun.inf in the
|
|
// root runs xlfqf.pif on open, explore and autoplay. The Sality autorun
|
|
// pattern: a random five-letter name with .pif, mixed-case shell verbs and
|
|
// random-junk comment lines in between.
|
|
assert(byName("xlfqf.pif") == Program);
|
|
assert(byName("XLFQF.PIF") == Program);
|
|
assert(byName("xlfqf.PiF") == Program);
|
|
|
|
// The payload shapes of a shortcut worm.
|
|
assert(byName("setup.exe") == Program);
|
|
assert(byName("TF card.EXE") == Program);
|
|
assert(byName("photo.jpg.exe") == Program); // double extension: the last one wins
|
|
assert(byName("screensaver.scr") == Program);
|
|
assert(byName("payload.dll") == Program);
|
|
assert(byName("install.vbs") == Script);
|
|
assert(byName("run.bat") == Script);
|
|
assert(byName("x.js") == Script);
|
|
assert(byName("tweak.reg") == Script);
|
|
assert(byName("maps.lnk") == Shortcut); // a folder's name with a shortcut behind it
|
|
assert(byName("tiles.LNK") == Shortcut);
|
|
assert(byName("site.url") == Shortcut);
|
|
assert(byName("desktop.scf") == Shortcut);
|
|
|
|
// Everything a real card holds must stay untouched: Elecrow's own content,
|
|
// wadamesh's data, the tile trees, and the "vaccine" folder some tools create.
|
|
const char* keep[] = {
|
|
"copyright.png", "test file.txt", "tiles", "maps", "15", "8.png", "123.jpg",
|
|
"contacts3", "ui_threads_v1.bin", "identity", "prefs.kv", "wallpaper.jpg",
|
|
"sdscan.lua", "sdscan.json", "hu.lang", "backup-2026-09-17.bin", "log.txt",
|
|
"autorun", "autorun.inf.bak", "autorun.ini", "info", ".exe.txt", "exe",
|
|
"program.exe~", "README", "", "notes.md", "track.gpx", "run.wav", "song.mp3",
|
|
};
|
|
for (const char* n : keep) {
|
|
if (byName(n) != None) { printf("false positive: '%s'\n", n); return 1; }
|
|
}
|
|
assert(byName(nullptr) == None);
|
|
assert(byName("trailingdot.") == None);
|
|
|
|
// Labels exist for every threat kind and not for None.
|
|
assert(label(None) == nullptr);
|
|
assert(label(Autorun) && label(Program) && label(Script) && label(Shortcut) && label(RenamedProgram));
|
|
|
|
// Content: a Windows program under an innocent name.
|
|
uint8_t pe[128];
|
|
makePe(pe, sizeof pe, 0x40);
|
|
assert(renamedProgram(pe, sizeof pe));
|
|
makePe(pe, sizeof pe, 0x78); // a typical real e_lfanew
|
|
assert(renamedProgram(pe, sizeof pe));
|
|
|
|
// "MZ" alone is not a program: 1 in 65,536 random blobs start with it.
|
|
uint8_t mz_only[128];
|
|
memset(mz_only, 0xA5, sizeof mz_only);
|
|
mz_only[0] = 'M'; mz_only[1] = 'Z';
|
|
assert(!renamedProgram(mz_only, sizeof mz_only));
|
|
|
|
// e_lfanew pointing inside the DOS header, past the end, or far across the
|
|
// card is rejected before anything is read there.
|
|
makePe(pe, sizeof pe, 0x10);
|
|
assert(!renamedProgram(pe, sizeof pe));
|
|
makePe(pe, sizeof pe, 126); // no room for the 4-byte signature
|
|
assert(!renamedProgram(pe, sizeof pe));
|
|
assert(!peOffsetPlausible(0x40, 3)); // tiny file: size - 4 must not wrap
|
|
assert(!peOffsetPlausible(70000, 1u << 20)); // plausible file, implausible offset
|
|
assert(peOffsetPlausible(0x80, 1u << 20));
|
|
|
|
// Too short to hold a DOS header at all.
|
|
assert(!mzHeader(pe, 10));
|
|
assert(!mzHeader(nullptr, 64));
|
|
|
|
// Media and wadamesh data do not start with MZ.
|
|
const uint8_t png[64] = { 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A };
|
|
assert(!mzHeader(png, sizeof png));
|
|
|
|
printf("test_sd_threat: all assertions passed\n");
|
|
return 0;
|
|
}
|