[Info] ShellInABox mit SSL-Zertifikat aus dem AVM-GUI nutzen

PeterPawn

IPPF-Urgestein
Mitglied seit
10 Mai 2006
Beiträge
15,138
Punkte für Reaktionen
1,703
Punkte
113
EDIT: Ich habe den Anhang entfernt, da sich das Ganze nun doch etwas anders entwickelt hat, als ich es ursprünglich erwartete. Es gibt jetzt im Freetz-Trunk die Möglichkeit, das Kennwort des AVM-Zertifikats zu benutzen und auch der Patch für ShellInABox wird dort (hoffentlich) demnächst verfügbar sein.

Seitdem AVM die Möglichkeit eingeführt hat, ein eigenes Zertifikat in der Box zu installieren (oder auch das von der Box selbst generierte eine gewisse Stabilität erreicht hat und sich nicht mehr alle Nase lang ändert), bietet es sich ja an, dieses Zertifikat auch für weitere Programme nutzbar zu machen.

Unter gewissen Umständen ist so eine "Nachnutzung" sogar unumgänglich, denn einige Browser(-Versionen) reagieren etwas allergisch, wenn ihnen für denselben Servernamen (z.B. eben fritz.box oder auch irgendein DynDNS-Name) unterschiedliche Zertifikate vorgesetzt werden, erst recht, wenn das in einem IFRAME innerhalb einer SSL-gesicherten Seite erfolgt.

AVM verschlüsselt aber den privaten Schlüssel für das Zertifikat, der in der Datei /var/flash/websrv_ssl_key.pem gespeichert wird, mit einem Kennwort, das anhand einzigartiger Merkmale der FRITZ!Box (na ja, besser wäre sicherlich "eindeutiger Merkmale") von einer Funktion (securestore_get) in einer AVM-Bibliothek (libboxlib.so) generiert wird. Das ist alles nicht neu und fand schon vor 06.20 im Prinzip genauso statt.

Seit 06.20 wird aber nun dieses Zertifikat (das steht wieder in /var/flash/websrv_ssl_cert.pem) auch für den FTP-Server verwendet, der bis zu diesem Zeitpunkt bei jedem Box-Start ein neues Zertifikat generiert hatte, was natürlich die Identifikation der eigenen FRITZ!Box anhand dieses Zertifikates praktisch unmöglich machte.

Mit der Übernahme des Box-Zertifikates auch für den FTP-Server (der basiert auf dem "ftpd" aus den GNU-Inetutils) hatte AVM aber nun ein Problem (das war auch der eigentliche Grund, warum vorher das Zertifikat stets neu generiert wurde, wenn man dem Kommentar in einer Quelltext-Datei glauben will) - irgendwie mußte der FTP-Daemon in die Lage versetzt werden, das Kennwort für die Entschlüsselung des "private key" zu erfahren, ohne daß man in dessen Quelltext den Aufruf von "securestore_get" einbaut, da dann wieder die GPLv2-Lizenz die Veröffentlichung der Quellen von "libboxlib" erforderlich machen würde. Also wurde das Tool "getprivkeypass" erfunden ...

Dieses kleine Tool bemüht sich nun (im Rahmen seiner Möglichkeiten) bei seinem Start sicherzustellen, daß es tatsächlich vom "ftpd" aus aufgerufen wurde. Ist das erkennbar nicht der Fall, wird ein (festes) Phantasie-Kennwort zurückgeliefert, mit dem natürlich das Keyfile nicht gelesen werden kann. Für die Identifikation des Elternprozesses liest "getprivkeypass" dazu den Inhalt von /proc/$(ppid)/cmdline und /proc/$(ppid)/stat aus und versucht in den gelesenen Daten die Zeichenkette ftpd zu finden. Wenn das erfolgreich ist, reicht das schon als "Beweis", daß der Aufruf aus dem "ftpd" heraus erfolgte und "getprivkeypass" ruft nun "securestore_get" für uns auf. Nach ein wenig Voodoo mit ein paar Bitknipsereien (das macht auch "securestore_get" schon intern ähnlich) rückt "getprivkeypass" aber dann einen Wert heraus, den man mit geeigneten Mitteln (wie, kann man in den Quellen des "ftpd" ja nachlesen) wieder in ein ordentliches Kennwort (8 Zeichen feste Länge zur Zeit) verwandeln kann. Mit diesem Kennwort kann man dann auch den privaten Schlüssel für andere Programme benutzen.

