Debugging für Fortgeschrittene: Post-Mortem einer Java-Bugsuche

Damit hatten wir nicht gerechnet: nachdem Snow Leopard von Apple veröffentlicht wurde, zeigte sich, dass unsere UKLAN-Admin-Anwendung darunter nicht lief. Die Version von Java, die mit Snow Leopard auf Macs erstmalig der Standard ist, ist Java 6. Diese Version hatten Kollegen mit Windows- und Linux-Rechner schon längst ohne Probleme im Einsatz, so dass nicht zu erwarten gewesen war, dass das auf Macs anders sein könnte.
Das Problem lag bei der Authentifizierung. Beim Anmeldeversuch kam die Meldung, dass diese gescheitert sei. Im Log konnte man zu der Exception einen recht langen Stacktrace sehen. Die entscheidende Meldung lautete:

java.lang.IllegalArgumentException: EncryptionKey: Key bytes cannot be null!

Der Fehler trat in einer internen Javaklasse auf, die nie explizit verwendet werden soll: sun.security.krb5.EncryptionKey. Das allein machte für mich klar, dass es sich um keinen Fehler in unserem Programm handeln konnte, sondern entweder einen im Betriebssystem oder im mitgelieferten Java. Da abzusehen war, dass das Problem durch Apple nicht kurzfristig gelöst werden würde, habe ich mich auf die Suche nach der eigentlichen Ursache gemacht. Am Anfang der Suche standen Beobachtungen:

  • Wenn man ein falsches Passwort eingab, wurde das korrekt als falsch erkannt. Das Problem trat also nur auf, wenn die Authentifizierung eigentlich erfolgreich war.
  • Wenn man bereits ein Kerberos-Ticket hatte, konnte das (nach einer kleinen Programmänderung) verwendet werden, so dass das Passwort gar nicht geprüft werden musste.

Die zweite Erkenntnis hat uns zumindest einen ersten Workaround verschafft, wenngleich der für die Anwender etwas lästig war. Die eigentliche Ursache war hingegen immer noch unklar. Durch die tatkräftige Hilfe eines Forummitglieds auf forums.sun.com bin ich in mehreren Schritten zu weiteren Erkenntnissen gekommen:

  • Die Exception wurde nur durch manche Ciphers verursacht – wie sich später zeigte, sind es die AES-basierten.
  • Unmittelbare Ursache der Exception war ein leeres Salt – dass die AES-Ciphers das nicht mögen, war auf verschiedenen Plattformen reproduzierbar.
  • Nach einer ganzen Weile wurde mir klar, dass die Authentifizierung manchmal klappte.

Das letzte Indiz wies mir den Weg. Wir befragen im Programm zunächst das DNS, welche Server im Active Directory der Uni als KDCs zur Verfügung stehen. Derzeit sind das sechs Stück:

% host -t srv _kerberos._udp.ad.uni-koeln.de
_kerberos._udp.ad.uni-koeln.de has SRV record 0 100 88 ads5.ad.uni-koeln.de.
_kerberos._udp.ad.uni-koeln.de has SRV record 0 100 88 advdc1.ad.uni-koeln.de.
_kerberos._udp.ad.uni-koeln.de has SRV record 0 100 88 rzkvdc1.ad.uni-koeln.de.
_kerberos._udp.ad.uni-koeln.de has SRV record 0 100 88 rzkvdc2.ad.uni-koeln.de.
_kerberos._udp.ad.uni-koeln.de has SRV record 0 100 88 rzkvdc3.ad.uni-koeln.de.
_kerberos._udp.ad.uni-koeln.de has SRV record 0 100 88 ads4.ad.uni-koeln.de.

