SendMessage Tutorial

05/16/2011 15:37 Kraizy​#1
Hi,
da hier immer öfters gefragt wird, wie man denn einen Text aus einem externen Programm (evtl. auch einigen Spielen) ausliest oder an diesen einen Text sendet, ohne dass dieser im Vordergrund ist, habe ich mich entschloßen ein kleines Tutorial zu schreiben. Programmieren werde ich das ganze in VB.NET, achja und ich werde hier keine Grundlagen erklären à la "nun doppelklick auf den Button, um in das Button_Click-Ereignis zu kommen", sowas müsstet ihr schon selbst wissen.

Eingeteilt ist das Tutorial in drei Bereiche, einmal das Auslesen, das Senden bzw. Schreiben eines Textes und das Anklicken eines Buttons.

Joa, und nun genug gequatscht, fangen wir mal an:

1. Teil - Auslesen

Was brauchen wir dafür?
  • Microsoft Spy++ (enthalten im Visual Studio [um unsere TextBox zu finden])
  • unser "Opfer"-Programm ;)

Als Testprogramm, habe ich einfach mal schnell ein simples Programm in VB.NET erstellt, welche zwei TextBoxen besitzt. Damit das ganze nicht zu leicht wird und man die Vorgänge besser verdeutlichen kann, habe ich diese bewusst in einige Steuerelemente gepackt. D.h. die TextBoxen sind in einer GroupBox, welche sich wiederum in einem TabControl befindet. Das macht das ganze etwas schwieriger, aber auch realistischer beim späteren Umsetzen an anderen Programmen, da diese ja auch oft komplex aufgebaut sind.

Mein Programm sieht also so aus:
[Only registered and activated users can see links. Click Here To Register...]

Nun kommen wir auch schon zu Spy++.
Dazu gehen wir in Visual Studio auf Extras -> Spy++.
Hier suchen wir nun nach unserem Testprogramm:
[Only registered and activated users can see links. Click Here To Register...]

Klicken wir nun links auf das [+]-Zeichen bis alle Untereinträge geöffnet wurden.
Spy++ zeigt uns den Aufbau des Programms an, also welches Control (Steuerelement, in unserem Fall also die TextBoxen) wo untergeordnet ist und wie dieses heißt.
Aufgelistet werden die Einträge folgendermaßen: "Fenstertitel" Klassenname
Fenstertitel habe ich im folgenden Bild grün markiert, und den Klassennamen blau:
[Only registered and activated users can see links. Click Here To Register...]

Nun haben wir auch schon alle Informationen, die wir brauchen und können mit dem Coding-Teil beginnen.
Um unsere beiden TextBoxen zu finden benutzen wir zwei Windows-API's:
  • FindWindow (um das Hauptfenster zu finden)
  • FindWindowEx (um die untergeordneten Controls zu finden)

PHP Code:
//FindWindow
<DllImport("user32.dll"SetLastError:=TrueCharSet:=CharSet.Auto)> _
Private Shared Function FindWindow_
     ByVal lpClassName 
As String_
     ByVal lpWindowTitle 
As String) As IntPtr
    End 
Function

//FindWindowEx
<DllImport("user32.dll"SetLastError:=TrueCharSet:=CharSet.Auto)> _
Private Shared Function FindWindowEx(ByVal parentHandle As IntPtr_
                      ByVal childAfter 
As IntPtr_
                      ByVal lpClassName 
As String_
                      ByVal lpWindowTitle 
As String) As IntPtr
    End 
Function 
Ersteinmal müssen wir das Hauptfenster selbst finden, dazu benutzen wir die FindWindow-API.

Schauen wir uns mal den Aufbau der FindWindow-API an:
  • FindWindow(lpClassName As String, lpWindowTitle As String) As IntPtr

Als 1. Parameter wird also der Klassenname und als 2. Parameter der Fenstertitel benötigt.

Nun gehen wir zurück zu Spy++ und nehmen die Werte raus.
Bei meinem Testprogramm ist der Klassenname WindowsForms10.Window.8.app.0.b7ab7b
und der Fenstertitel Form1.