Um also "getprivkeypass" zu "mißbrauchen", benötigt man nichts weiter als ein paar Zeilen C-Code und einen Programmnamen, der irgendwo (der Test erfolgt derzeit mit strstr) die Zeichenfolge "ftpd" enthält. Dazu habe ich ein kleines C-Programm geschrieben, das in nicht allzu ferner Zukunft in einem weiteren Beitrag (und dann sicherlich auch als Vorschlag für ein "enhancement" im Freetz-Trac) auftauchen wird. Damit kann man dann auch einen Apache2-Server (Direktive "SSLPassPhraseDialog") mit dem Box-Zertifikat "füttern" oder meinetwegen auch das Box-Zertifikat (nach entsprechender Umwandlung) für einen Dropbear-SSH-Server auf der Box verwenden. Aber dieses Programm ist hier eigentlich gar nicht das Thema, dieser Ausflug sollte nur die Notwendigkeit und die Möglichkeiten eines solchen "Proxy-Programms" verdeutlichen, mit dem man "getprivkeypass" zur Herausgabe "überreden" kann.

Denn bei dem, was ich eigentlich mit dem statisch gelinkten "shellinaboxd"-Binary erreichen will, wäre die Notwendigkeit eines weiteren Programms ja wenig hilfreich. Also habe ich in meinen Patch für "ShellInABox 2.14" auch noch den Code aufgenommen, um direkt das "getprivkeypass"-Binary aufzurufen, wenn es vorhanden und ausführbar ist und wenn die o.a. Voraussetzungen für das "Verwirren" von "getprivkeypass" auch vorliegen (also der Name des ShellInABox-Daemons irgendwo "ftpd" enthält).

Der Patch im Anhang Freetz-Trunk erweitert also den "shellinaboxd" um eine Option mit dem Namen "--cert-from-box", die den zusätzlichen Code überhaupt erst aktiviert. Für das "Ermitteln" des Kennworts gibt es dann drei mögliche Wege:

1. Die Angabe von "--cert-from-box" erfolgt in einer Form, daß nach der Option gleich noch das Kennwort für den privaten Schlüssel angegeben wird (also "--cert-from-box=boxpassword"). Dann wird genau dieses angegebene Kennwort verwendet, wie das vorher ermittelt wurde, interessiert natürlich nicht. Nachteilig ist es aber weiterhin, daß dann das Kennwort während der gesamten Laufzeit des "shellinaboxd" an diversen Stellen auch abgegriffen werden kann (zu dieser Problematik, von Environment bis /proc/self/cmdline und den jeweiligen Vor- und Nachteilen, gibt es andere ausführlichere Quellen).

2. Es wird kein Kennwort mit der Option "--cert-from-box" festgelegt. Dann prüft der zusätzliche Code als erstes, ob irgendwo im Namen des Programms "ftpd" enthalten ist. Wenn "ftpd" gefunden wird, wird direkt "getprivkeypass" aufgerufen (wenn das Tool auch noch gefunden wird und ausführbar ist), dann braucht es keine weiteren Programme und damit existieren keine weiteren Abhängigkeiten (bei meiner statisch gelinkten Variante). Der Aufruf kann natürlich auch über einen Symlink erfolgen, der den benötigten Aufbau hat ... ich verwende zum Beispiel den Namen "ftpd_siab" für einen Symlink auf "shellinaboxd", damit kommen keine Unklarheiten auf, was sich hinter dem Namen eigentlich verbirgt.

3. Übrig bleibt noch die Form, bei der weder das Kennwort beim Aufruf angegeben wurde noch der Dateiname ein "ftpd" irgendwo enthält. Dann versucht der Code über die Environment-Variable "KEYPASSWORDPROXY" einen Programmnamen für den oben beschriebenen "Proxy" zu lesen und diesen aufzurufen (wieder nach einem Test, ob die Datei existiert und ausführbar ist), um an das Kennwort zu gelangen. Das ist dann zumindest wieder eine Variante, bei der das Kennwort nicht permanent irgendwo im Speicher liegt wie bei Variante 1.