Da die Authentifizierung manchmal klappte, lag der Verdacht nahe, dass nur manche der Server das Problem provozieren. Also habe ich die Server der Reihe nach fest im Programm verdrahtet. Dabei zeigte sich, dass Authentifizierung nur bei einem der sechs Server möglich war – wohlgemerkt, nur unter Java 6 auf Macs! Auf allen anderen Systemen funktionierten alle sechs Server. Wie ich vom zuständigen Kollegen erfuhr, läuft der funktionierende Server unter Windows Server 2008, die anderen hingegen noch unter Windows Server 2003. Aber in was unterschied sich die Authentifizierung? Mit Wireshark ließ sich beobachten, dass die Antwort des W2K8-Servers einen Unterschied aufwies.

W2K3:
Encryption type: rc4-hmac (23)
Salt:
Encryption type: des-cbc-md5 (3)
Salt: ...

W2K8:
Encryption type: rc4-hmac (23)
Encryption type: des-cbc-md5 (3)
Salt: ...

Hm, ein leeres Salt – war da nicht was? Richtig, das leere Salt produziert eine Exception. Aber wieso wird hier das leere Salt in der Antwort des Servers übernommen, auf anderen Plattformen aber nicht? Die Klasse mit dem relevanten Code gehört zum dem kleinen Teil, der nicht im Source verfügbar ist – mit Ausnahme von OpenJDK. Also habe ich dort nachgesehen. In Zeile 360 beginnt der Code, der das Salt auswertet, das der KDC in seiner Antwort übergibt:

360 // update salt in PrincipalName
361 byte[] newSalt = error.getSalt();
362 if (newSalt != null && newSalt.length > 0) {
363 princ.setSalt(new String(newSalt));
364 }

Im OpenJDK wird also explizit getestet, dass der String eine Länge > 0 hat. Hatte Apple diesen Test etwa entfernt? Ich hätte es bei der Spekulation belassen müssen, wenn ich nicht auf das javap-Kommando hingewiesen worden wäre. Damit kann man Javaklassen disassemblieren. Das funktioniert auch für die Systemklassen. Und da konnte man folgenden Unterschied erkennen:

javap -v sun.security.krb5.Credentials

JDK 5
85: invokevirtual #77 // Method sun/security/krb5/internal/KRBError.getSalt:()[B
88: astore 6
90: aload 6
92: ifnull 114
95: aload 6
97: arraylength
98: ifle 114
101: aload_0
102: new #44 // class java/lang/String
105: dup
106: aload 6
108: invokespecial #78 // Method java/lang/String."":([B)V
111: invokevirtual #79 // Method sun/security/krb5/PrincipalName.setSalt:(Ljava/lang/String;)V

In Zeile 98 wird die Länge getestet. Ist sie 0, springt das Programm zu Zeile 114, also hinter die setSalt()-Anweisung.

JDK 6
85: invokevirtual #81; //Method sun/security/krb5/internal/KRBError.getSalt:()[B
88: ifnull 107
91: aload_0
92: new #51; //class java/lang/String
95: dup
96: aload 5
98: invokevirtual #81; //Method sun/security/krb5/internal/KRBError.getSalt:()[B
101: invokespecial #82; //Method java/lang/String."":([B)V
104: invokevirtual #83; //Method sun/security/krb5/PrincipalName.setSalt:(Ljava/lang/String;)V
107: aload_2
108: ifnull 131
111: aload_2
112: aload_0
113: invokevirtual #84; //Method sun/security/krb5/PrincipalName.getSalt:()Ljava/lang/String;

Hier fehlt der Längentest! Der Vergleich mit einem Windowsrechner zeigte, dass das dortige Java 6 den Test enthielt. Es handelt sich also zweifelsfrei um einen Bug in Apples Javaversion. Nachdem ich all diese Rechercheergebnisse an Apple gemeldet hatte, wurde der Bug dort auch sofort anerkannt. Bis Apple einen Fix rausbringt, könnte allerdings erfahrungsgemäß noch einige Zeit vergehen. Da die Ursache jetzt aber erkannt war, konnte ich einen Workaround implementieren, der ohne Mitwirkung der Anwender funktioniert: wenn die besagte Exception auftritt, wird die Authentifizierung einfach nochmal durchgeführt, dann aber immer gegen den W2K8-Server. Der ist zwar jetzt ein „single point of failure“ für Macs mit Java 6, aber damit können wir leben.

