Fernsteuern von anderen Programmen mit Mirana


Programme fernsteuern - Fernsteuerung anderer Progamme mit Mirana durch Simulation von Tastatureingaben und Mausklicks sowie Windows-Nachrichten, DDE und COM.


Durch das Fernsteuern anderere Programme kann man sich viel Klickarbeit ersparen, vor allem wenn viele Dateien in immer gleichen Weise bearbeitet werden sollen oder wenn bei einer Anwendung, die eigentlich im Hintergrund arbeiten soll, immer wieder nervige Dialoge aufpoppen, die immer gleich beantwortet werden müssen. Mirana (mirana.de.to, sites.google.com/site/miranasoft/ oder miranasoft.jimdo.de) enthält dazu einige spezielle Funktionen, die beim Steuern anderer Programme sehr hilfreich sind.

Folgende Möglichkeiten sind denkbar:
  1. Simulation der Tastatur-Eingaben und Mausaktionen
  2. Benutzen einer eingebauten Fernsteuerschnittstelle wie DDE oder OLE bzw. COM.
 Die zweite Möglichkeit soll hier nur kurz gestreift werden, da längst nicht jedes Programm eine solche Möglichkeit bietet und die Beschreibung der Schnittstelle oft schwer zu finden oder gar nicht öffentlich zugänglich ist. So kann man etwa mit folgendem Mirana-Befehl eine Seite im Internetexplorer öffnen:
DdeExecute "IExplore","WWW_OpenURL", "www.wikipedia.de", 5000
Folgende Befehle erhöhen in der in Excel geöffenten Datei Mappe1.xls in Tabelle1 den benannten Wert „Name1“ um eins:
StrDef str,100
DdeRequest "Excel","[Mappe1.xls]Tabelle1","Name1",str,100
DdePoke "Excel","[Mappe1.xls]Tabelle1","Name1",#,(Val(str,2)+1)

Ohne Kenntnis einer Schnittstelle funktioniert dagegen die  Simulation von Tastatur-Eingaben und Mausaktionen sowie Steuerung über Windows-Nachrichten. Allerdings kann nach einer Versionsänderung eine Anpassung erforderlich sein, wenn dabei die Bedienoberfläche geändert wurde.

Dabei führt man mit dem Programm einfach die gewünschten Aktionen aus, bemüht sich dabei aber, möglichst nur die Tastatur zu benutzen. Sind in einem Menü rechts vom Menütext Tastenbefehle angegeben, sollte man diese verwenden. Andernfalls sollte man schauen, ob einzelne Buchstaben unterstrichen sind und kann diese dann zusammen mit der Alt-Taste verwenden. Wenn in einem Menü oder einem Dialog keine Buchstaben unterstrichen sind, hilft oft, die Alt-Taste kurz drücken und wieder loslassen. Hilft das nicht, kann man es mit dem ersten Buchstaben des Menütextes versuchen (ev. mehrfach, gefolgt von der Eingabetaste), oder man bewegt sich mit TAB, Shift+TAB oder den Kursortasten durch das Menü oder den Dialog und drückt an der richtigen Stellen die Eingabetaste oder die Leertaste. Bei Karteikarten ist auch Strg+TAB oder Shift+Strg+TAB möglich, um zwischen den Reitern zu wechseln.

Damit man sich die Eingaben nicht alle merken muss, ist ein sog. Keylogger sehr nützlich, welcher alle Tastendrücke aufzeichnet. Dazu habe ich ein kleines Mirana-Makro namens Keylogger.mac geschrieben (steht in Mirana.zip im Verzeichnis Makros). Dies taugt zwar weniger zu Spionagezwecken, weil es sich keine Mühen macht, seine Tätigkeit zu verbergen. Dafür kommt es ohne Installation und Treiber aus. Außerdem werden auch alle Mausklicks aufgezeichnet, und zwar mit der genauen Position des Mauszeigers und Informationen über das Fenster, in dem der Mausklick landet und über dessen Elternfenster. Dies ist sehr nützlich, wenn eine reine Tastaturbedienung nicht möglich ist. Aber auch bei Tastatureingaben muss man dafür sorgen, dass sie im richtigen Fenster landen oder warten, bis ein Fenster geöffnet oder geschlossen wurde. Dazu braucht man Informationen wie Fenstertitel oder Klassennamen, um das richtige Fenster zu finden. Das gleiche gilt auch, wenn man einem Fenster oder Steuerelement eine Nachricht schicken will (hier ist auch die ID wichtig).