Wenn Box-Key und -Zertifikat nicht gelesen werden können, verweigert der zusätzliche Code bei Angabe von "--cert-from-box" die Arbeit, es wird also auch kein neues Zertifikat für SIAB automatisch generiert.

Ob ich das irgendwann als Patch im Freetz-Trac einreiche, weiß ich noch nicht so genau. Es ist für AVM ja ein Leichtes, den bisher arg blauäugigen Test auf "ftpd" zu verschärfen oder sich wieder etwas Neues einfallen zu lassen, um "getprivkeypass" irgendwie zu ersetzen. Das macht meine Lösung stark vom derzeitigen Stand (gilt von 06.20 bis 06.24) abhängig, falls AVM da irgendetwas dagegen haben sollte. Mir fällt zwar auch nicht ein, was das sein sollte (es spricht für mich deutlich mehr dafür, das Box-Zertifikat mehrfach zu verwenden anstatt es mehrfach an verschiedenen Stellen auf der Box zu speichern), aber man kann ja nie wissen ... ich habe auch absichtlich "nur" den Weg über "getprivkeypass" gewählt und den Aufruf von "securestore_get" außen vor gelassen. Aber ein simples Disassemblieren von "getprivkeypass" reicht ja schon aus, um auch dort die notwendigen Voraussetzungen für den Aufruf zu erkunden. Da das auch die "bad guys" machen könn(t)en, sehe ich die Sicherheit nicht beeinträchtigt.

Wenn man auf der Box ein verschlüsseltes Zertifikat speichert (das ist grundsätzlich auch richtig so in meinen Augen, damit da kein Mißverständnis entsteht) und das muß für die Verwendung automatisch entschlüsselt werden, dann braucht es eben auch einen Weg, das zu tun. Da beißt die Maus keinen Faden ab und wenn damit der private Schlüssel etwas weniger "vertrauenswürdig" ist, muß man das eben beim Umgang mit solchen Verbindungen berücksichtigen. Da kann AVM auch wenig bis nichts dagegen tun ... wer es anders und sicherer haben will, braucht eine Art TPM per USB o.ä. angebunden - das ist dann sogar in meinen Augen für ein Consumer-Gerät eher Overkill, solange solche Teile nicht irgendwo in den Prozessoren direkt eingebaut sind und nur noch benutzt werden müssen.

Ach so, falls sich jemand fragen sollte, was man mit dem Patch nun machen soll ... einfach in das Verzeichnis "make/shellinabox/patches" eines Freetz-Trunks legen (die Erweiterung ".txt" dabei weglassen, das Forum erlaubt nur keine Dateien mit ".patch" als Extension) und dann das Image neu bauen lassen.
 
Zuletzt bearbeitet:
Ich antworte mal hier, meine Antwort bezieht sich auch auf Deine Frage(n) im axTLS Ticket.

Grundsätzlich gefällt mir sehr die Möglichkeit das Box-Zertifikat auch für andere Pakete nutzen zu können. Ich weiß nicht, ob AVM hier mitliest und was sie davon hält (i.e. ob sie was unternimmt, um uns den Weg zu verbauen), ich stelle mir das Ganze in etwa so vor - eine Shared-Library, die folgende Funktionen unterstützt / Merkmale hat:
  • Funktion, die die Frage beantwortet, handelt es sich um eine Fritz!OS Version, auf der es das Box-Zertifikat überhaupt auf dem Weg ausgelesen werden kann
  • eine/mehrere Funktion(en) zum Auslesen des Key-Passworts (quasi alles, was Du in libhttp/ssl.c gepackt hast bis auf sslSetCertificateFromBox)
  • Funktionen/Konstanten, die die Pfade zu dem Key- bzw. dem Cert-File liefert, wären ebenso wünschenswert
  • die Library selbst soll nach Möglichkeit keine Abhängigkeit zu irgendeiner SSL-Library haben

Die Pakete, die das Box-Zertifikat nutzen möchten, könnten gegen die Library gelinkt werden und müssten nur noch dahingehend angepasst werden, dass sie eben das Box-Zertifikat nutzen ohne sich aber aufs Neue drum zu kümmern, wie das nun im Detail geht.
 
