heute wollen wir uns von Laie zu Laie ein wenig mit DirectX beschäftigen. Als Beispiel nehmen wir eine etwas angestaubte Version, Direct3D8. Die an diesem Beispiel gezeigten Schritte können jedoch auch problemlos auf andere DirectX Versionen übertragen werden.
Vorher jedoch noch kurz was über meine Person. Ich bin bei back2hack.cc als jx bzw. jxMANNY bekannt und beschäftige mich ab und zu mit dem Programmieren oder dem Kompromittieren bestehender Software-Architekturen. Heute konzentrieren wir uns auf das letztere.
Wenn ihr euch jetzt fragt, was das ganze soll und wofür man es benutzen kann, dann schaut euch den folgenden Screenshot an ( auf das Bild klicken zum vergrößern ) . Es geht um die Leiste am oberen Rand, vorstellbar sind aber dutzende andere Anwendungsgebiete. Also weiterlesen!
Herangehensweise
So wird es am Ende Aussehen (zum Vergrößern bitte anklicken). Kommen wir nun aber zum theoretischen Teil. Unser Ziel ist es Text über einen Direct3D8 Spiel anzeigen zu lassen. Wie kann man dies erreichen? Man wird die Direct3D Funktion “Present()” hooken müssen. Dies kann man über sehr viele verschiedene Wege erreichen, die ich jedoch nicht erläutern werden, da wir uns auf eine Proxy-DLL (trojan-dll) beschränken werden. Eine gute Übersicht gibt es hier: http://www.codeproject.com/kb/system/hooksys.aspx. Weiterführende Artikel sind dort auch zu finden.
Die Proxy-DLL
Was ist eine Proxy-DLL? Es ist eine DLL, die die Funktionen der Original-DLL, in unserem Falle die d3d8.dll, nach außen hin anbietet und im Kern die Funktionen der Original DLL aufruft. Dabei lässt sich zwischen den einzelnen Schritten eigener Code einschleusen, um zum Beispiel eigene Sachen zu zeichnen:
Spiel <–> Unserer DLL <–> Original DLL
Das Spiel ruft dann zum Beispiel eine Funktion auf:
Spiel --> Present() --> Blit() --> Present() Rückgabewert <-- ret <-- ret
Unsere DLL wird aufgerufen, doch statt dass wir einfach den Call weiterreichen, führen wir erst unsere eigene Funktion aus und blitten zB. noch Text auf das Fenster, bevor wir Present() aufrufen. Danach leiten wir einfach den Rückgabewert weiter.
Wie muss unsere Proxy-DLL nun jedoch explizit aufgebaut sein? Sprich welches Interface muss es anbieten? Schauen wir uns dazu mal die originale d3d8.dll an und die ausführbare Datei des Spiels in PEiD an.
( PEiD – d3d8.dll )
Wir sehen, dass die d3d8.dll eine für uns wichtige Funktion, nämlich die Direct3DCreate8() nach außen hin anbietet. Doch wie sieht denn der Prototyp von der Funktion aus? Die Calling Convention? Das brauchen wir, um die Funktion später auch in unserer DLL anbieten zu können. Werfen wir einen Blick in den d3d8.h Header:
[codesyntax lang=“cpp“] IDirect3D8 * WINAPI Direct3DCreate8(UINT SDKVersion);[/codesyntax]
Die Funktion gibt einen Zeiger auf ein Direct3D8 Interface zurück, erwartet als Parameter einen unsigned Integer und wird mit der Calling Convention WINAPI (== __stdcall) aufgerufen. Das sollten wir uns für später merken.
Schauen wir uns jetzt die Empires_DMW, die ausführbare Datei unseres Opferprogramms an:
Huh? Die d3d8 wird ja gar nicht importiert? Nun sollte man sich die Datei nochmal genauer anschauen in OllyDbg, ob und wie die DLL dynamisch geladen wird. Einfach in Olly laden und ein BP auf die LoadLibrary API bzw. GetProcAdress. Das entsprechende Listing sieht dann so aus:
[codesyntax lang=“asm“]1006FB5D |. 6A 08 |PUSH 8 ; /Flags = LOAD_WITH_ALTERED_SEARCH_PATH
1006FB5F |. 6A 00 |PUSH 0 ; |hFile = NULL
1006FB61 |. FF75 E0 |PUSH [LOCAL.8] ; |FileName = “C:\\PROGRAM FILES\\EDMW\\d3d8.dll”
1006FB64 |. FF15 EC800C10 |CALL DWORD PTR DS:[<&KERNEL32.LoadLibraryExA>] ; \LoadLibraryExA
[/codesyntax]
Da haben wir ja schon die Stelle, wo die d3d8.dll geladen wird, und zwar in der “Low-Level Engine.dll” (die haben wirklich ein Space in den Namen gehauen), welche wiederum von der EMPIRES_DMW.EXE geladen wird (siehe oben). Setzt man einen BP auf auf GetProcAdress wird man sehen, dass auch die Direct3DCreate8() Funktion geladen wird.
Jetzt wissen wir schon mal das nötigste, um schon einmal das Interface anzubieten. Später müssen wir nur noch herausfinden, welche Funktionen das Interface IDirect3D8, das man durch die Direct3DCreate8() bekommt, anbietet. Dazu später mehr. Fangen wir an mit der DLL:
[codesyntax lang=“cpp“]BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
// to avoid compiler lvl4 warnings
LPVOID lpDummy = lpReserved;
lpDummy = NULL;
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH: InitInstance(hModule); break;
case DLL_PROCESS_DETACH: ExitInstance(); break;
case DLL_THREAD_ATTACH: break;
case DLL_THREAD_DETACH: break;
}
return TRUE;
}[/codesyntax]
Die Entrypoint der DLL. Nichts besonderes. In InitInstance werden ein paar globale Variablen mit NULL initialisiert und bei ExitInstance werden Resourcen freigegeben.
[codesyntax lang=“cpp“]// Exported function (faking d3d8.dll’s one-and-only export)
IDirect3D8* WINAPI Direct3DCreate8(UINT SDKVersion)
{
if (!gl_hOriginalDll) LoadOriginalDll();
typedef IDirect3D8 *(WINAPI* D3D8_Type)(UINT SDKVersion);
D3D8_Type D3DCreate8_fn = (D3D8_Type) GetProcAddress( gl_hOriginalDll, “Direct3DCreate8″);
// Debug
if (!D3DCreate8_fn)
{
OutputDebugString(“PROXYDLL: Pointer to original D3DCreate8 function not received ERROR ****\r\n”);
::ExitProcess(0); // exit the hard way
}
IDirect3D8 *pIDirect3D8_orig = D3DCreate8_fn(SDKVersion);
gl_pmyIDirect3D8 = new myIDirect3D8(pIDirect3D8_orig);
return (gl_pmyIDirect3D8);
}[/codesyntax]
Als erstes wird in der Funktion LoadOriginalDll die originale d3d8.dll direkt aus dem system32 Ordner geladen. Einfach nur LoadLibrary, GetProcAdress und dann in einem globalen Funktionszeiger abspeichern. Dann erstellen wir uns ein eigenes Direct3D8 Interface, an das wir später alle Funktionsaufrufe weiterleiten können. Dieses wird in einer eigenen Klasse gekapselt, die von dem Interface erbt. Schließlich geben wir dann unser eigenes Interface-Objekt zurück, so dass alle Funktionsaufrufe des Spiel daran gerichtet werden. Die Funktion ist nicht als extern C deklariert. Dafür liegt eine Definitionsdatei bei, die die Funktion eindeutig exportiert. Was jetzt natürlich interessiert ist, wie unser eigenes Interface implementiert ist. Das ist sehr einfach:
[codesyntax lang=“cpp“]// myIDirect3D8.h
#pragma once
class myIDirect3D8 : public IDirect3D8
{
public:
myIDirect3D8(IDirect3D8 *pOriginal);
virtual ~myIDirect3D8();
// START: The original DX8.1a function definitions
HRESULT __stdcall QueryInterface(REFIID riid, void** ppvObj);
ULONG __stdcall AddRef(void);
ULONG __stdcall Release(void);
HRESULT __stdcall RegisterSoftwareDevice(void* pInitializeFunction);
UINT __stdcall GetAdapterCount(void);
HRESULT __stdcall GetAdapterIdentifier(UINT Adapter,DWORD Flags,D3DADAPTER_IDENTIFIER8* pIdentifier);
UINT __stdcall GetAdapterModeCount(UINT Adapter);
HRESULT __stdcall EnumAdapterModes(UINT Adapter,UINT Mode,D3DDISPLAYMODE* pMode);
HRESULT __stdcall GetAdapterDisplayMode( UINT Adapter,D3DDISPLAYMODE* pMode);
HRESULT __stdcall CheckDeviceType(UINT Adapter,D3DDEVTYPE CheckType,D3DFORMAT DisplayFormat,D3DFORMAT BackBufferFormat,BOOL Windowed);
HRESULT __stdcall CheckDeviceFormat(UINT Adapter,D3DDEVTYPE DeviceType,D3DFORMAT AdapterFormat,DWORD Usage,D3DRESOURCETYPE RType,D3DFORMAT CheckFormat);
HRESULT __stdcall CheckDeviceMultiSampleType(UINT Adapter,D3DDEVTYPE DeviceType,D3DFORMAT SurfaceFormat,BOOL Windowed,D3DMULTISAMPLE_TYPE MultiSampleType);
HRESULT __stdcall CheckDepthStencilMatch(UINT Adapter,D3DDEVTYPE DeviceType,D3DFORMAT AdapterFormat,D3DFORMAT RenderTargetFormat,D3DFORMAT DepthStencilFormat);
HRESULT __stdcall GetDeviceCaps(UINT Adapter,D3DDEVTYPE DeviceType,D3DCAPS8* pCaps);
HMONITOR __stdcall GetAdapterMonitor(UINT Adapter);
HRESULT __stdcall CreateDevice(UINT Adapter,D3DDEVTYPE DeviceType,HWND hFocusWindow,DWORD BehaviorFlags,D3DPRESENT_PARAMETERS* pPresentationParameters,IDirect3DDevice8** ppReturnedDeviceInterface);
// END: The original DX8,1a functions definitions
private:
IDirect3D8 * m_pIDirect3D8;
};
[/codesyntax]
Wir bieten einen Konstruktor mit einem IDirect3D8 Zeiger an, über den wir dann später die originalen Funktionen aufrufen können. Wir erben von dem IDirect3D8 Interface, damit wir unser Objekt auch bei der Direct3DCreate8 Funktion zurückgeben können. Und schließlich werden in der Header Datei sämtliche Funktionen deklariert, die unsere Klasse anbieten muss. Die können aus dem d3d8.h Header entnommen werden. Eine saumäßige Tipparbeit ist das.
Die Implementierungen der Funktionen sind bis auf einige Ausnahmen genauso einfach gestrickt:
[codesyntax lang=“cpp“]HRESULT __stdcall myIDirect3D8::GetAdapterIdentifier(UINT Adapter,DWORD Flags,D3DADAPTER_IDENTIFIER8* pIdentifier)
{
return(m_pIDirect3D8->GetAdapterIdentifier( Adapter, Flags, pIdentifier));
}
UINT __stdcall myIDirect3D8::GetAdapterModeCount(UINT Adapter)
{
return(m_pIDirect3D8->GetAdapterModeCount(Adapter));
}
HRESULT __stdcall myIDirect3D8::EnumAdapterModes(UINT Adapter,UINT Mode,D3DDISPLAYMODE* pMode)
{
return(m_pIDirect3D8->EnumAdapterModes( Adapter, Mode, pMode) );
}[/codesyntax]
Einfach oder? Einfach nur den Rückgabewert der Originalfunktion zurückgeben. An einigen Stellen muss man jedoch aufpassen:
[codesyntax lang=“cpp“]HRESULT __stdcall myIDirect3D8::QueryInterface(REFIID riid, void** ppvObj)
{
*ppvObj = NULL;
HRESULT hRes = m_pIDirect3D8->QueryInterface(riid, ppvObj);
if (hRes == NOERROR)
{
*ppvObj = this;
}
return hRes;
}
ULONG __stdcall myIDirect3D8::Release()
{
OutputDebugString(“PROXYDLL: myIDirect3D8::Release\r\n”);
extern myIDirect3D8* gl_pmyIDirect3D8;
ULONG count = m_pIDirect3D8->Release();
if (count == 0)
{
gl_pmyIDirect3D8 = NULL;
delete(this);
}
return(count);
}
[/codesyntax]
Bei der COM-typischen Funktion QueryInterface sollten wir natürlich unser eigenes Interface zurückgeben und bei der Release Funktion müssen wir erst alle Objekte, die wir erstellt haben sollten löschen bevor wir das originale Release() aufrufen, weil sonst der Referenzzähler nicht auf 0 geht und das Objekt nicht gelöscht wird. Für jedes Objekt, das wir erstellen, wird der Refcounter +1 gezählt und wenn ein Objekt gelöscht wird -1. Das D3D Interface wird nur dann richtig freigegeben, wenn der Refcounter 0 ist. Aber das sind COM-Geschichten. Dazu könnt ihr euch selber was nachlesen.
Wichtig ist folgender Teil. Wir haben zwar jetzt unser eigenes Direct3D Interface zwischengeschaltet, aber damit können wir ja noch nichts über das Spiel drüber malen. Ihr könnt übrigens ein paar Messageboxes einbauen oder euch die Debug Informationen anschauen (DebugView). Dann werdet ihr sehen, dass es schon funktioniert. Egal, wir brauchen jetzt noch unser eigenes Direct3DDevice8, welches das Spiel über folgende Funktion erhält:
[codesyntax lang=“cpp“]HRESULT __stdcall myIDirect3D8::CreateDevice(UINT Adapter,D3DDEVTYPE DeviceType,HWND hFocusWindow,DWORD BehaviorFlags,D3DPRESENT_PARAMETERS* pPresentationParameters,IDirect3DDevice8** ppReturnedDeviceInterface)
{
// global var
extern myIDirect3DDevice8* gl_pmyIDirect3DDevice8;
HRESULT hres = m_pIDirect3D8->CreateDevice( Adapter, DeviceType, hFocusWindow, BehaviorFlags, pPresentationParameters, ppReturnedDeviceInterface);
gl_pmyIDirect3DDevice8 = new myIDirect3DDevice8(*ppReturnedDeviceInterface);
*ppReturnedDeviceInterface = gl_pmyIDirect3DDevice8;
return(hres);
}[/codesyntax]
Genau die gleiche Geschichte wie in der Direct3DCreate8 Funktion, die unsere DLL exportiert. Wir holen uns einen echten Zeiger auf ein echtes Interface und geben unsere eigene Direct3DDevice8 Implementierung, die vom Interface erbt, zurück. Bei der Klasse müssen wir nochmal das gleiche Spielchen machen wie bei unser Direct3D Klasse. Das heißt: Alle Funktionen nach außen hin anbieten und an das echte Device weiterleiten. Ich denke, das brauche ich nicht extra nochmal explizit aufführen.
Kommen wir also zum letzten und schönsten Teil dieses Artikels. Das eigentliche Zeichnen. Es hat jetzt weniger mit der Technik von oben, sondern mehr mit ganz normalem Direct3D zu tun. Im Konstruktor unseres eigenen Direct3DDevice8 speichern wir einen Zeiger auf das Original und initialisieren ein paar Variablen:
[codesyntax lang=“cpp“]myIDirect3DDevice8::myIDirect3DDevice8(IDirect3DDevice8* pOriginal)
{
m_pIDirect3DDevice8 = pOriginal; // store the pointer to original object
ticks = GetTickCount();
frames = 0;
fps = 0;
bShow = FALSE;
OutputDebugString(“PROXYDLL: myIDirect3DDevice8\r\n”);
}[/codesyntax]
Aufpassen muss man natürlich wieder bei QueryInterface und Release. Zudem müssen wir jetzt auch noch die Present Funktion ein wenig abändern, da wir ja unsere schöne Leiste überblenden wollen:
[codesyntax lang=“cpp“]HRESULT __stdcall myIDirect3DDevice8::Present(CONST RECT* pSourceRect,CONST RECT* pDestRect,HWND hDestWindowOverride,CONST RGNDATA* pDirtyRegion)
{
this->ShowWeAreHere();
HRESULT hres = m_pIDirect3DDevice8->Present( pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion);
return (hres);
}[/codesyntax]
Wir rufen erst unsere Funktion ShowWeAreHere() auf und dann erst Present(). In unserer Funktion können wir dann noch schön unsere eigenen Sachen auf den Backbuffer blitten:
[codesyntax lang=“cpp“]void myIDirect3DDevice8::ShowWeAreHere(void)
{
if((GetAsyncKeyState(0x4A) & 1) && (GetAsyncKeyState(VK_RCONTROL) & 1)) // J-Key
bShow = !bShow;
if(!bShow)
return;
// Create a D3DX font object
ID3DXFont *m_font;
LOGFONT font_t;
memset(&font_t, 0, sizeof(font_t));
HDC hDC = GetDC(NULL);
font_t.lfHeight = -MulDiv(10, ::GetDeviceCaps(hDC, LOGPIXELSY), 72);
ReleaseDC(NULL, hDC);
font_t.lfWidth = 0;
font_t.lfEscapement = 0;
font_t.lfOrientation = 0;
font_t.lfWeight = FW_BOLD;
font_t.lfItalic = FALSE;
font_t.lfUnderline = FALSE;
font_t.lfStrikeOut = FALSE;
font_t.lfCharSet = DEFAULT_CHARSET;
font_t.lfOutPrecision = 0;
font_t.lfClipPrecision = 0;
font_t.lfQuality = DEFAULT_QUALITY;
font_t.lfPitchAndFamily = FF_DONTCARE;
strcpy_s(font_t.lfFaceName, 6, “Arial”);
if(S_OK != D3DXCreateFontIndirect( this, &font_t, &m_font ))
{
m_font = NULL;
OutputDebugString(“PROXYDLL: Font could not be created.\r\n”);
return;
}
// get screen resolution
D3DDISPLAYMODE d3dMode;
memset(&d3dMode, 0, sizeof(d3dMode));
if(S_OK != this->GetDisplayMode(&d3dMode))
return;
UINT width = d3dMode.Width;
RECT rct;
rct.left = 0;
rct.top = 0;
rct.right = width; // 50
rct.bottom = 15;
D3DRECT dx_rct;
dx_rct.x1 = rct.left;
dx_rct.y1 = rct.top;
dx_rct.x2 = rct.right;
dx_rct.y2 = rct.bottom;
// Create a colour for the text – in this case red
D3DCOLOR fontColor = D3DCOLOR_ARGB(255,255,0,0);
// clear
m_pIDirect3DDevice8->Clear(1, &dx_rct, D3DCLEAR_TARGET, D3DCOLOR_ARGB(255,255,255,0),0 ,0);
// get fps
frames++;
if(GetTickCount() – ticks > 1000)
{
ticks = GetTickCount();
fps = frames;
frames = 0;
}
/***********************************************
************* GET CURRENT WINAMP TITLE ********/
LPCSTR szClassName = “Winamp v1.x”;
LPCSTR szTitleEnd = ” – Winamp”;
CHAR szToDraw[512];
strcpy_s(szToDraw, 512, “not running”);
HWND hwnd = FindWindow(szClassName, NULL);
if(hwnd != NULL)
{
CHAR buffer[512];
DWORD length = GetWindowText(hwnd, buffer, 512);
strcpy_s(szToDraw, 512, “unknown title”);
if(length > 0)
{
char* pch = strstr(buffer, szTitleEnd);
char temp = *pch;
*pch = ‘\0′;
strcpy_s(szToDraw, 512, buffer);
*pch = temp;
pch += 9;
strcat_s(szToDraw, 512, pch);
}
}
/********************** END *******************
***********************************************/
char buf[1024];
sprintf_s(buf, 1024, “FPS: %d Winamp: %s TASTELESS ROCKS! greets jx”, fps, szToDraw);
// Draw some text
m_font->DrawText(buf, -1, &rct, 0, fontColor);
OutputDebugString(“PROXYDLL: ShowWeAreHere\r\n”);
m_font->Release();
}[/codesyntax]
Jaja… GetAsyncKeyState… Egal. Wir prüfen, ob die J-Taste (sie steht für Jeix und juhuu) und invertieren dementsprechend den Wert der bShow Bool-Variable. Ist sie true, zeigen wir unsere Leiste an. Dazu erstellen wir als erstes eine Font, damit wir Text zeichnen können. Daraufhin berechnen wir die Screen-Breite, damit die Leiste über die ganzen Bildschirm geht. Dann berechnen wir mit einem kleinen Pfusch die FPS (das brauche ich denke ich nicht erklären), lesen noch einmal eben den aktuellen Winamp Titel aus (ihr kennt das ja. Man hat 5000 Lieder und hört ein gutes und will wissen welches es ist) setzen alles in einem String zusammen und rufen DrawText auf. Fertig ist die Leiste.
Fazit
Mit dieser Technik kann man einiges erreichen. Man kann wie Xfire oder Steam ein Chat, Webbrowser oder alles mögliche über alle DirectX Anwendungen übermalen und somit ein interaktives Spiel gestalten. Oder der gute alte Wallhack. Man kann natürlich auch die Polygone zählen, etc. und die Spielperformance bestimmen. Ich hoffe ihr habt wenigstens etwas davon verstanden, um dies jedoch in vollem Umfang verstehen zu können, solltet ihr euch das Projekt in seiner Gänze downloaden und in dem Editor eurer Wahl noch einmal Schritt für Schritt durchgehen.
Happy hacking!
greets jx
Die Projektdateien: direct3d_hooking_tasteless