Man startet also Keylogger.mac innerhalb von Mirana oder startet direkt das kompilierte Keylogger.exe und klickt dann auf „Start“. Dann startet man das fernzusteuernde Programm oder holt es in den Vordergrund. Als erstes sollte man die Titelleiste des Programms anklicken (wenn es keine Titelleiste hat, dann irgendwo ins Programmfenster), damit der Keylogger die Fensterinformationen aufzeichnet. Dies sollte man auch bei jedem im Verlauf der Sitzung neu geöffneten Fenster oder Dialog tun. Dann führt man die gewüschte Aktionen durch, wobei die Menüs mit der Tastatur bedient werden sollten. Bei den Dialogen kann man beide Varianten probieren, also Tastaur- und Maussteuerung, um dann später die einfachere oder effizientere und sicherere Variante zu wählen zu können.

Sobald alle Aktionen aufgezeichnet wurden, klickt man im Keylogger auf „Stop“. Es öffnet sich dann ein Notepad-Fenster mit der erzeugten Log-Datei. Hier ist zu beachten, dass jeder Mausklick in einer eigenen Zeile steht, während die Tasten nacheinander in einer Zeile stehen, bis nach 80 Spalten ein Zeilenumbruch erfolgt. Das genaue Format der Aufzeichnung steht im Kopfbereich der Datei Keylogger.mac (mit Mirana.exe oder einem beliebigen Texteditor öffnen, z. B. Notepad).

Nun startet man Mirana.exe und erzeugt mit „New“ ein neues Fenster für das Fernsteuermakro.
Als erstes muss das Makro nun prüfen, ob das zu steuernde Programm schon läuft. Dazu benutzen wir die Funktion FindChildWindow. Diese Funktion kann anhand des Titels und/oder des Klassennamens jedes in Windows geöffnete Fenster finden, egal, ob es sich um ein sog. Toplevel-Fenster oder um ein in ein anderes Fenster eingebettetes Kindfenster handelt. Da das Hauptfenster eines Programms immer ein Toplevel-Fenster ist, ist der erste Parameter -1. Die Parameter 2 und 3 sind Fenstertitel und Klassenname, welche man aus den Keylogger-Einträgen hinter „window=“ und „class=“ entnehmen kann. Gibt man für einen der beiden Parameter "NULL" ein, dann kann der entsprechende Name beliebig sein. Dies macht Sinn, wenn z. B. der Titel Zusatzinformationen enthält, die jedesmal anders sein können.

Die Funktion FindChildWindow gibt ein Handle des Fensters zurück, wenn es existiert, sonst 0. Dieses Handle übergibt man der Windows-API-Funktion SetForegroundWindow, und das Fenster in den Vordergrund zu holen. Dadurch erreicht man, dass nachfolgende Tastatureingaben über dem Befehl PostKeyString in diesem Fenster landen. Existiert das Fenster noch nicht, muss man das Programm mit dem Befehl Exec starten und warten, bis das Fenster erscheint.

Hier ist ein sehr einfaches Beispiel für eine Fernsteuerung des auf jedem Windowsrechner vorhandenen Programms Notepad. Dabei soll einfach ein neuer Tagebucheintrag zu einer Datei hinzugefügt werden:
sFile=!Macropath+"Mein Tagebuch.txt"
if not FileExist(sFile)
  WriteFile sFile,"Mein Tagebuch"+$13+$10+"-------------"+$13+$10
  CloseFiles
endif
hWndMain = FindChildWindow(-1, "Mein Tagebuch.txt - Editor", "Notepad")
if hWndMain=0
  Exec "notepad "+sFile
  Loop   ;warten, bis Fenster erscheint
    hWndMain = FindChildWindow(-1, "Mein Tagebuch.txt - Editor", "Notepad")
    if (hWndMain<>0) BreakLoop
    Pause 50
  EndLoop
