Ein Beispiel zur Verwendeung von CBT-Hooks

Bisweilen möchte man das Verhalten bestimmter Programme etwas ändern. In diesem Beispiel geht es darum, für eine scherzhafte Anwendung Bilder aus einer Digitalkamera zu modifizieren oder zu ersetzen.

Diese Webseite wendet sich an Windows-C-Programmierer. Das Beispiel wurde in Visual C++ 6.0 erstellt.

Wie immer bei mir ohne Laufzeitbibliothek.

Das Wirtsprogramm

Die Bilder sollen life geknipst und umgehend auf einem Beamer dargestellt werden. Dazu eignet sich, je nach Kameramodell, bspw. das Programm DSLR Remote Pro. (Nur wenige Digitalkameras sind für diese Art Livebildübertragung geeignet!)

Ein (menschlicher!) Assistent soll nun durch Tastendruck steuern, dass bestimmte Motive nicht klappen (bspw. unscharf oder einfach schwarz werden).

Der Ansatz

Würde das Wirtsprogramm als Quelltext vorliegen, wäre alles ganz einfach.
Dies ist jedoch unter Windows selten der Fall, und für kommerzielle Software anscheinend niemals.

Dennoch ist man nicht chancenlos. Dazu gibt es Windows Hooks.

Als erstes wurde mittels Spy++ (beim Visual C++ dazu) untersucht, welche Art Kindfenster dieses Programm zur Bildausgabe verwendet.
Das Programm verwendet ein Fenster der Klasse "Static" mit dem entscheidenden Stilbit SS_IMAGE. Dies ist ein Glücksfall, weil die Nachricht zum Setzen des Bildes bekannt ist: STM_SETIMAGE. Diese Nachricht abfangen und ändern ist das Ziel.

Programmiertechnik

Kode unterschieben

Um überhaupt Kode im Kontext des Wirtsprogramms ausführen zu können, muss dieser im Adressraum des Wirtsprogramms erscheinen. Dies erfordert eine DLL.

Es gibt verschiedene Methoden, um eine DLL in einen Wirtsprozess einzuschleusen. Bei der Verwendung von globalen Windows-Hooks (SetWindowHookEx()) kümmert sich Windows bereits selbst darum, und man hat die wenigste Arbeit.

Solche Hook-DLLs benötigen zwanghaft eine Variable, die über alle Prozesse sichtbar ist; sie muss sich in einem sog. shared-Datensegment befinden. Die Attribute für das Segment namens .shared (Name beliebig) werden mit der Linker-Befehlszeile in den Projekteinstellungen festgelegt: /section:.shared,rws

Wichtig! Falsche Projekteinstellungen führen zum Windows-Crash!

Kode im richtigen Moment ausführen lassen

Um sich beim Versenden von STM_SETIMAGE einzuhängen (wird ja sicherlich per SendMessage() oder SendDlgItemMessage() verschickt), wäre der Hooktyp WH_CALLWNDPROC naheliegend. Er bewirkt jedoch viel zuviel Filteraufwand.

Wesentlich zielgerichteter ist es, das gewünschte Fenster als Unterklasse zu erstellen, d.h. die Fensterprozedur anzuzapfen. Dazu muss man nur das Erzeugen des Fensters abfangen. Dies erledigt man mit CBT-Hooks (CBT = computer based training, svw. Lernprogramm). Das heißt, der Hooktyp ist WH_CBT.

Jetzt muss die Anzapfung nur noch bei dem gewünschten Fenster erfolgen. Die notwendigen Vergleichswerte beschafft man sich mittels Spy++.

Für die Anzapfung der Tastatur wird noch ein prozess-lokaler Hook gesetzt.

Die veränderte Fensterprozedur

Die Anzapfung muss jetzt nichts anderes machen als den Parameter lParam zu modifizieren.

Je nach zu ladendem Bild wird mittles LoadImage() ein Bitmap geladen und der lParam-Parameter ersetzt. Welche Datei geladen wird, bestimmt sich aus dem letzten Tastendruck, welcher mit dem Tastatur-Hook (s.u.) erfasst wird. Da aus der Windows-SDK-Dokumentation nicht hervorgeht, wer das Handle löschen soll, bin ich davon ausgegangen, dass der Aufrufer von SendMessage(...,STM_SETIMAGE,...) ein gültiges Handle solange behalten muss, wie das Bild dargestellt werden soll. Das heißt, der Aufrufer muss das Bitmap-Handle löschen. Deshalb wird noch eine entsprechende globale Variable geführt und das ursprüngliche lParam-Handle unbeachtet gelassen.

Eigentlich müsste man noch STM_GETIMAGE filtern und den ursprünglichen lParam-Wert zurückgeben.

Der Tastatur-Hook

Die Tastatur-Anzapfung ist simpel. Wird eine der gewünschten Tasten (numerischer Tastenblock 0..9) gedrückt, wird eine globale Variable gesetzt und der Tastendruck „aufgegessen“ – sonst würde das Wirtsprogramm einen Piep ausgeben.
NumLock muss eingeschaltet sein!

Start des Programms

DLLs kann man nicht starten! Vor geraumer Zeit habe ich ein Hilfsprogramm llib.exe („LoadLib“) geschrieben, was eine DLL lädt (oder mehrere) und dann endlos wartet. llib.exe kann nur gekillt werden, bspw. mit dem Task-Manager oder killllib.exe.

Der Startup-Kode der DLL installiert den Hook; darafhin kriecht die DLL in jeden laufenden Prozess. Weil der Shutdown-Kode der DLL beim Killen von llib.exe aufgerufen wird und dieser den Hook entfernt, zieht sich die DLL umgehend aus den anderen Prozessen zurück.
Nur den Unterklassen-Kode müsste man von Hand aushängen (fehlt hier).

Ganz ohne DLL-Startprogramm kommt man aus, wenn man der DLL einen Einsprungpunkt verpasst, um diese mit rundll32.exe zu starten. Dies wurde hier nicht bewerkstelligt, sondern bei meiner InpOut32.dll.

Debuggen

Mir ist nicht klar, wie man so ein Programm sinnvoll debugt, außer mit SoftICE. Denn Visual C++ debuggt ja nur den eigenen Prozess. Daher habe ich mich darauf verlassen, einigermaßen fehlerfrei zu programmieren, und habe alle Ausgaben mittels OutputDebugString() und MessageBeep() bzw. Beep() eingebaut.

Um diese Meldungen zu sehen, sollte man das Programm DbgView (gibt's kostenlos bei Micorosoft) benutzen.

Ach ja, der Quelltext

...zum Nachlesen und zum Runterladen. Der gesamte DLL-Quelltext ist nur 130 Zeilen lang.