Der Workaround ist nur in der Testversion von UKLAN-Admin enthalten.

Der geschilderte Fall zeigt, wie verzwickt die Fehlersuche sein kann. Der Fehler tritt schließlich nur auf, wenn man

  • einen Mac einsetzt, der Java 6 als präferiertes Java eingestellt hat (d.h. alle Macs mit 10.6, sehr wenige mit 10.5)
  • Kerberos 5 zur Authentifizierung nutzt
  • kein anderweitig bezogenes Kerberos-Ticket besitzt
  • keinen Windows 2008 Server hat (wie es mit anderen KDCs als W2K3 aussieht, weiß ich allerdings nicht)
  • man nicht die Cipherliste in /Library/Preferences/edu.mit.Kerberos durch einen Eintrag für default_tkt_enctypes auf nicht-AES-Ciphers eingeschränkt hat (der Trick klappt allerdings nicht für WebStart-Programme)


Flattr this

Snow Leopard enthält Cisco IPSec VPN

Obwohl Apple angekündigt hatte, dass Snow Leopard keine neuen Features aufweisen würde, hat sich doch so manches getan, allerdings zumeist unter der Oberfläche. Eine dieser etwas versteckten Neuerungen ist integrierte Unterstützung für Ciscos Variante eines IPSec VPNs. Da iPhone und iPod Touch das seit Firmwareversion 2.0 schon konnten, ist die Integration in Snow Leopard keine ganz große Überraschung. Es fällt aber auf, dass im Ggs. zum iPhone bei OS X keine Spur des offiziellen Cisco-Logos zu erkennen ist. Und die Implementierung erfolgt auf Basis von racoon, eines Open Source VPN-Clients aus FreeBSD.

Wir propagieren hier an der Uni zwar mittlerweile vornehmlich die Verwendung des Cisco AnyConnect-Clients, der anstelle von IPSec DTLS verwendet, aber durch die direkte OS-Integration ist Apples Variante mglw. noch einen Tick komfortabler und robuster.

Wer’s probieren möchte, muss entweder im alten „tsunami“-WLAN oder außerhalb des UKLAN sein. Dann öffnet man Systemeinstellungen, dort Netzwerk, klickt auf das + oberhalb des Schlosses, wählt dort als Anschluss VPN, wählt als VPN-Typ Cisco IPSec, und bestätigt die Eingabe. Nun trägt man als Serveradresse vpngate.uni-koeln.de ein, Accountname und Passwort, klickt auf „Identifizierungseinstellungen …“, trägt dort als Gruppenname uklan-full ein, als Schlüssel uklan und klickt auf OK. Dann noch Anwenden und Verbinden, und fertig. Eine ausführlichere Doku mit Screenshots folgt evtl. später.

Mit dem Excel aus Office 2008 mit einem Intel-Mac auf MySQL zugreifen

Heute kam mir zum ersten Mal eine sinnvolle Anwendung für eine ODBC-Anbindung einer MySQL-Datenbank an Microsoft Excel in den Sinn. Zum Glück war meine erste Aktion nach der Idee eine Google-Suche. Dadurch habe ich einen Foreneintrag gefunden, der mir eine Menge Arbeit erspart hat.
Der Hintergrund ist, dass es eine Reihe von Problemen gibt:

  • Obwohl Office 2008 Macs mit Intel-Prozessoren nativ unterstützt, ist das Hilfsprogramm Microsoft Query seit 2002 unverändert und hat nur PowerPC-Code.
  • Der freie ODBC-Connector von MySQL wird nicht als „fat binary“ angeboten, sondern nur wahlweise als ppc- oder x86-Code.
  • Excel 2008 hat eine hartkodierte Liste von unterstützten ODBC-Konnektoren, die nicht den MySQL-Connector enthält.