endif
Decl SetForegroundWindow "i=i,user32.dll"
SetForegroundWindow(hWndMain)
PostKeyString "",0x223   ;[lctrl-0x23(end)] -> ans Textende
sDatum=!Time
sDatum[10]=0     ;nur Datum, keine Uhrzeit
sDatum+=":"+$10  ;: und neue Zeile hinzufügen
PostKeyString $10+sDatum
PostKeyString "Keine besondere Vorkommnisse."+$10
PostKeyString "ds",-4  ;Speichern mit Alt+D,S
End

So könnten die entsprechenden Keylogger-Einträge ausgesehen haben:
[0x01(lmouse)]{Pos=622,255, window="Mein Tagebuch.txt - Editor", class="Notepad",
    Pos=361,240, ID=-1064044752}
[lctrl-0x23(end)]17[0xbe(.)]04[0xbe(.)]2012[rshft-0xbe(.)][0x0d(ent)][rshft-K]ei
ne besondere [rshft-V]orkommnisse[0x0d(ent)][lalt-d][lalt-s]

Informationen zu den Mirana-Befehlen und -Funktionen erhält man, indem man im Mirana-Editorfenster das entsprechende Wort anklickt und F1 drückt. Windows-API-Funktionen wie SetForegroundWindow oder SetCursorPos kann man dagegen bei Google eingeben, ev. zusammen mit „msdn“. Solche Funktionen müssen vor der ersten Verwendung in mit dem Befehl Decl deklariert werden. Eine Sammlung mit wichtigen Deklarationen ist in WindowsApi.mac enthalten.

Übersicht der wichtigsten Befehle:


FindChildWindow(hParentWnd, Title, Class): sucht ein Fenster mit einem bestimmten Titel und/oder Klassenamen und gibt ein Handle des Fensters zurück, wenn es existiert, sonst 0. Ist hParentWnd=-1, so werden nur Toplevel-Fenster gesucht, ist hParentWnd=0, werden alle Fenster durchsucht, andernfalls werden nur die Kindfenster von hParentWnd sowie deren Kindfenster usw. durchsucht. Wird bei einem der beiden Namen Title oder Class "NULL" eingegeben, dann spielt dieser Name keine Rolle (Class kann auch ganz weggelassen werden). Mit einem vierten Parameter (Skip) kann man eine gewisse Anzahl Treffer überspringen, falls es mehrere Fenster mit dem Suchkriterium gibt, dem Fenstertitel kann man danach mit !StrResult abfragen.

PostKeyString Text: Simuliert die Eingabe eines Textes über die Tastatur. Der Text kann (in Anführungszeichen) beliebige Zeichen enthalten (Anführungszeichen verdoppeln!), auch Zeichen, die nicht auf der Tastatur stehen – die entsprechende Tastenkombinationen werden passend simuliert.

PostKeyString Text, opt  mit negativem opt: Simuliert die Eingabe eines Textes mit einer oder mehreren dabei ständig gedrückt gehaltenen Umschalttasten. opt=-1: Shift, opt=-2: Strg, opt=-4: Alt, die Werte können addiert werden.

PostKeyString "",keycode: Simuliert den Druck einer einzelnen Taste über seinen Keycode (1-255, siehe Mirana-Hilfe von !KeyStat). Auch Mausklicks können so simuliert werden: Linksklick=1, Rechtsklick=2, Mausradklick=4. Durch Addieren eines Wertes > 255 können zusätzlich Umschalttasten gedrückt werden oder die Taste wird nur niedergedrückt oder nur losgelassen, siehe Mirana-Hilfe.

SetForegroundWindow (hWnd): Bringt das Fenster mit dem Handle hWnd in den Vordergrund
Deklaration: Decl SetForegroundWindow "i=i,user32"

SetCursorPos(x, y): Setzt den Mauszeiger auf die Bildschirmposition x, y
Deklaration: Decl SetCursorPos "l=ll,user32"

GetWindowRect(hWnd, vRect): Abfrage von Position und Größe eines Fensters hWnd. Der Vektor vRect muss zuvor mit vRect=Vector(4,4) erzeugt werden. Dabei sind die Werte vRect[0] bis vRect[3] die Rechteckpositionen Links, Oben, Rechts, Unten.
Deklaration: Decl GetWindowRect "i=ip,user32"
Als Beispiel Mausklick auf die Mitte von Button hButt im aktiven Fenster:
vRect=Vector(4,4)
GetWindowRect(hButt,vRect)
SetCursorPos((vRect[0]+vRect[2])/2, (vRect[1]+vRect[3])/2)
PostKeyString "", 1   ;Linksklick

