halihalo sehr geehrte leser,
heute nehmen wir mal das thema detours ein wenig durch.
zu aller erst wollen wir mal schauen was das überhaupt ist.
hier ist ein kleines beispiel eines programmes wie es in assembler aussieht:
theoretisch intercepted eine detour einen funktionsaufruf, fängt ihn so gesehen ab und leitet ihn um.Code:7C801D77 > 8BFF MOV EDI,EDI 7C801D79 55 PUSH EBP 7C801D7A 8BEC MOV EBP,ESP 7C801D7C 837D 08 00 CMP DWORD PTR SS:[EBP+8],0 7C801D80 53 PUSH EBX 7C801D81 56 PUSH ESI 7C801D82 74 14 JE SHORT kernel32.7C801D98
praktisch gesehen wird also meistens eine jmp instruction an das erste byte der funktion geschrieben, mit darauf folgender adresse. wodurch wir insgesamt auf 5 bytes kommen die überschrieben werden. jedoch muss man häufig auch noch ein paar bytes mehr mit nop's überschreiben, da man ansonsten irgendeinen befehl zur hälfte überschreibt.
also würde die funktion in asm jetzt so aussehen:
an der adresse zu der wir springen, können wir jetzt irgendeinen code ausführen, müssen jedoch bevor wir zurück zur original adresse ( + 5 bytes, wollen ja nicht nochmal unsere zwischengeschobene funktion aufrufen -> endless loop ) noch die opcodes, sprich die befehle ausführen die wir überschrieben haben. dies würde dann in etwas so aussehen:Code:7C801D77 E9 ADRESSE JMP ADRESSE 7C801D7C 837D 08 00 CMP DWORD PTR SS:[EBP+8],0 7C801D80 53 PUSH EBX 7C801D81 56 PUSH ESI 7C801D82 74 14 JE SHORT kernel32.7C801D98
doch genug zur theorie, jetzt folgt ein beispiel wie man eine funktion in c schreiben kann, die genau dies für uns tut.Code:5C801D77 > 8BFF MOV EDI,EDI ; überschrieben opcodes 5C801D79 55 PUSH EBP 5C801D7A 8BEC MOV EBP,ESP 7C801D7C E9 ORIGADDRESSE JMP ORIGADRESSE ; jmp back zur orig. adresse + 5 bytes
zuerst alloziieren wir uns ein wenig speicher für unser gateway im betroffenen prozess. dafür verwenden wir virtualalloc. die funktion hat 4 parameter, der erste ist die startadresse. dort können wir getrost 0 eingeben.
der zweite gibt an wieviel platz wir benötigen, in dem fall wäre das die größe der überschriebenen bytes + 5 bytes für die jmp instruction zur orig. funktion.
der dritte parameter ist der typ der speicher alloziation.
dort hätten wir einmal mem_reserve, womit wir uns ein wenig virtual speicher des prozesses reservieren und dann mem_commit womit wir den speicher mit 0 memsetten. der vierte parameter ist der protection typ des speicherbereiches, bzw. der page. dort benutzen wir PAGE_EXECUTE_READWRITE.
wenn die funktion failed gibt sie 0 zurück, ansonsten die adresse des reservierten speichers. um genauere infos über den grund warum die funktion einen fehler verursacht hat zu bekommen, sollte man sich anschauen was getlasterror für einen fehler index ausspuckt.
joah, dann holen wir uns read und write rechte für die bytes die wir überschreiben möchten. wir kopieren die bytes erstmal an die adresse unseres gateways. dafür benutzen wir entweder memcpy ( wenn man context intern arbeitet, z.b. mit einer geladenen dll ) oder readprocessmemory und writeprocessmemory ( wenn man z.b. aus einer externen application eine detour setzt ). in diesem beispiel mache ich es mit memcpy, da es einfach eine zeile weniger code istCode:DWORD dwDetourFunction( DWORD dwAddressOfFunctionToIntercept, DWORD dwAddressOfFunctionToJmp, DWORD dwNumberOfOverwrittenOpcodes ) { char szErrorLog[256]; DWORD dwReservedMemorySpace = 0x0; dwReservedMemorySpace = ( DWORD )VirtualAlloc( 0, ( dwNumberOfOverwrittenOpcodes + 0x5 ), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE ); if( dwReservedMemorySpace == NULL ) { sprintf( szErrorLog, "dwDetourFunction -> VirtualAlloc failed -> GetLastError: %d", GetLastError( ) ); MessageBoxA( GetForegroundWindow( ), szErrorLog, "ERROR", MB_ICONERROR | MB_OK ); return 0x0; } }.
aber zuerst müssen wir wie gesagt die rechte setzen. dazu benutzen wir virtualprotect. hier gibt es wieder 4 parameter, der erste gibt die adresse an von der aus beginnend die speicherrechte geändert werden sollen. jedoch ist davon dann die ganze page betroffen. der zweite gibt wiederrum die menge der bytes an die betroffen sind, falls diese z.b. in eine zweite page reichen ist diese auch komplett davon getroffen. der dritte parameter gibt den protection typ an und der vierte gibt den alten, sprich den aktuellen protection typ zurück. wenn die funktion failed gibt diese widerum auch 0 zurück.
jetzt verwenden wir memcpy um die originalen opcodes in unser gateway zu kopieren. hier gibt es 3 parameter. der erste gibt die source adresse an, der zweite die destination adresse und der dritte die anzahl der bytes die kopiert werden sollen.Code:DWORD dwDetourFunction( DWORD dwAddressOfFunctionToIntercept, DWORD dwAddressOfFunctionToJmp, DWORD dwNumberOfOverwrittenOpcodes ) { char szErrorLog[256]; DWORD dwReservedMemorySpace = 0x0; DWORD dwOldProtection = 0x0; dwReservedMemorySpace = ( DWORD )VirtualAlloc( 0, ( dwNumberOfOverwrittenOpcodes + 0x5 ), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE ); if( dwReservedMemorySpace == NULL ) { sprintf( szErrorLog, "dwDetourFunction -> VirtualAlloc failed -> GetLastError: %d", GetLastError( ) ); MessageBoxA( GetForegroundWindow( ), szErrorLog, "ERROR", MB_ICONERROR | MB_OK ); return 0x0; } if( VirtualProtect( dwAddressOfFunctionToIntercept, dwNumberOfOverwrittenOpcodes, PAGE_READWRITE, &dwOldProtect ) == 0 ) { sprintf( szErrorLog, "dwDetourFunction -> VirtualProtect failed -> GetLastError: %d", GetLastError( ) ); MessageBoxA( GetForegroundWindow( ), szErrorLog, "ERROR", MB_ICONERROR | MB_OK ); return 0x0; } }
gut da wir unser gateway nun mit den originalen opcodes gefüllt haben, schreiben wir die jmp instruction zur originalen funktion hinter den code den wir eben reinkopiert haben. da man allerdings mit relativen adressen arbeiten muss, müssen wir dazu eine kleine berechnung benutzen.Code:DWORD dwDetourFunction( DWORD dwAddressOfFunctionToIntercept, DWORD dwAddressOfFunctionToJmp, DWORD dwNumberOfOverwrittenOpcodes ) { char szErrorLog[256]; DWORD dwReservedMemorySpace = 0x0; DWORD dwOldProtection = 0x0; dwReservedMemorySpace = ( DWORD )VirtualAlloc( 0, ( dwNumberOfOverwrittenOpcodes + 0x5 ), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE ); if( dwReservedMemorySpace == NULL ) { sprintf( szErrorLog, "dwDetourFunction -> VirtualAlloc failed -> GetLastError: %d", GetLastError( ) ); MessageBoxA( GetForegroundWindow( ), szErrorLog, "ERROR", MB_ICONERROR | MB_OK ); return 0x0; } if( VirtualProtect( dwAddressOfFunctionToIntercept, dwNumberOfOverwrittenOpcodes, PAGE_READWRITE, &dwOldProtect ) == 0 ) { sprintf( szErrorLog, "dwDetourFunction -> VirtualProtect failed -> GetLastError: %d", GetLastError( ) ); MessageBoxA( GetForegroundWindow( ), szErrorLog, "ERROR", MB_ICONERROR | MB_OK ); return 0x0; } memcpy( dwAddressOfFunctionToIntercept, dwReserveMemorySpace, dwNumberOfOverwrittenBytes ); }
diese sieht wie folgt aus -> adressewohingesprungenwird - minusadressevonderweggesprungenwird.
in dem beispiel arbeite ich mit pointern, man kann dazu aber auch genauso gut memcpy oder writeprocessmemory benutzen.
jetzt können wir auch gleich den funktionsaufruf ansich umleiten, dazu gehen wir genau wie bei unserem gateway jmp vor.Code:DWORD dwDetourFunction( DWORD dwAddressOfFunctionToIntercept, DWORD dwAddressOfFunctionToJmp, DWORD dwNumberOfOverwrittenOpcodes ) { char szErrorLog[256]; DWORD dwReservedMemorySpace = 0x0; DWORD dwOldProtection = 0x0; dwReservedMemorySpace = ( DWORD )VirtualAlloc( 0, ( dwNumberOfOverwrittenOpcodes + 0x5 ), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE ); if( dwReservedMemorySpace == NULL ) { sprintf( szErrorLog, "dwDetourFunction -> VirtualAlloc failed -> GetLastError: %d", GetLastError( ) ); MessageBoxA( GetForegroundWindow( ), szErrorLog, "ERROR", MB_ICONERROR | MB_OK ); return 0x0; } if( VirtualProtect( dwAddressOfFunctionToIntercept, dwNumberOfOverwrittenOpcodes, PAGE_READWRITE, &dwOldProtect ) == 0 ) { sprintf( szErrorLog, "dwDetourFunction -> VirtualProtect failed -> GetLastError: %d", GetLastError( ) ); MessageBoxA( GetForegroundWindow( ), szErrorLog, "ERROR", MB_ICONERROR | MB_OK ); return 0x0; } memcpy( dwAddressOfFunctionToIntercept, dwReserveMemorySpace, dwNumberOfOverwrittenBytes ); //jmp opcode [E9] *( PBYTE )( dwReservedMemorySpace + dwNumberOfOverwrittenBytes ) = 0xE9; //im debugger würde das nun wie folgt aussehen: //0xADRESSE orig opcode; //0xADRESSE3 orig opcode; //0xADRESSE5 orig opcode; //0xADRESSE7 jmp; //jetzt folgt die angabe der adresse an die gesprungen werden soll, dafür benutzen wir die berechnung wie ich sie vorhin erklärt habe *( PDWORD )( dwReservedMemorySpace + dwNumberOfOverwrittenBytes + 0x1 ) = ( ( dwAddressOfFunctionToIntercept + dwNumberOfOverwrittenBytes ) - ( dwReservedMemorySpace + dwNumberOvOverwrittenBytes + 0x5 ) ); //einige werden sich sicher fragen warum da jetzt 5 bytes dazuadiert werden, das liegt einfach daran, das der jmp erst am ende ausgeführt wird, sprich nach der letzten ziffer der adressenangabe. und da ein jmp 1 opcode ist und eine adresse aus 4 bytes besteht, ist der sprung punkt 5 bytes weiter. //im debugger schaut das nun so aus: //im debugger würde das nun wie folgt aussehen //0xADRESSE 235253 origopcodes; //0xADRESSE3 2351 origopcodes; //0xADRESSE5 3452 origopcodes; //0xADRESSE7 e9 23456321 jmp relativadresse; }
so jetzt kann es vorkommen, das wenn man einen befehl z.b. zur hälfte überschreibt, das wir die restlichen opcodes noch mit nops belegen müssen.Code:DWORD dwDetourFunction( DWORD dwAddressOfFunctionToIntercept, DWORD dwAddressOfFunctionToJmp, DWORD dwNumberOfOverwrittenOpcodes ) { char szErrorLog[256]; DWORD dwReservedMemorySpace = 0x0; DWORD dwOldProtection = 0x0; dwReservedMemorySpace = ( DWORD )VirtualAlloc( 0, ( dwNumberOfOverwrittenOpcodes + 0x5 ), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE ); if( dwReservedMemorySpace == NULL ) { sprintf( szErrorLog, "dwDetourFunction -> VirtualAlloc failed -> GetLastError: %d", GetLastError( ) ); MessageBoxA( GetForegroundWindow( ), szErrorLog, "ERROR", MB_ICONERROR | MB_OK ); return 0x0; } if( VirtualProtect( dwAddressOfFunctionToIntercept, dwNumberOfOverwrittenOpcodes, PAGE_READWRITE, &dwOldProtect ) == 0 ) { sprintf( szErrorLog, "dwDetourFunction -> VirtualProtect failed -> GetLastError: %d", GetLastError( ) ); MessageBoxA( GetForegroundWindow( ), szErrorLog, "ERROR", MB_ICONERROR | MB_OK ); return 0x0; } memcpy( dwAddressOfFunctionToIntercept, dwReserveMemorySpace, dwNumberOfOverwrittenBytes ); //jmp opcode [E9] *( PBYTE )( dwReservedMemorySpace + dwNumberOfOverwrittenBytes ) = 0xE9; //im debugger würde das nun wie folgt aussehen: //0xADRESSE orig opcode; //0xADRESSE3 orig opcode; //0xADRESSE5 orig opcode; //0xADRESSE7 jmp; //jetzt folgt die angabe der adresse an die gesprungen werden soll, dafür benutzen wir die berechnung wie ich sie vorhin erklärt habe *( PDWORD )( dwReservedMemorySpace + dwNumberOfOverwrittenBytes + 0x1 ) = ( ( dwAddressOfFunctionToIntercept + dwNumberOfOverwrittenBytes ) - ( dwReservedMemorySpace + dwNumberOvOverwrittenBytes + 0x5 ) ); //einige werden sich sicher fragen warum da jetzt 5 bytes dazuadiert werden, das liegt einfach daran, das der jmp erst am ende ausgeführt wird, sprich nach der letzten ziffer der adressenangabe. und da ein jmp 1 opcode ist und eine adresse aus 4 bytes besteht, ist der sprung punkt 5 bytes weiter. //im debugger schaut das nun so aus: //im debugger würde das nun wie folgt aussehen //0xADRESSE 235253 origopcodes; //0xADRESSE3 2351 origopcodes; //0xADRESSE5 3452 origopcodes; //0xADRESSE7 e9 23456321 jmp relativadresse; //HOOK!!!!!!!!!!!!!!! //jmp instruction setzen *( PBYTE )dwAddressOfFunctionToIntercept = 0xE9; //rva berechnen und setzen *( PDWORD )( dwAddressOfFunctionToIntecept + 0x1 ) = ( dwAddressOfFunctionToJmp - ( dwAddressOfFunctionToIntercept + 0x5 ) ); }
dazu können wir eine einfache forschleife verwenden:
letzten endes setzen wir wieder die originalen speicherrechte und returnen die adresse des gateways, damit wir dieses im späteren verlauf aufrufen können um die originale funktion ohne unsere detour zu callen.Code:DWORD dwDetourFunction( DWORD dwAddressOfFunctionToIntercept, DWORD dwAddressOfFunctionToJmp, DWORD dwNumberOfOverwrittenOpcodes ) { char szErrorLog[256]; DWORD dwReservedMemorySpace = 0x0; DWORD dwOldProtection = 0x0; dwReservedMemorySpace = ( DWORD )VirtualAlloc( 0, ( dwNumberOfOverwrittenOpcodes + 0x5 ), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE ); if( dwReservedMemorySpace == NULL ) { sprintf( szErrorLog, "dwDetourFunction -> VirtualAlloc failed -> GetLastError: %d", GetLastError( ) ); MessageBoxA( GetForegroundWindow( ), szErrorLog, "ERROR", MB_ICONERROR | MB_OK ); return 0x0; } if( VirtualProtect( dwAddressOfFunctionToIntercept, dwNumberOfOverwrittenOpcodes, PAGE_READWRITE, &dwOldProtect ) == 0 ) { sprintf( szErrorLog, "dwDetourFunction -> VirtualProtect failed -> GetLastError: %d", GetLastError( ) ); MessageBoxA( GetForegroundWindow( ), szErrorLog, "ERROR", MB_ICONERROR | MB_OK ); return 0x0; } memcpy( dwAddressOfFunctionToIntercept, dwReserveMemorySpace, dwNumberOfOverwrittenBytes ); //jmp opcode [E9] *( PBYTE )( dwReservedMemorySpace + dwNumberOfOverwrittenBytes ) = 0xE9; //im debugger würde das nun wie folgt aussehen: //0xADRESSE orig opcode; //0xADRESSE3 orig opcode; //0xADRESSE5 orig opcode; //0xADRESSE7 jmp; //jetzt folgt die angabe der adresse an die gesprungen werden soll, dafür benutzen wir die berechnung wie ich sie vorhin erklärt habe *( PDWORD )( dwReservedMemorySpace + dwNumberOfOverwrittenBytes + 0x1 ) = ( ( dwAddressOfFunctionToIntercept + dwNumberOfOverwrittenBytes ) - ( dwReservedMemorySpace + dwNumberOvOverwrittenBytes + 0x5 ) ); //einige werden sich sicher fragen warum da jetzt 5 bytes dazuadiert werden, das liegt einfach daran, das der jmp erst am ende ausgeführt wird, sprich nach der letzten ziffer der adressenangabe. und da ein jmp 1 opcode ist und eine adresse aus 4 bytes besteht, ist der sprung punkt 5 bytes weiter. //im debugger schaut das nun so aus: //im debugger würde das nun wie folgt aussehen //0xADRESSE 235253 origopcodes; //0xADRESSE3 2351 origopcodes; //0xADRESSE5 3452 origopcodes; //0xADRESSE7 e9 23456321 jmp relativadresse; //HOOK!!!!!!!!!!!!!!! //jmp instruction setzen *( PBYTE )dwAddressOfFunctionToIntercept = 0xE9; //rva berechnen und setzen *( PDWORD )( dwAddressOfFunctionToIntecept + 0x1 ) = ( dwAddressOfFunctionToJmp - ( dwAddressOfFunctionToIntercept + 0x5 ) ); //nops setzen?!?! for( DWORD dwNOP = 0x5; dwNOP < dwNumberOfOverwrittenBytes; dwNOP += 0x1 ) { //opcode von nop ist 90 *( PBYTE )( dwAddressOfFunctionToIntercept + dwNOP ) = 0x90; } }
und hier ist noch ein kleines anwendungsbeispiel für einen loadlibrarya hook:Code:DWORD dwDetourFunction( DWORD dwAddressOfFunctionToIntercept, DWORD dwAddressOfFunctionToJmp, DWORD dwNumberOfOverwrittenOpcodes ) { char szErrorLog[256]; DWORD dwReservedMemorySpace = 0x0; DWORD dwOldProtection = 0x0; dwReservedMemorySpace = ( DWORD )VirtualAlloc( 0, ( dwNumberOfOverwrittenOpcodes + 0x5 ), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE ); if( dwReservedMemorySpace == NULL ) { sprintf( szErrorLog, "dwDetourFunction -> VirtualAlloc failed -> GetLastError: %d", GetLastError( ) ); MessageBoxA( GetForegroundWindow( ), szErrorLog, "ERROR", MB_ICONERROR | MB_OK ); return 0x0; } if( VirtualProtect( dwAddressOfFunctionToIntercept, dwNumberOfOverwrittenOpcodes, PAGE_READWRITE, &dwOldProtect ) == 0 ) { sprintf( szErrorLog, "dwDetourFunction -> VirtualProtect failed -> GetLastError: %d", GetLastError( ) ); MessageBoxA( GetForegroundWindow( ), szErrorLog, "ERROR", MB_ICONERROR | MB_OK ); return 0x0; } memcpy( dwAddressOfFunctionToIntercept, dwReserveMemorySpace, dwNumberOfOverwrittenBytes ); //jmp opcode [E9] *( PBYTE )( dwReservedMemorySpace + dwNumberOfOverwrittenBytes ) = 0xE9; //im debugger würde das nun wie folgt aussehen: //0xADRESSE orig opcode; //0xADRESSE3 orig opcode; //0xADRESSE5 orig opcode; //0xADRESSE7 jmp; //jetzt folgt die angabe der adresse an die gesprungen werden soll, dafür benutzen wir die berechnung wie ich sie vorhin erklärt habe *( PDWORD )( dwReservedMemorySpace + dwNumberOfOverwrittenBytes + 0x1 ) = ( ( dwAddressOfFunctionToIntercept + dwNumberOfOverwrittenBytes ) - ( dwReservedMemorySpace + dwNumberOvOverwrittenBytes + 0x5 ) ); //einige werden sich sicher fragen warum da jetzt 5 bytes dazuadiert werden, das liegt einfach daran, das der jmp erst am ende ausgeführt wird, sprich nach der letzten ziffer der adressenangabe. und da ein jmp 1 opcode ist und eine adresse aus 4 bytes besteht, ist der sprung punkt 5 bytes weiter. //im debugger schaut das nun so aus: //im debugger würde das nun wie folgt aussehen //0xADRESSE 235253 origopcodes; //0xADRESSE3 2351 origopcodes; //0xADRESSE5 3452 origopcodes; //0xADRESSE7 e9 23456321 jmp relativadresse; //HOOK!!!!!!!!!!!!!!! //jmp instruction setzen *( PBYTE )dwAddressOfFunctionToIntercept = 0xE9; //rva berechnen und setzen *( PDWORD )( dwAddressOfFunctionToIntecept + 0x1 ) = ( dwAddressOfFunctionToJmp - ( dwAddressOfFunctionToIntercept + 0x5 ) ); //nops setzen?!?! for( DWORD dwNOP = 0x5; dwNOP < dwNumberOfOverwrittenBytes; dwNOP += 0x1 ) { //opcode von nop ist 90 *( PBYTE )( dwAddressOfFunctionToIntercept + dwNOP ) = 0x90; } //speicherrechte wiederherstellen if( VirtualProtect( dwAddressOfFunctionToIntercept, dwNumberOfOverwrittenBytes, dwOldProtect, new DWORD ) == 0 ) { sprintf( szErrorLog, "dwDetourFunction -> VirtualProtect failed -> GetLastError: %d", GetLastError( ) ); MessageBoxA( GetForegroundWindow( ), szErrorLog, "ERROR", MB_ICONERROR | MB_OK ); return 0x0; } //whats that ? TerminateProcess( GetCurrentProcess( ), 0 ); return dwReservedMemorySpace; }
ihr könnt den code so nicht zu 100% übernehmen, da ihr einige typecast errors bekommt und sich der prozess in dem ihr dies anwendet sofort schließen würde. will damit nur sicherstellen das ihr das grundprinzip auch einigermaßen verstanden habt.Code://typedefs erstellen typedef HMODULE ( WINAPI* LoadLibraryA_Typedef )( LPCTSTR lpFileName ); LoadLibraryA_Typedef LoadLibraryA_Gate = 0; //function die wir anstelle der echten loadlibrarya funktion callen HMODULE WINAPI LoadLibraryA_Detour( LPCTSTR lpFileName ); { //mache iwas mit params oder so //aufruf der originalen loadlibrarya return ( *LoadLibraryA_Gate )( lpFileName ); } //hooken der loadlibrarya funktion mit unserer detourfunction funktion //brauch ansich nur einmal gecalled zu werden, zumindest je nachdem wie der target prozess dies handled HANDLE hKernel32 = GetModuleHandle( "kernel32.dll" ); LoadLibraryA_Gate = ( LoadLibraryA_Typedef )dwDetourFunction( GetProcAddress( hKernel32, "LoadLibraryA" ), &LoadLibraryA_Detour, 0x5 ); if( LoadLibraryA_Gate == 0 ) { MessageBox( GetForegroundWindow( ), "CRITICAL ERROR, CANT HOOK LOADLIBRARYA, TERMINATING PROCESS", "BURNITLIKEFIRE", MB_ICONERROR | MB_OK ); TerminateProcess( GetCurrentProcess( ), 0 ); }
der code ist auch nicht getestet worden, hab ihn einfach frei hand mit hilfe vom msdn und ein paar überlegungen niedergeschrieben.
er sollte aber 100% gehen, da das prinzip ansich klar ist. es könnte wie gesagt nur ein paar compiler errors geben, z.b. wegen schreibfehlern oder den besagten typecasts.
und es gibt auch das eine schmankerl mit dem terminierenden prozess
demnächst gibts ein paar sachen zu iat und eat hooking.
doch jetzt muss klein steeno erstmal ins bettele


.
Zitieren
aber auch schön geschrieben mal sehn wann mein c++ und Co wieder geht und ich Zeit finde das zu testen




Lesezeichen