Wenn man eine schnelle und einfache Lösung für alle genannten Problem will, kann man einen der offiziell von Microsoft unterstützten Konnektoren kaufen. Getestet habe ich nur den von Actual Technologies (siehe unten).

Da es einen freien Konnektor gibt, sehe ich aber nicht ein, warum ich einen kaufen soll. Hier sind die Schritte, um den freien MySQL-Connector benutzen zu können:

  • bei MySQL die Konnektoren für PowerPC und x86 separat runterladen (im „package format“)
  • Achtung: in gemounteter Form heißen beide gleich, so dass man die Architektur nicht mehr erkennen kann. Deshalb sollten die Images jeweils manuell nach Bedarf gemountet werden.
  • Das Image für x86 mounten (Doppelklick) und den Installer für x86 ausführen
  • im Terminal wie folgt aus den Shared Libraries beider Architekturen sog. „fat binaries“ bauen:
  • mkdir ODBC_ppc ODBC_x86 ODBC_fat
  • cd ODBC_x86
  • pax -zrf /Volumes/MySQL Connector ODBC 5.1/MySQL Connector ODBC 5.1.pkg/Contents/Archive.pax.gz
  • x86-Image auswerfen, ppc-Image mounten
  • cd ../ODBC_ppc
  • pax -zrf /Volumes/MySQL Connector ODBC 5.1/MySQL Connector ODBC 5.1.pkg/Contents/Archive.pax.gz
  • cd ..
  • lipo ./ODBC_ppc/usr/local/lib/libmyodbc3S-5.1.5.so ./ODBC_x86/usr/local/lib/libmyodbc3S-5.1.5.so -output ODBC_fat/libmyodbc3S-5.1.5.so -create
  • lipo ./ODBC_ppc/usr/local/lib/libmyodbc3S.so ./ODBC_x86/usr/local/lib/libmyodbc3S.so -output ODBC_fat/libmyodbc3S.so -create
  • lipo ./ODBC_ppc/usr/local/lib/libmyodbc5.so ./ODBC_x86/usr/local/lib/libmyodbc5.so -output ODBC_fat/libmyodbc5.so -create
  • sudo cp ODBC_fat/* /usr/local/lib/
  • An dieser Stelle hat man einen „fetten“ MySQL-Connector, den man mit Dienstprogramme->ODBC-Administrator konfigurieren kann. Ich habe einen Benutzer-DSN hinzugefügt.
  • Da Excel sich noch weigert, wenn man Daten->Externe Daten->Neue Abfrage erstellen auswählt, muss man die Demo-Version von Actual Technologies installieren.
  • Jetzt startet Microsoft Query, wenn man es aus Excel aufruft, und man kann auch den MySQL-Connector benutzen


Flattr this

DNS-Abfragen mit JNDI

Für unser Java-Projekt benötigen wir genau eine DNS-Abfrage. In der Vergangenheit haben wir dazu dnsjava verwendet, aber eigentlich ist es natürlich Overkill, für eine einzige Abfrage eine komplette zusätzliche Library mitzuschleppen. Deshalb war ich froh zu entdecken, dass DNS mittlerweile mit Java-Bordmitteln funktioniert. Es ist ein bisschen versteckt, weil es Teil der JNDI-Extension ist, die für Verzeichnisdienste aller Art benutzt werden kann.

Wenn man aber erstmal weiß, dass damit auch DNS gemeint ist, geht der Rest ziemlich einfach, wenn man davon absieht, dass man sich das eigentliche Ergebnis mit einer Regular Expression extrahieren muss:

DirContext ictx = new InitialDirContext();
Attribute myAttr = ictx.getAttributes("dns:/_kerberos._udp.ad.uni-koeln.de",
        new String[] {"SRV"}).get("SRV");
NamingEnumeration<?> myEnum = myAttr.getAll();
Pattern p = Pattern.compile(".*\s(\p{Alpha}.*$)");
Matcher m;
while (myEnum.hasMoreElements()) {
      m = p.matcher(myEnum.next().toString());
      if (m.find()) {
   	  myKRB_Servers.append(m.group(1)+":");
      }
}
System.setProperty("java.security.krb5.kdc", myKRB_Servers.toString());

Wir setzen auf diese Weise die Adressen der jeweils aktuellen Kerberos5-Server im Active Directory der Uni. Möglicherweise geht das noch eleganter, aber schon so ist es eine deutliche Verbesserung gegenüber dem vorigen Konstrukt.

Passwortmanager in Browsern sind unsicher

Heise berichtet über einen Browser-Test mit erschreckenden Ergebnissen. Ich habe den Test selbst mit Safari und Firefox 3 durchgespielt. Wenn man das sieht, sollte man eigentlich auf die Verwendung dieser Manager verzichten. Andererseits verwende ich viele unterschiedliche Passwörter (auch aus Sicherheitserwägungen) und kann mir kaum für jede Website das jeweilige Passwort merken …
Man muss hoffen, dass die Browserhersteller hier reagieren.

Perl und Unicode

Obwohl ich eigentlich ganz gut verstehe, wie Unicode in seinen verschiedenen Ausprägungen funktioniert, war mir (und ist zum Teil noch) ein Rätsel, wie Perl damit umgeht. In der Vergangenheit hatte ich vornehmlich das Problem, dass Skripte obskure Unicode-Fehlermeldungen an Stellen produzierten, an denen ich garantiert nicht mit Unicode arbeiten wollte. Ursache ist die Verwendung eines Unicode-Locales unter Linux, z.B. das bei uns standardmäßig eingesetzte „de_DE.UTF-8“. Dafür benutze ich seit einiger Zeit diesen Quickfix:

if (defined $ENV{"LANG"}) {
exec 'env', 'LANG=C', $0, @ARGV unless $ENV{"LANG"} eq "C";
}

Damit wird das Skript garantiert im Locale „C“ ausgeführt.

Jetzt hatte ich zum ersten Mal eine Situation, in der ich wirklich Unicode benutzen wollte. Es ging darum, Umlaute im Input in die Umschreibung „ae“ etc. umzuwandeln. Mein erster Versuch dafür war grundsätzlich richtig:

$input =~ s/ä/ae/g;

Das funktionierte aber nicht, d.h. die Umlaute blieben erhalten. Nach etwas Suchen habe ich gefunden, dass man das Pragma utf8 setzen muss, wenn solche Zeichen im Skripttext auftauchen. So funktioniert es also:

use utf8;

$input =~ s/ä/ae/g;

NB: das setzt voraus, dass das Skript mit einem UTF8-fähigen Editor geschrieben und gespeichert ist.

Phishers Fritz

… fängt frische Phishe.

Derzeit sind wieder Phishing-Mails im Umlauf, die gezielt Passwörter von Nutzern der Uni Köln abphishen wollen. Hintergrund ist, dass mit Hilfe der Passwörter Spamkampagnen geskriptet über das Webmail-System der Uni gefahren werden können. Das ist kürzlich bereits einmal gelungen. Wir haben das Problem recht schnell erkannt und den geknackten Account temporär gesperrt, aber die Gefahr ist damit nicht grundsätzlich gebannt. Und es beweist, dass es immer wieder leichtgläubige Opfer gibt. Ein einziges reicht ja schon, um potenziell viel Schaden anzurichten.

Das größte Problem bei solchen Spam-Wellen ist aus unserer Sicht, dass die Reputation der Uni sinkt – sowohl die reale als auch die virtuelle. Mit Letzterem meine ich, dass Mailserver der Provider nur noch gedrosselt von uns Mail annehmen, bzw. wir im schlimmsten Fall auf sog. Blacklists landen und gar keine Mail mehr von uns angenommen wird. Beides ist in der Vergangenheit schon vorgekommen.