SendMessageA (hWnd, Msg, WPar, LPar): Sendet die Nachricht Msg an das Fenster mit dem Handle hWnd
Deklaration: Decl SendMessageA "i=lllp,user32"

Die wichtigsten Nachrichten:
WM_LBUTTONDOWN = 0x0201
WM_LBUTTONUP   = 0x0202
WM_KEYDOWN = 0x0100
WM_KEYUP = 0x0101
WM_CHAR = 0x0102   ;z.B. SendMessageA(hEdit, WM_CHAR, 'A', 3) -> "AAA"
WM_COMMAND = 0x0111  ;z.B. SendMessageA(hDlg,WM_COMMAND,1,0)
;                     -> Klick auf Button mit ID=1 in Dialog hDlg
Als Beispiel Mausklick auf Button hButt im Fenster hPopup:
hButt=FindChildWindow(hPopup, "Download fortsetzen", "Button")
WM_LBUTTONDOWN = 0x0201
WM_LBUTTONUP   = 0x0202
x=4, y=4   ;Klickposition relativ zur Buttonposition
SendMessageA(hButt, WM_LBUTTONDOWN, 0, x or (y<<16))
SendMessageA(hButt, WM_LBUTTONUP, 0, x or (y<<16))

PostMessageA (hWnd, Msg, WPar, LPar): Wie SendMessageA, allerdings kehrt diese Funktion sofort zurück, und nicht erst, wenn die Nachricht bearbeitet wurde. Ungeeignet, wenn Werte zurückgegeben werden sollen, zwingend nötig beim Öffnen von modalen Dialogen (SendMessageA würde hier hängen, bis der Dialog wieder geschlossen wird).
Deklaration: Decl PostMessageA "i=lllp,user32"
 
GetWindowTextA(hWnd, sText, MaxSize): Abfragen des Fenstertextes.
Deklaration: Decl GetWindowTextA "i=i$i,user32"
Beispiel:
StrDef sTxt,400
GetWindowTextA(hWnd, sTxt, 400)
 
SetWindowTextA(hWnd, sText): Ändern eines Fenstertextes.
Deklaration: Decl SetWindowTextA "i=is,user32"

IsWindow(hWnd): Abfrage, ob hWnd noch ein gültiges Fensterhandle ist.
Deklaration: Decl IsWindow "l=l,user32"

Steuerung per Mausklicks und Nachrichten 