Zuletzt bearbeitet:
Ich nehme mal die Punkte kurz auf:

Funktion, die die Frage beantwortet, handelt es sich um eine Fritz!OS Version, auf der es das Box-Zertifikat überhaupt auf dem Weg ausgelesen werden kann
Das wäre für mich schon durch die Frage beantwortet, ob "getprivkeypass" existiert ... wie es nach eventuellen Änderungen seitens AVM dann aussehen würde, kann ich auch noch nicht sagen.

eine/mehrere Funktion(en) zum Auslesen des Key-Passworts (quasi alles, was Du in libhttp/ssl.c gepackt hast bis auf sslSetCertificateFromBox
Das ist (für andere Patches) im Prinzip so auch schon fertig, allerdings nicht als dynamische Library ... halte ich auch für weniger sinnvoll, linken muß man es ohnehin und so sehr trägt es nicht auf. Der Vorteil des "einfachen" Austausch einer solchen Lib, die dann wieder alle anderen Komponenten automatisch benutzen, fällt auf einer FRITZ!Box mit SquashFS ja ohnehin nicht groß ins Gewicht, aber wenn man unbedingt eine so-Lib daraus machen will, ist das ja auch kein Problem.
Code:
/* BoxKeyPasswordCallback.c -- provide FRITZ!Box key password for
                               OpenSSL based utilities via callback
[...]
*/

#define _GNU_SOURCE
#include "BoxKeyPassword_Callback.h"

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/errno.h>

unsigned int BoxKeyPassword_FromVendorTool(char * dest, int size)
{
/* The used code to obfuscate the real password with the pid value of the
   calling process can be found at the 'ftpd.c' file from vendor's open
   source package.
*/
	unsigned int	len = 0;
	char		exec_pipe[512];
	char		password[32];
	int		my_pid = 0;
	FILE *		pipe_handle = NULL;
	size_t		outLen = 0;
	unsigned int	count = 0;
	int		high_order_bit = 0;
	struct stat	avm_command_stat;

	memset(exec_pipe, 0, sizeof(exec_pipe));
	memset(password, 0, sizeof(password));
	memset(&avm_command_stat, 0, sizeof(avm_command_stat));
	if (stat(FRITZBOX_VENDOR_COMMAND, &avm_command_stat) == 0) 
	{
		if ((avm_command_stat.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) != 0)
		{
			my_pid = getpid();
			sprintf(exec_pipe, "%s %u", FRITZBOX_VENDOR_COMMAND, my_pid);
			pipe_handle = popen(exec_pipe, "r");
			if (pipe_handle != NULL)
			{
				outLen = fread(password, 1, sizeof(password) - 1, pipe_handle);
				if (outLen > 0)
				{
					password[outLen] = 0;
					for (; count < outLen; count++)
					{
						high_order_bit = my_pid & (1 << 31);
						password[count] ^= (char) (my_pid & 255);
						my_pid = my_pid << 1;
						if (high_order_bit != 0) my_pid |= 1;
					}
					len = strlen(password);
					if ((int) len > size)
					{
						strncpy(dest, password, (size_t) size);
						dest[size - 1] = 0;
						len = strlen(dest);
					} 
					else
					{
						 strcpy(dest, password);
					}
				}
				pclose(pipe_handle);
			}
		}
	}
	return len;
}

unsigned int BoxKeyPassword_FromProxy(char * dest, int size)
{
	unsigned int	len = 0;
	char *		proxy = NULL;
	char		password[32];
	FILE *		pipe_handle = NULL;
	struct stat	proxy_command_stat;

	memset(password, 0, sizeof(password));
	memset(&proxy_command_stat, 0, sizeof(proxy_command_stat));
	proxy = getenv(FRITZBOX_PROXY_COMMAND_ENV);
	if (proxy)
	{
		if (stat(proxy, &proxy_command_stat) == 0)
		{
			if ((proxy_command_stat.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) != 0)
			{
				pipe_handle = popen(proxy, "r");
				if (pipe_handle != NULL)
				{
					len = fread(password, 1, sizeof(password) - 1, pipe_handle);
					if ((int) len > size)
					{
						len = (size_t) size;
					}
					strncpy(dest, password, len);
					dest[len] = 0;
					pclose(pipe_handle);
				}
			}
		}
	}
	return len;
}

unsigned int BoxKeyPassword_FileContains(const char *filename, const char *needle)
{
	unsigned int	rc = 1;
	FILE *		handle = NULL;
	char		buffer[256];
	size_t		nlen = 0;
	size_t		len;
	unsigned int	i;

	nlen = strlen(needle);
/* - there has to be something to look for and it has to be shorter than 128 bytes
   - the special use case here allows us to limit the read bytes to 256 for each
     file, because the searched string is the binary name near the start of lines
*/
	if (nlen && nlen < 128) 
	{
		if ((handle = fopen(filename, "r")) != NULL) 
		{
			len = fread(buffer, 1, sizeof(buffer), handle);
			if (len > 0)
			{
				for (i = 0; i < (len - nlen); i++)
				{
					if (strncmp(&buffer[i], needle, nlen) == 0)
					{
						rc = 0;
						break;
					}
				}
			}
			fclose(handle);
			handle = NULL;
		}
	}
	return (rc == 0 ? 1 : 0);
}

unsigned int BoxKeyPassword_ProcessNameContainsFtpd(void)
{
	return BoxKeyPassword_FileContains("/proc/self/cmdline", "ftpd") && BoxKeyPassword_FileContains("/proc/self/stat", "ftpd");
}

int BoxKeyPassword_Callback(char * buf,int size,int rwflag,void * userdata)
{
/* There are three possible solutions to get the correct password from the box:

   1. use the (vendor supplied) 'getprivkeypass' binary to get it, but this is 
      only possible, if our own name contains the string 'ftpd' somewhere

   2. use another utility (for example my own 'boxkeypasswordftpdproxy') to call
      'getprivkeypass' from an external process with the required name

   3. call the function 'securestore_get' from vendor's 'libboxlib.so' directly

   We'll implement only the solution 1 and 2 here and the third point will be used
   later, if the vendor takes another step to obliberate our 'abuse' of the supplied
   tool 'getprivkeypass'. It's easy enough to check the process name with a more
   specific function than strstr, which would force the 'proxy' process to enhance
   it's efforts to fool the tool and sooner or later we just reach a point, where 
   abusing vendor supplied tools isn't efficient anymore. 

   If the  binary is called with a name already containing a 'ftpd' substring, there 
   are no needs to call an external proxy, so it's the best choice here and we'll try 
   to detect this first.
*/
	unsigned int	len = 0;
	
	if (rwflag) return 0;		/* no encryption supported, only used for decrypting private key */
	if (!userdata) return 0;	/* missing user data pointer */
	if (userdata == (void *) -1)
	{
		if (BoxKeyPassword_ProcessNameContainsFtpd())
		{
			len = BoxKeyPassword_FromVendorTool(buf, size);
		}
		else
		{
			len = BoxKeyPassword_FromProxy(buf, size);
		}
	}
	else
	{
		len = strlen((char *) userdata);
		if ((int) len > size - 1)
		{
			strcpy(buf, (char *) userdata);
		}
		else
		{
			strncpy(buf, (char *) userdata, (size_t) size);
			buf[size - 1] = 0;
		}
	}
	return (int) len;
}
Funktionen/Konstanten, die die Pfade zu dem Key- bzw. dem Cert-File liefert, wären ebenso wünschenswert
Code:
/* BoxKeyPasswordCallback.h -- provide FRITZ!Box key password for
                               OpenSSL based utilities via callback

[...]
*/

#ifndef BOXKEYPASSWORD_H
#define BOXKEYPASSWORD_H

#define FRITZBOX_SSL_CERTFILE      "/var/flash/websrv_ssl_cert.pem"
#define FRITZBOX_SSL_KEYFILE       "/var/flash/websrv_ssl_key.pem"
#define FRITZBOX_VENDOR_COMMAND    "/bin/getprivkeypass"
#define FRITZBOX_PROXY_COMMAND_ENV "KEYPASSWORDPROXY"

unsigned int BoxKeyPassword_FromVendorTool(char * dest, int size);
unsigned int BoxKeyPassword_FromProxy(char * dest, int size);
unsigned int BoxKeyPassword_FileContains(const char *filename, const char *needle);
unsigned int BoxKeyPassword_ProcessNameContainsFtpd(void);
int BoxKeyPassword_Callback(char * buf,int size,int rwflag,void * userdata);

#endif
Außer Du meintest tatsächlich noch eine Funktion, die den Pfad direkt im Programm zur Verfügung stellt, halte ich aber für unnötig ... ich kenne kein Programm, wo man den Pfad nicht im Rahmen der Konfiguration angeben könnte und wenn man das Kennwort dann über die Callback-Funktion setzt (über SSL_CTX_set_default_passwd_cb mit Pointer auf BoxKeyPassword_Callback), dann sollte das in aller Regel ausreichend sein. Wenn dabei keines der beiden Kriterien erfüllt ist (kein "ftpd" im Link zum Aufruf und keine Environment-Variable KEYPASSWORDPROXY), dann spielt die Callback-Funktion praktisch keine Rolle (in den Programmen, die ich bisher gepatched habe jedenfalls) und das Verhalten ist identisch mit dem ohne "boxcert"-Patch.

die Library selbst soll nach Möglichkeit keine Abhängigkeit zu irgendeiner SSL-Library haben
Solange die nicht auch noch einen Ersatz für "SSL_CTX_set_default_passwd_cb" bereitstellen soll, ist das kein Problem. Allerdings sollten dann andere SSL-Stacks zumindest ähnliche Callback-Funktionen anbieten, damit man mit einem passenden Wrapper dort übersetzen kann.

Das mit einer Zusammenfassung der Funktionen in einer Library hatte ich tatsächlich schon so ausgeführt (s.o.), wenn ich das explizit noch einmal in SIAB eingebaut habe, dann nur um das wirklich als komplett statisches Binary (~ 1.4 MB für 7490) benutzen zu können, ohne da erst irgendwelche Libs und/oder zusätzlichen Programme mitschleppen zu müssen.

Der Reaktion vom AVM sehe ich erst einmal gelassen entgegen ... und wenn man sie nicht testet, ist alles ohnehin nur Spekulation. Meine Argumentation pro/contra Sicherheit des PKey habe ich ja im axTLS-Ticket schon dargelegt, mehr ist (aus meiner Sicht) dazu nicht zu sagen.

Ach so ... ich vergaß noch folgendes: Auch der "Standard-Kennwortdialog" von OpenSSL (PEM_def_callback irgendwo in den Crypto-Funktionen zum Lesen von PEM-Files) prüft als erstes, ob im userdata-Parameter beim Aufruf der Callback-Funktion eventuell das richtige Kennwort schon steht. Es gäbe also auch die Möglichkeit, das genau umgekehrt zu implementieren und das Kennwort schon vor der Benutzung des privaten Schlüssels über "SSL_CTX_set_default_passwd_cb_userdata" richtig zu setzen. Das hatte ich auch in Erwägung gezogen, dann aber wieder verworfen, weil dieses Kennwort dann auch zum Verschlüsseln herangezogen werden könnte - das wollte ich nicht haben, aus taktischen Erwägungen ... wobei ich die dritte Möglichkeit, nämlich PEM_def_callback direkt in der libcrypto.so zu modifizieren, gar nicht so abwegig fand, denn diese Routine kommt ja nur dann zum Einsatz, wenn ein Programm keine eigene Callback-Funktion bereitstellt.
 
Zuletzt bearbeitet:

Statistik des Forums

Themen
244,695
Beiträge
2,216,692
Mitglieder
371,315
Neuestes Mitglied
jack-mack
Holen Sie sich 3CX - völlig kostenlos!
Verbinden Sie Ihr Team und Ihre Kunden Telefonie Livechat Videokonferenzen

Gehostet oder selbst-verwaltet. Für bis zu 10 Nutzer dauerhaft kostenlos. Keine Kreditkartendetails erforderlich. Ohne Risiko testen.

3CX
Für diese E-Mail-Adresse besteht bereits ein 3CX-Konto. Sie werden zum Kundenportal weitergeleitet, wo Sie sich anmelden oder Ihr Passwort zurücksetzen können, falls Sie dieses vergessen haben.