Wir erstellen uns also eine IntPtr-Variable und suchen das Hauptfenster:
PHP Code:
Dim Hauptfenster As IntPtr FindWindow("WindowsForms10.Window.8.app.0.b7ab7b""Form1"
So, das Hauptfenster haben wir also schonmal.
Gehen wir zurück zu Spy++ und schauen welches Control darunterliegt -> das TabControl.

Ab hier verwenden wir die FindWindowEx-API. Schauen wir uns mal den Aufbau an:
  • FindWindowEx(parentHandle As IntPtr, childAfter As IntPtr, lpClassName As String, lpWindowTitle As String) As IntPtr

parentHandle = das übergeordnete Control. Gehen wir in Spy++ einen Eintrag über unserem TabControl nach oben und was sehen wir? Das Hauptfenster selbst. D.h. das TabControl liegt direkt auf dem Hauptfenster. Da wir diesen ja schon gefunden haben und ihn der Variable "Hauptfenster" übergeben haben, benutzen wir diese einfach auch.

childAfter = Rechtsklick auf den Eintrag (das TabControl) in Spy++ -> Eigenschaften -> oben im Reiter auf Fenster -> Vorheriges Fenster -> Beschriftung: *WERT*

[Only registered and activated users can see links. Click Here To Register...]

Wie wir sehen steht da einfach nur "", also nichts. In VB.NET schreiben wir also "IntPtr.Zero" hin.

lpClassName = Klassenname, das blau markierte auf dem 3. Bild oder Rechtsklick auf den Eintrag -> Eigenschaften -> oben im Reiter auf Klasse -> Klassenname: *WERT*

Bei mir ist es WindowsForms10.SysTabControl32.app.0.b7ab7b.

lpWindowTitle = Fenstertitel, das grün markierte auf dem 3. Bild.
Bei mir steht wieder einfach nur "", hier benutzen wir in VB.NET "Nothing".

Wir erstellen also auch für das TabControl eine IntPtr-Variable und übergeben ihr die Informationen:

PHP Code:
Dim TabControl As IntPtr FindWindowEx(HauptfensterIntPtr.Zero"WindowsForms10.SysTabControl32.app.0.b7ab7b"Nothing
Laut Spy++ kommt als nächstes die TabPage1 des TabControls. Diese befindet sich logischerweise im TabControl, also benutzen wir als parentHandle die eben erstellte IntPtr-Variable "TabControl" und die anderen Werte lesen wir einfach in Spy++ aus, genauso wie eben zuvor.

PHP Code:
Dim TabPage1 As IntPtr FindWindowEx(TabControlIntPtr.Zero"WindowsForms10.Window.8.app.0.b7ab7b""TabPage1"
Als nächstes kommt die GroupBox, hier genau das selbe:

PHP Code:
Dim GroupBox As IntPtr FindWindowEx(TabPage1IntPtr.Zero"WindowsForms10.Window.8.app.0.b7ab7b""Test Group"
So, nun kommen wir zu unseren beiden TextBoxen.
Unser Problem ist jedoch, dass beide den gleichen Klassennamen haben und der Fenstertitel ist einfach der Text der in der TextBox steht. Da wir diesen ja auslesen wollen und dieser sich ändert, können wir ihn schlecht der IntPtr-Variable übergeben. Was machen wir denn nun? Zum Glück gibt es ja den Parameter childAfter in der FindWindowEx-API. Wie bereits gesagt finden wir diesen, indem wir Rechtsklick auf den Eintrag in Spy++ machen -> Eigenschaften -> oben im Reiter auf Fenster -> Vorheriges Fenster -> Beschriftung: *WERT*

Die obere TextBox in Spy++ (welche komischerweise die 2. TextBox in meinem TestProgramm ist) hat als Wert: "", also nichts. D.h. wir benutzen hier wieder IntPtr.Zero. Bei der unteren TextBox haben wir jedoch den Wert: "second textbox" (der Textinhalt der 2. TextBox in meinem Testprogramm). D.h. hier benutzen wir nicht IntPtr.Zero, sondern die obere TextBox selbst:

PHP Code:
//da in Spy++ die 2. TextBox über der 1. ist, mache ich das hier genauso:
Dim TextBox2 As IntPtr FindWindowEx(GroupBoxIntPtr.Zero"WindowsForms10.EDIT.app.0.b7ab7b"Nothing)

Dim TextBox1 As IntPtr FindWindowEx(GroupBoxTextBox2"WindowsForms10.EDIT.app.0.b7ab7b"Nothing
Nun haben wir endlich unsere beiden TextBoxen gefunden und können den Text auslesen. Dazu benötigen wir einige API's und die Funktion zum Auslesen selbst (diese werde ich jetzt nicht detailiert erklären):

PHP Code:
//SendMessageByInt (um die Textlänge auszulesen)
Private Declare Function SendMessageByInt Lib "user32.dll" Alias "SendMessageA" _
  
(ByVal hWnd As IntPtr_
  ByVal uMsg 
As Integer_
  ByVal wParam 
As Integer_
  ByVal lParam 
As Integer) As Integer

//SendMessageByString
Private Declare Function SendMessageByString Lib "user32.dll" Alias "SendMessageA" _
     
(ByVal hWnd As IntPtr_
     ByVal uMsg 
As Integer_
     ByVal wParam 
As Integer_
     ByVal lParam 
As StringBuilder) As Integer

//Konstanten
Const WM_GETTEXT As Integer = &HD
Const WM_GETTEXTLENGTH As Integer = &HE

//Auslesen-Funktion:
Public Function GetText(ByVal hwnd As IntPtr) As String
        
If (Not hwnd.Equals(IntPtr.Zero)) Then
            Dim length 
As Integer SendMessageByInt(hwndWM_GETTEXTLENGTH00)

            If 
length 0 Then
                Dim SB 
As New StringBuilder(length)
                
Dim Result As Int32 SendMessageByString(hwndWM_GETTEXTlength 1SB)

                If 
Result <> 0 Then
                    
Return SB.ToString()
                Else
                    Return 
String.Empty
                
End If
            Else
                Return 
"Kein Text vorhanden"
            
End If
        Else
            Return 
"Handle nicht gefunden"
        
End If
    
End Function 
Die GetText-Funktion benötigt nur einen Parameter, das Handle des Controls. Diese haben wir ja bereits in unsere IntPtr-Variablen geschrieben und können diese nun einfach übergeben. Um nun z.B. den Text der 2. TextBox in einer MessageBox auszugeben benutzen wir einfach folgenden Aufruf:

PHP Code:
MessageBox.Show(GetText(TextBox2)) 
Setzen wir diesen Aufruf z.B. in einen Button und testen es einfach mal.
Mein Ergebnis:
[Only registered and activated users can see links. Click Here To Register...]
Klappt also ;)

So, nun kommen wir zum zweiten Teil - Text in die TextBox schreiben.

2. Teil - Schreiben/Senden
In diesem Teil werden wir nun versuchen, den Text aus der TextBox nicht auszulesen, sondern diesen zu ändern bzw. setzen. Da wir ja bereits im 1. Teil unsere beiden TextBoxen gefunden haben, müssen wir auch nicht mehr so viel tun.

Um den Text zu übergeben benötigen wir wieder die SendMessage-API, nun jedoch mit einem kleinen Unterschied was die Parameter angeht:

PHP Code:
//SendMessage
Private Declare Function SendMessage Lib "user32.dll" Alias "SendMessageA" _
  
(ByVal handle As IntPtrByVal uMsg As Integer_
   ByVal wParam 
As IntPtrByVal lParam As String) As IntPtr

//Konstante
Const WM_SETTEXT As Integer = &HC 
Nun rufen wir diese einfach auf und übergeben die Parameter:
PHP Code:
//hiermit setze ich den Text der 1. TextBox in meinem Testprogramm auf "test123"
SendMessage(TextBox1WM_SETTEXT0"test123"
Das ganze nun z.B. in einen Button einfügen und probieren, mein Ergebnis:

Vorher:
[Only registered and activated users can see links. Click Here To Register...]

Nachher:
[Only registered and activated users can see links. Click Here To Register...]

Klappt also auch!

Ja, das wars eigentlich auch schon mit dem 2. Teil. Etwas kurz, aber naja. Werde mich bemühen, die einzelnen Parameter nochmal genauer zu beschreiben, damit auch jeder versteht, warum der Aufruf denn so aussieht.

Achja, habe mich entschloßen, noch einen 3. Teil zu erstellen, indem gezeigt wird, wie man einen Button anklicken kann. Da ich gerade jedoch nicht viel Zeit habe, wird es etwas dauern, also einfach ein bisschen warten.;)

3. Teil - Button anklicken
So, nach sehr langer Zeit kommt nun auch der 3. Teil - einen Button anklicken.
Dieser Teil wird auch nicht besonders lang sein, da wir nur den Button, den wir anklicken wollen, finden müssen (wird bereits in Teil 1 bzw. 2 detailiert erklärt) und mit der SendMessage-API klicken lassen.

Die Deklarationen:
PHP Code:
//SendMessage-API, um den Button anzuklicken
Private Declare Function SendMessage Lib "user32.dll" Alias "SendMessageA" _
  
(ByVal hWnd As IntPtr_
  ByVal uMsg 
As Integer_
  ByVal wParam 
As Integer_
  ByVal lParam 
As Integer) As Int32

//Konstante
Const BM_Click As Integer = &HF5 
Nun der Aufruf:
PHP Code:

SendMessage
(ButtonHandleBM_Click00
In diesem Beispiel ist "ButtonHandle" die IntPtr-Variable, die die Werte (ClassName bzw. WindowTitle) des Buttons, den wir anklicken wollen, enthält. Wie bereits gesagt, ist das Finden des Controls im 1. Teil erklärt.

Das war's auch schon mit dem 3. Teil!

4. Teil - ComboBox-Eintrag auswählen
Da mich einige Leute in Skype gefragt haben, wie man einen Eintrag aus einer ComboBox auswählen kann, mache ich hier nochmal kurz ein kleines Beispiel.

Deklarationen:
PHP Code:
Private Declare Function SendMessage Lib "user32.dll" Alias "SendMessageA" _
  
(ByVal handle As IntPtrByVal uMsg As Integer_
   ByVal wParam 
As IntPtrByVal lParam As String) As IntPtr

Const CB_SETCURSEL As Integer = &H14E 
Aufruf:
PHP Code:
SendMessage(ComboBoxHandleCB_SETCURSEL0Nothing
In diesem Beispiel wird der 1. Eintrag ausgewählt (d.h. 0 ist der 1. Eintrag, 1 ist der 2. Eintrag usw). Achja, den Text der ComboBox auszulesen funktioniert genauso wie im 1. Teil des Tutorials.

Mit freundlichen Grüßen
Kraizy
05/24/2011 18:50 Kraizy​#2
#Teil 2 hinzugefügt
05/24/2011 21:33 mrapc#3
Super geil danke man
Damit kann man mal richtig was Anfangen :D
05/27/2011 03:22 xSnake#4
Ich frage mich wie du dafür so wenig thx gekriegt hast ?! :o
Das Tut ist perfect !

Hast mir sehr geholfen !!!
05/27/2011 13:53 Kraizy​#5
Quote:
Originally Posted by G.Hirn View Post
Ich frage mich wie du dafür so wenig thx gekriegt hast ?! :o
Tja..fast keiner schaut sich halt erstmal im Forum um, bevor er Threads erstellt. Werden immer noch Fragen zu diesem Thema gestellt, kann man wohl nichts machen^^
08/24/2011 15:16 mrapc#6
Ich habe bei Folgenden stellen schwirikeiten:
Code:
<DllImport
Fehler
Der <DllImport is nicht definiert

und
Code:
StringBuilder
Fehler:
StingBuilder ist nicht Definiert

Code:
SendMessageByInt
Fehler
Auf das Objekt kann aufgrund seiner Schutzstufe nicht zugegriffen werden...
08/24/2011 20:14 Kraizy​#7
Normalerweise ist bei einem Fehler der Begriff blau unterstrichen und ein kleiner roter Strich vorhanden. Wenn du mit der Maus auf diesen Strich gehst, poppt ein kleines weißes Kästchen mit einem roten Ausrufezeichen auf. Dort draufklicken und du bekommst Vorschläge, wie du den Fehler verbessern kannst. In diesem Fall: Importieren Sie "System.Runtime.InteropServices". Also einfach ein Import setzen:

PHP Code:
Imports System.Runtime.InteropServices

Public Class Form1

<DllImport("user32.dll"SetLastError:=TrueCharSet:=CharSet.Auto)> _
Private Shared Function FindWindow_
     ByVal lpClassName 
As String_
     ByVal lpWindowTitle 
As String) As IntPtr
    End 
Function

End Class 
08/24/2011 21:32 mrapc#8
man hätte mir auch auffallen können :D
08/27/2011 13:25 oOSevenDeluxeOo#9
Danke
:D
09/02/2011 20:28 Diablo_#10
Hi,

nettes Tutorial. EIne Frage hätte ich aber. Kann man mit Spy++ auch die APIs von anderen Programmen wie zum Beispiel Skype herausfinden? Dort gibt es ja auch die "Textbox", wo man seinen Text reinschreibt, den man dann sendet.

Geht das damit auch bei Spielen?

Grüße
09/02/2011 20:57 Kraizy​#11
Was genau meinst du mit API's von anderen Programmen? Also die TextBox von Skype, wo du deinen Text reinschreibst, kannst du damit finden ja, aber ich weiß nicht genau welche API's du meinst.
Bei Spielen kommts halt drauf an, was das für Spiele sind, aber in den meisten Fällen wird das damit denke ich nicht klappen. Um trotzdem noch Tastenanschläge an ein Spiel zu senden (z.B. Counter-Strike usw.) dann musst du es mal mit Send- oder DirectInput versuchen, einfach mal danach googlen...
09/26/2011 22:52 Banana_jo#12
Ich hab noch ne frage auf:

StringBuilder (ist nicht definiert)

wie mach ich das genau? :D
als was soll ich das definieren?

mfg
Banana


edit:
sry hab schon :D
DANKE FÜR DEN TUT :D

EDIT2:

ich hab ein problem :)

undzwar will ich halt die einzelnen handles von einem 3D game raussuchen aber in spy gibts nur EIN fenster und der classenname ist nur "GxWindowClassD3d" und wenn ich textauslesen will kommt immer "Handle nicht gefunden" obwohl ich Den Klassenname und den titel eingegeben habe...

kann mir vllt da jemand helfen?
09/27/2011 14:31 Kraizy​#13
Um welches Spiel geht es denn? Ich denke aber, dass das mit SendMessage eh nicht klappen wird..
09/27/2011 16:55 Banana_jo#14
also ich habs jetz geschaft an meinem haupt fenster etwas zu senden...damit hier

Code:
        sTitle = "Titel"
        lhWnd = FindWindow(vbNullString, sTitle)
        TextBox2.Text = lhWnd
        SendMessage(lhWnd, WM_CHAR, 49, vbNullString)
nur die "49" ist ein buchstabe...und jetz frage ich mich wie ich alle tasten die ich drücke senden kann...
sohne art keylogger hab ich aber wie verbinde ich die beiden...
ich hab versucht die 49 durch texte auszutauschen aber funzt nicht...


oder kann mir jemand sagen ob es ne liste gibt wo die zahlen drinne stehen...wie die zahlen heißen..
vllt gibt es eine möglichkeit ein keylogger zu erstellen der die zahlen auspuckt..anstatt den buchstaben den man drückt..


edit:
so habs geschaft mit dem keylogger mit den zahlen..

so mein hauptproblem ist wenn ich den sendmessage in nen timer lege wird die letzgedrückte taste DAUERTHAFT gesendet solange der timer auf true ist...aber wie mach ich das das der timer nur wenn ich wirklich raufdrücke senden soll...und zb wenn ich jetz gedrückt halte er dann öfters sendet..

mfg
Banana
09/27/2011 17:12 Kraizy​#15
Wie sieht denn die SendMessage-Funktion aus, die du benutzt?
Edit: Hier mal ein Beispiel, wenn du auf deiner Form irgendwas in deine TextBox eintippst, wird dies auch an das Programm gesendet:
PHP Code:
//Deklarationen
Private Declare Function SendMessage Lib "user32.dll" Alias "SendMessageA" (ByVal hwnd As IntPtrByVal wMsg As IntegerByVal wParam As IntegerByVal lParam As Integer) As Integer
Private Const WM_CHAR As Integer = &H102
Private hwnd As IntPtr

//Bei Eingabe in deine TextBox
Private Sub deineTextBox_KeyPress(...) Handles deineTextBox.KeyPress
   SendMessage
(hwndWM_CHARAsc(e.KeyChar), 0)
End Sub 
Mit e.KeyChar bekommst du eben das Zeichen, der gedrückten Taste und mit Asc() wandelst du das dann in den richtigen Integer-Wert um.

Wenn du das ganze ohne TextBox machen willst, dann frägst du die gedrückten Tasten systemweit ab und übergibst es dann der SendMessage-Funktion. Die Tasten kannst du mit der GetAsyncKeyState-API abfangen.