Wenn man mit der Steuerung per Tastatur nicht alle Funktionen erreicht, kann man auch den Mauszeiger mit SetCursorPos positionieren und mit PostKeyString "",1  einen Mausklick erzeugen. Die richtige Position kann man aus dem Keylogger-Log auslesen ([0x01(lmouse)]{Pos=…). Um aber unabhängig von der aktuellen Fensterposition zu werden, kann man die Position mit GetWindowRect abfragen. Da im Keylogger-Log auch die Fensterpositionen aufgezeichnet sind, kann man sich leicht die richtige Kursorposition ausrechnen.

Falls es die Bedienoberfläche die Standardelemente von Windows benutzt, kann man diese auch durch Senden einer Windows-Nachricht steuern. Dazu braucht man zwar mehr Vorwissen darüber, welches Element auf welche Nachricht reagiert, aber dafür muss das entsprechende Fenste nicht aktiv sein.

Das ist dann wichtig, wenn das zu steuernde Programm im Hintergrund arbeiten soll, wärend der Benutzer mit einem anderen Programm arbeitet. Würden immer wieder irgend welche Fenster in den Vordergrund kommen, wärend der Benutzer gerade tippt oder mit der Maus arbeitet, würden die Eingaben im falschen Fenster landen und die Fernsteuerung durcheinanderbringen. Windows-Nachrichten werden dagegen immer direkt an ein bestimmtes Fenster geschickt, unabhängig davon, ob es aktiv ist, oder nicht.
Beispiel:
hDlg= FindChildWindow(0, "Schriftart", "#32770")
hButt=FindChildWindow(hDlg, "OK", "Button")
WM_LBUTTONDOWN = 0x0201
WM_LBUTTONUP   = 0x0202
WM_KEYDOWN = 0x0100
WM_KEYUP = 0x0101
Decl SendMessageA "i=lllp,user32"
SendMessageA(hButt, WM_LBUTTONDOWN, 0, 0)
SendMessageA(hButt, WM_LBUTTONDOWN, 0, 0)
SendMessageA(hButt, WM_LBUTTONUP, 0, 0)
Der letzte Parameter in SendMessageA (hier 0), legt fest, wo auf dem Button der Klick landet: Lpar = x + (y<<16). 0 wäre dann die linke odere Ecke, das ist ok. Wichtig ist nur, dass der Klick nicht außerhalb landet.
Statt der Maus kann man auch die Leertaste betätigen, man benötigt aber noch den „keyboard scan code“ der Leertaste (0x39):
SendMessageA(hButt, WM_KEYDOWN, ' ', 0x390001)
SendMessageA(hButt, WM_KEYDOWN, ' ', 0x390001)
SendMessageA(hButt, WM_KEYUP, ' ', 0x390001)
Der zweimalige Aufruf von WM_LBUTTONDOWN  oder WM_KEYDOWN macht es zuverlässiger – immer wenn der Button schon ausgewählt war, funktionierte es bei mir sonst nicht, warum auch immer.
Die letzten drei Zeilen kann man sich sparen, wenn man gleich folgendes aufruft:
FindChildWindow(hDlg, "OK", "Button", 0x01000000)
Der letzte Parameter 0x01000000 führt zu einer Leertastennachricht an den gefundenen Button.

Fenster mit der Klasse "Edit" reagieren dagegen auf die Nachricht WM_CHAR. Folgende Zeilen schicken "AAA" an das Fenster:
hEdit=FindChildWindow(hWndMain, "", "Edit")
WM_CHAR = 0x0102
SendMessageA(hEdit, WM_CHAR, 'A', 3)

An eine Dialogbox (Klasse "#32770") kann man auch eine WM_COMMAND-Nachricht schicken, wobei Wpar die ID des zu aktivierenden Buttons ist. In einer Messagebox ist typischerweise "OK"=1, "Abbrechen"= 2, "A&bbrechen"=3, "Wieder&holen"=4, "&Ignorieren"=5, "&Ja"=6, "&Nein"=7 (Das & kennzeichnet unterstrichene Zeichen). Die richtige ID kann man aus dem Keylogger-Log auslesen, wenn man auf den jeweiligen Button klickt.
Die folgenden Zeilen beantworten eine Ja/Nein-Messagebox mit der Titelzeile "Bestätigung" mit Ja:
hMgsBox = FindChildWindow(0, "Bestätigung", "#32770")
WM_COMMAND = 0x0111
SendMessageA(hMgsBox,WM_COMMAND,6,0)
Alternative:
FindChildWindow(hMgsBox, "&Ja", "Button", 0x01000000)
Wenn es nur einen Dialog mit einem Ja-Button gibt, kann der erste Parameter von FindChildWindow auch 0 sein. Dann wird der erste gefundene "&Ja"-Button betätigt:
FindChildWindow(0, "&Ja", "Button", 0x01000000)
Hier ist zu beachten, dass die Schreibweise exakt stimmen muss, also korrekte Groß-/Kleinschreibung und & für unterstichene Zeichen, so wie der Name auch im Keylogger-Log auftaucht.

Auch Menüs können oft per Nachricht geöffnet werden, allerdings braucht man dann einen geeigneten Resource-Editor wie z.B. ResHacker.exe, um die Menüs aus der Exe herauszulesen und so die passende ID herauszufinden (in ResHacker die Zahl hinter dem Menütext). Allerdings sollte hier PostMessageA benutzt werden – falls nämlich mit der Nachricht ein modaler Dialog geöffnet wird, würde SendMessageA hängen, bis der Dialog wieder geschlossen wird, und dies könnte dann nur noch der Benutzter tun.
Beispiel:
Decl PostMessageA "i=lllp,user32"
WM_COMMAND = 0x0111
ID=57696  ;Menü Format/Font…
hWndMain = FindChildWindow(-1, "NULL", "WordPadClass")
PostMessageA(hWndMain,WM_COMMAND,ID,0)

Keine Kommentare:

Kommentar veröffentlichen